From f517a5a1d7c0766ed50f8dfd5ee69e65525ac744 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:57:54 +0800 Subject: [PATCH] test(javascript): consolidate source analysis and export regressions --- .../EnhancedLiteralTracing.test.ts | 31 --- .../JavaScriptModuleSpecifierPaths.test.ts | 57 +++--- .../JavaScriptReturnShapeProjection.test.ts | 166 ---------------- .../electronStaticAnalysisOptions.test.ts | 178 ++++-------------- ...scriptExportShapeComparisonSchemas.test.ts | 106 +---------- ...avascriptFeatureSeed.array_members.test.ts | 100 +++++----- .../javascript/javascriptRecovery.test.ts | 13 -- ...tSemanticAnalysis.builtin_defaults.test.ts | 42 +---- .../javascriptSemanticAnalysis.calls.test.ts | 13 -- ...criptSemanticAnalysis.member_depth.test.ts | 70 +++---- .../javascriptSemanticAnalysis.unions.test.ts | 56 ++++-- .../javascriptSemanticQuerySchemas.test.ts | 57 +----- .../javascriptSemanticTraversal.test.ts | 66 ------- ...ptStaticAnalysis.member_path_depth.test.ts | 5 +- ...javascriptStaticAnalysisSourceMaps.test.ts | 30 --- .../applicationEvidenceMcp.test.ts | 47 +---- .../applicationExportShapeMcp.test.ts | 127 +++++++++++-- tests/acceptance/applications/mcpHarness.ts | 39 ++++ 18 files changed, 352 insertions(+), 851 deletions(-) delete mode 100644 src/application/EnhancedLiteralTracing.test.ts delete mode 100644 src/application/javascript/JavaScriptReturnShapeProjection.test.ts delete mode 100644 src/domain/javascript/javascriptSemanticTraversal.test.ts create mode 100644 tests/acceptance/applications/mcpHarness.ts diff --git a/src/application/EnhancedLiteralTracing.test.ts b/src/application/EnhancedLiteralTracing.test.ts deleted file mode 100644 index 42e61974ed..0000000000 --- a/src/application/EnhancedLiteralTracing.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { ok } from "../domain/result.js"; -import { traceLiteralFeature } from "./EnhancedLiteralTracing.js"; - -describe("literal feature tracing", () => { - it("returns matched procedure and string facts with references", async () => { - const result = await traceLiteralFeature( - async (name) => { - if (name === "search_strings") - return ok([{ address: "0x1000", value: "launch feature" }]); - if (name === "search_procedures") - return ok([{ address: "0x2000", value: "launchFeature" }]); - if (name === "xrefs") return ok([]); - if (name === "resolve_containing_procedure") return ok(null); - throw new Error(`Unexpected analysis call: ${name}`); - }, - { query: "launch feature", case_sensitive: false }, - ); - expect(result).toMatchObject({ - ok: true, - value: { - matches: [ - { type: "string", address: "0x1000", value: "launch feature" }, - { type: "procedure", address: "0x2000", value: "launchFeature" }, - ], - references: [], - }, - }); - }); -}); diff --git a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts index 63f44337eb..ee5809771f 100644 --- a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts +++ b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts @@ -20,37 +20,46 @@ const filesFor = (paths: readonly string[]) => ]), ); -const cases = ["#", "?"].flatMap((punctuation) => - (["require", "import", undefined] as const).flatMap((moduleKind) => - (["literal", "stripped", "both"] as const).map((layout) => ({ - punctuation, - moduleKind, - layout, - })), - ), -); - describe("module specifier punctuation", () => { - it.each(cases)( - "resolves $moduleKind with $punctuation and $layout files", - ({ punctuation, moduleKind, layout }) => { + it.each(["#", "?"])( + "keeps CommonJS punctuation and strips URL suffixes for ESM (%s)", + (punctuation) => { const literal = `app/dep.cjs${punctuation}literal.cjs`; const stripped = "app/dep.cjs"; - const files = filesFor([ - ...(layout !== "stripped" ? [literal] : []), - ...(layout !== "literal" ? [stripped] : []), - ]); - const expected = moduleKind === "require" ? literal : stripped; - const result = resolveArtifactPathByContext({ + const commonJs = resolveArtifactPathByContext({ declaredPath: `./dep.cjs${punctuation}literal.cjs`, sourcePath: "app/main.js", context: "module-specifier", - files, - ...(moduleKind === undefined ? {} : { moduleKind }), + files: filesFor([literal, stripped]), + moduleKind: "require", }); - expect(result).toMatchObject({ - resolution_status: files.has(expected) ? "resolved" : "not-found", - resolved_path: files.has(expected) ? expected : null, + expect(commonJs).toMatchObject({ + resolution_status: "resolved", + resolved_path: literal, + }); + + const esm = resolveArtifactPathByContext({ + declaredPath: `./dep.cjs${punctuation}literal.cjs`, + sourcePath: "app/main.js", + context: "module-specifier", + files: filesFor([literal, stripped]), + moduleKind: "import", + }); + expect(esm).toMatchObject({ + resolution_status: "resolved", + resolved_path: stripped, + }); + + const noStrippedTarget = resolveArtifactPathByContext({ + declaredPath: `./dep.cjs${punctuation}literal.cjs`, + sourcePath: "app/main.js", + context: "module-specifier", + files: filesFor([literal]), + moduleKind: "import", + }); + expect(noStrippedTarget).toMatchObject({ + resolution_status: "not-found", + resolved_path: null, }); }, ); diff --git a/src/application/javascript/JavaScriptReturnShapeProjection.test.ts b/src/application/javascript/JavaScriptReturnShapeProjection.test.ts deleted file mode 100644 index 11d91c02f9..0000000000 --- a/src/application/javascript/JavaScriptReturnShapeProjection.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; -import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; -import { projectJavaScriptExportReturnShapes } from "./JavaScriptReturnShapeProjection.js"; - -const projectReturns = (body: string) => { - const ir = analyzeJavaScriptSemantics(`export function result() { ${body} }`); - const link = ir.moduleLinks.find( - ({ exportedName }) => exportedName === "result", - ); - if (link === undefined) throw new Error("Expected the result export"); - const projected = projectJavaScriptExportReturnShapes({ - ir, - link, - modulePath: "fixture.js", - baseCoverage: completeApplicationCoverage(), - }); - if (projected === null) throw new Error("Expected exported return shapes"); - return projected; -}; - -const unknownBranchField = { - path: "", - state: "unknown", - value: null, - reason: "Branches have incompatible values.", -}; - -describe("exported primitive union return shapes", () => { - const conditionalCases = [ - "missing", - "unsupported()", - '({ key: "value" })', - '["value"]', - '+"invalid"', - ].flatMap((other) => [ - `condition ? (choice ? "one" : "two") : ${other}`, - `condition ? ${other} : (choice ? "one" : "two")`, - ]); - const logicalCases = [ - { operator: "||", known: '(choice ? "" : "one")' }, - { operator: "&&", known: '(choice ? 0 : "one")' }, - { operator: "??", known: '(choice ? null : "one")' }, - ].flatMap(({ operator, known }) => [ - `${known} ${operator} missing`, - `missing ${operator} ${known}`, - ]); - - it.each([...conditionalCases, ...logicalCases])( - "projects an unknown field instead of a definite union for %s", - (expression) => { - const projected = projectReturns(`return ${expression};`); - - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "ambiguous", - fields: [unknownBranchField], - property_coverage: [], - }, - ]); - // Complete return-site collection does not make its values known. - expect(projected.properties.return_shape_coverage).toEqual({ - status: "complete", - retained_return_sites: 1, - omitted_return_sites: 0, - omitted_fields: 0, - omitted_property_coverage: 0, - projection_complete: true, - }); - expect(projected.coverage).toEqual(completeApplicationCoverage()); - expect(projected.limitations).toContain( - "Return shapes are inferred from inert syntax and do not prove runtime behavior.", - ); - }, - ); - - it("keeps an incompatible object field unknown beside a known literal", () => { - const projected = projectReturns(` - return { - kind: "result", - value: condition ? (choice ? "one" : "two") : missing, - }; - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "object", - fields: [ - { path: "/kind", state: "literal", value: "result", reason: null }, - { ...unknownBranchField, path: "/value" }, - ], - property_coverage: [{ path: "", status: "complete", omitted: 0 }], - }, - ]); - }); - - it("retains valid nested unions, duplicates, and null alternatives", () => { - const projected = projectReturns(` - return condition - ? (choice ? "two" : "one") - : (other ? "one" : null); - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "union", - fields: [ - { - path: "", - state: "union", - value: [null, "one", "two"], - reason: null, - }, - ], - property_coverage: [], - }, - ]); - }); - - it("preserves three independent direct returns without merging their values", () => { - const projected = projectReturns(` - if (condition) return choice ? "one" : "two"; - if (other) return missing; - return { kind: "object" }; - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "union", - fields: [ - { path: "", state: "union", value: ["one", "two"], reason: null }, - ], - property_coverage: [], - }, - { - source_range: expect.any(Object), - value_status: "unknown", - fields: [ - { - path: "", - state: "unknown", - value: null, - reason: "Unbound identifier missing.", - }, - ], - property_coverage: [], - }, - { - source_range: expect.any(Object), - value_status: "object", - fields: [ - { path: "/kind", state: "literal", value: "object", reason: null }, - ], - property_coverage: [{ path: "", status: "complete", omitted: 0 }], - }, - ]); - expect(projected.properties.return_shape_coverage).toMatchObject({ - status: "complete", - retained_return_sites: 3, - omitted_return_sites: 0, - projection_complete: true, - }); - }); -}); diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 16569d46f2..024df75824 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -2,156 +2,46 @@ import { describe, expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; -describe.each(["webPreferences", "serviceName"] as const)( - "Electron %s option lookup", - (name) => { - const value = - name === "webPreferences" - ? '{ preload: "./kept.js", sandbox: true }' - : '"kept"'; - const explicit = `${name}: ${value}`; - const method = `${name}() { return ${value}; }`; - const getter = `get ${name}() { return ${value}; }`; - const setter = `set ${name}(value) {}`; +describe("Electron option evidence", () => { + it("keeps the final explicit preload and service name with unrelated keys", () => { + const analysis = analyzeJavaScriptStaticSource(` + new BrowserWindow({ webPreferences: { preload: "./kept.js", sandbox: true }, title: "App" }); + utilityProcess.fork("./worker.js", [], { serviceName: "kept", cwd: "/app" }); + `); - const explicitCases = [ - ["plain key", explicit], - ["quoted key", `"${name}": ${value}`], - ["computed string key", `["${name}"]: ${value}`], - ["duplicate key", `${name}: null, ${explicit}`], - ["computed duplicate key", `${name}: null, ["${name}"]: ${value}`], - ["earlier spread", `...options, ${explicit}`], - ["earlier computed identifier", `[key]: null, ${explicit}`], - ["earlier computed expression", `[getKey()]: null, ${explicit}`], - ["earlier getter", `${getter}, ${explicit}`], - ["earlier method", `${method}, ${explicit}`], - ["unrelated later key", `${explicit}, title: "title"`], - ["unrelated computed literal", `${explicit}, ["title"]: null`], - ["unrelated numeric key", `${explicit}, [1]: null`], - ["unrelated empty key", `${explicit}, [""]: null`], - ["unrelated getter", `${explicit}, get title() { return "title"; }`], - ]; - - it.each(explicitCases)( - "retains the explicit value with %s", - (_, members) => { - const analysis = analyzeOption(name, `{ ${members} }`); - expect(analysis.parse_status).toBe("complete"); - if (name === "webPreferences") { - expect(analysis.electron.browser_windows).toEqual([ - expect.objectContaining({ - options_status: "object-literal", - web_preferences_status: "object-literal", - preload_path: "./kept.js", - preload_resolution_context: "module-specifier", - web_preferences: [ - { name: "preload", value: literal("./kept.js") }, - { name: "sandbox", value: literal(true) }, - ], - }), - ]); - } else { - expect(analysis.electron.utility_processes).toEqual([ - expect.objectContaining({ service_name: "kept" }), - ]); - } - }, - ); - - const dynamicCases = [ - ["same-spelled computed binding", `[${name}]: ${value}`], - ["different computed binding", `[key]: ${value}`], - ["computed expression", `[getKey()]: ${value}`], - ["unevaluated template key", `[\`${name}\`]: ${value}`], - ["spread only", "...options"], - ["later spread", `${explicit}, ...options`], - ["later computed binding", `${explicit}, [key]: null`], - ["later computed expression", `${explicit}, [getKey()]: null`], - ["exact getter", getter], - ["exact setter", setter], - ["exact method", method], - ["computed string method", `["${name}"]() { return ${value}; }`], - ["overriding getter", `${explicit}, ${getter}`], - ["overriding setter", `${explicit}, ${setter}`], - ["overriding method", `${explicit}, ${method}`], - ["dynamic explicit value", `${name}: dynamicValue`], - ["null explicit value", `${name}: null`], - ["duplicate null value", `${explicit}, ${name}: null`], - ]; - - it.each(dynamicCases)("preserves uncertainty for %s", (_, members) => { - expectUnknownOption(name, `{ ${members} }`, "object-literal"); - }); - - it.each(["null", "options"])( - "preserves nonliteral options %s", - (options) => { - expectUnknownOption(name, options, "dynamic"); - }, - ); - - it.each(["", "{}", '{ title: "title", ["other"]: null }'])( - "reports an omitted option in %s without inventing a default", - (options) => { - const analysis = analyzeOption(name, options); - expect(analysis.parse_status).toBe("complete"); - if (name === "webPreferences") - expect(analysis.electron.browser_windows[0]).toMatchObject({ - options_status: options === "" ? "missing" : "object-literal", - web_preferences_status: "missing", - web_preferences: [], - preload_path: null, - preload_resolution_context: null, - }); - else - expect( - analysis.electron.utility_processes[0]?.service_name, - ).toBeNull(); - }, - ); - }, -); - -type OptionName = "webPreferences" | "serviceName"; - -const analyzeOption = (name: OptionName, options: string) => - analyzeJavaScriptStaticSource(` - const webPreferences = "title"; - const serviceName = "env"; - ${ - name === "webPreferences" - ? `new BrowserWindow(${options});` - : `utilityProcess.fork("./worker.js", []${options === "" ? "" : `, ${options}`});` - } - `); - -const literal = (value: string | boolean) => ({ - status: "literal", - value, - expression: null, -}); - -const expectUnknownOption = ( - name: OptionName, - options: string, - optionsStatus: "object-literal" | "dynamic", -): void => { - const analysis = analyzeOption(name, options); - expect(analysis.parse_status).toBe("partial"); - expect(analysis.parse_error_count).toBe(0); - expect(analysis.limitations.join(" ")).toContain("remain unknown"); - if (name === "webPreferences") + expect(analysis.parse_status).toBe("complete"); expect(analysis.electron.browser_windows[0]).toMatchObject({ - options_status: optionsStatus, + preload_path: "./kept.js", + preload_resolution_context: "module-specifier", + web_preferences: [ + { name: "preload", value: { status: "literal", value: "./kept.js" } }, + { name: "sandbox", value: { status: "literal", value: true } }, + ], + }); + expect(analysis.electron.utility_processes[0]).toMatchObject({ + module_path: "./worker.js", + service_name: "kept", + }); + }); + + it("keeps values unknown when a later computed key can override them", () => { + const analysis = analyzeJavaScriptStaticSource(` + new BrowserWindow({ webPreferences: { preload: "./maybe.js" }, [key]: null }); + utilityProcess.fork("./worker.js", [], { serviceName: "maybe", [key]: null }); + `); + + expect(analysis.parse_status).toBe("partial"); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.limitations.join(" ")).toContain("remain unknown"); + expect(analysis.electron.browser_windows[0]).toMatchObject({ + preload_path: null, + preload_resolution_context: null, web_preferences_status: "dynamic", web_preferences: [], - preload_path: null, - preload_resolution_context: null, }); - else expect(analysis.electron.utility_processes[0]).toMatchObject({ module_path: "./worker.js", - module_expression: null, service_name: null, }); -}; + }); +}); diff --git a/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts b/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts index acb1fe1f19..3ab413e858 100644 --- a/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts +++ b/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts @@ -1,9 +1,6 @@ import { expect, it } from "vitest"; -import { - javaScriptExportShapeComparisonResultSchema, - projectedExportReturnShapesSchema, -} from "./javascriptExportShapeComparisonSchemas.js"; +import { projectedExportReturnShapesSchema } from "./javascriptExportShapeComparisonSchemas.js"; const projectionWithField = (field: unknown) => ({ semantic_role: "export-return-shapes", @@ -54,104 +51,3 @@ it("parses projected fields into literal, union, or unknown values", () => { ).success, ).toBe(true); }); - -it("accepts every projected return site and field without presentation caps", () => { - const fields = Array.from({ length: 65 }, (_, index) => ({ - path: `/field-${String(index)}`, - state: "literal", - value: index, - reason: null, - })); - const propertyCoverage = fields.map(({ path }) => ({ - path, - status: "complete", - omitted: 0, - })); - const shapes = Array.from({ length: 33 }, () => ({ - source_range: { - start: { line: 1, column: 0 }, - end: { line: 1, column: 10 }, - }, - value_status: "object", - fields, - property_coverage: propertyCoverage, - })); - - expect( - projectedExportReturnShapesSchema.parse({ - ...projectionWithField(fields[0]), - static_return_shapes: shapes, - return_shape_coverage: { - status: "complete", - retained_return_sites: shapes.length, - omitted_return_sites: 0, - omitted_fields: 0, - omitted_property_coverage: 0, - projection_complete: true, - }, - }).static_return_shapes, - ).toHaveLength(33); -}); - -it("accepts complete candidate and change inventories without page ceilings", () => { - const evidenceId = `ev_${"a".repeat(64)}`; - const graphId = `jag_${"b".repeat(64)}`; - const selector = { - evidence_id: evidenceId, - graph_id: graphId, - requested_module_path: "index.js", - requested_export_name: "render", - candidates: Array.from({ length: 1_001 }, (_, index) => ({ - node_id: `jag_node_${index.toString(16).padStart(64, "0")}`, - module_path: "index.js", - export_name: "render", - matches_requested_module: true, - matches_requested_export: true, - })), - omitted_candidates: 0, - status: "missing", - selected_node_id: null, - }; - const changes = Array.from({ length: 10_001 }, (_, index) => ({ - change_id: `jesc_change_${index.toString(16).padStart(64, "0")}`, - status: "unknown", - path: `/field-${String(index)}`, - discriminant: null, - left: { availability: "absent" }, - right: { availability: "absent" }, - left_source_range: null, - right_source_range: null, - evidence_links: [evidenceId, `ev_${"c".repeat(64)}`], - limitations: [], - })); - - expect( - javaScriptExportShapeComparisonResultSchema.parse({ - comparison_id: `jesc_${"d".repeat(64)}`, - left: selector, - right: { ...selector, graph_id: `jag_${"e".repeat(64)}` }, - summary: { added: 0, removed: 0, changed: 0, unknown: changes.length }, - changes, - coverage: { - status: "complete-within-inputs", - left_graph_status: "complete", - right_graph_status: "complete", - paired_variants: 0, - unpaired_left_variants: 0, - unpaired_right_variants: 0, - omitted_left_variants: 0, - omitted_right_variants: 0, - left_source_omitted_variants: 0, - right_source_omitted_variants: 0, - left_omitted_fields: 0, - right_omitted_fields: 0, - left_omitted_property_coverage: 0, - right_omitted_property_coverage: 0, - omitted_candidates: 0, - omitted_changes: 0, - }, - evidence_links: [evidenceId, `ev_${"c".repeat(64)}`], - limitations: [], - }).changes, - ).toHaveLength(10_001); -}); diff --git a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts index bc3828fb56..15a6f84454 100644 --- a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts +++ b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts @@ -253,56 +253,58 @@ describe("feature seed selection controls", () => { }); describe("public feature trace from contextBridge source members", () => { - it.each(matchingModes)( - "traces $match / $value / case-sensitive $case_sensitive", - (mode) => { - const graph = contextBridgeGraph(); - const before = structuredClone(graph); - const input = { - sourceEvidenceId, - graph, - nativeEvidence: [], - direction: "both" as const, - }; - const generic = traceApplicationFeature({ - ...input, - seed: { kind: "string", ...mode }, - }); - const typed = traceApplicationFeature({ - ...input, - seed: { kind: "api", ...mode }, - }); + it("traces one exact member seed through the public feature workflow", () => { + const mode = { + value: "openProject", + match: "exact" as const, + case_sensitive: true, + }; + const graph = contextBridgeGraph(); + const before = structuredClone(graph); + const input = { + sourceEvidenceId, + graph, + nativeEvidence: [], + direction: "both" as const, + }; + const generic = traceApplicationFeature({ + ...input, + seed: { kind: "string", ...mode }, + }); + const typed = traceApplicationFeature({ + ...input, + seed: { kind: "api", ...mode }, + }); - expect(typed.coverage).toEqual({ - status: "complete-within-source", - source_graph_status: "complete", - total_seed_matches: 1, - }); - expect(typed.seed_matches).toEqual(generic.seed_matches); - expect(typed.seed_matches).toEqual([ - { - node_id: graph.nodes[0]?.node_id, - kind: "context-bridge-api", - basis: "property", - field: "observations[0].properties.members[1]", - }, - ]); - expect(typed.graph?.nodes).toEqual(graph.nodes); - expect(typed.graph?.edges).toEqual([]); - expect(typed.evidence_links).toEqual([sourceEvidenceId]); - expect(typed.summary).toMatchObject({ - matched_seeds: 1, - traced_nodes: 1, - traced_edges: 0, - observed_facts: 1, - inferred_facts: 0, - unknown_facts: 0, - unavailable_facts: 0, - }); - expect(typed.native_handoffs).toEqual([]); - expect(graph).toEqual(before); - }, - ); + expect(typed.coverage).toEqual({ + status: "complete-within-source", + source_graph_status: "complete", + total_seed_matches: 1, + }); + expect(typed.seed_matches).toEqual(generic.seed_matches); + expect(typed.seed_matches).toEqual([ + { + node_id: graph.nodes[0]?.node_id, + kind: "context-bridge-api", + basis: "property", + field: "observations[0].properties.members[1]", + }, + ]); + expect(typed.graph?.nodes).toEqual(graph.nodes); + expect(typed.graph?.edges).toEqual([]); + expect(typed.evidence_links).toEqual([sourceEvidenceId]); + expect(typed.summary).toMatchObject({ + matched_seeds: 1, + traced_nodes: 1, + traced_edges: 0, + observed_facts: 1, + inferred_facts: 0, + unknown_facts: 0, + unavailable_facts: 0, + }); + expect(typed.native_handoffs).toEqual([]); + expect(graph).toEqual(before); + }); it("keeps a missing member explicit without changing source coverage", () => { const graph = contextBridgeGraph(); diff --git a/src/domain/javascript/javascriptRecovery.test.ts b/src/domain/javascript/javascriptRecovery.test.ts index 5f9a2b687b..4a48a44b00 100644 --- a/src/domain/javascript/javascriptRecovery.test.ts +++ b/src/domain/javascript/javascriptRecovery.test.ts @@ -2,19 +2,6 @@ import { describe, expect, it } from "vitest"; import { javascriptRecoveryInputSchema } from "./javascriptRecovery.js"; describe("JavaScript recovery requests", () => { - it("defaults omitted transform metadata without requiring engine confirmations", () => { - expect( - javascriptRecoveryInputSchema.parse({ - path: "/tmp/bundle.js", - output_directory: "/tmp/recovered", - }), - ).toEqual({ - path: "/tmp/bundle.js", - output_directory: "/tmp/recovered", - extraction_mode: "structural", - rewrite_level: "standard", - }); - }); it.each([ { path: "", output_directory: "/tmp/recovered" }, { path: "/tmp/bundle.js" }, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts index 0e4b2c22a5..15a4619c3b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts @@ -1,39 +1,15 @@ import { expect, it } from "vitest"; + import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -it.each(["namespace", "default", "named-default"])( - "retains Node built-in effects from %s imports", - (mode) => { - const imports = ["fs", "child_process", "timers", "http"] - .map((name, index) => { - const declaration = - mode === "namespace" - ? `* as m${String(index)}` - : mode === "default" - ? `m${String(index)}` - : `{default as m${String(index)}}`; - return `import ${declaration} from 'node:${name}';`; - }) - .join("\n"); - const ir = analyzeJavaScriptSemantics(`${imports} - const text = m0.readFileSync('config.json','utf8'); - const stream = m0.createReadStream('config.json'); stream.destroy(); - const child = m1.spawn('/usr/bin/tool', ['--version']); - const timer = m2.setTimeout(work,1000); m2.clearTimeout(timer); - const request = m3.get('http://localhost/'); +it("does not assign built-in APIs to unrelated or shadowed bindings", () => { + const ir = analyzeJavaScriptSemantics(` + import fs from "./fs.js"; + import { constants as constantsFs } from "node:fs"; + fs.createReadStream("config.json"); + constantsFs.createReadStream("config.json"); + function run(fs) { fs.createReadStream("config.json"); } `); - expect(ir.configurationOperations).toHaveLength(1); - expect(ir.resourceOperations).toHaveLength(2); - expect(ir.childProcessSpawns).toHaveLength(1); - expect(ir.timerOperations).toHaveLength(2); - expect(ir.requestOperations).toHaveLength(1); - }, -); -it.each([ - "import fs from './fs.js'; fs.createReadStream('config.json');", - "import {constants as fs} from 'node:fs'; fs.createReadStream('config.json');", - "function run(fs) { fs.createReadStream('config.json'); }", -])("does not invent a built-in namespace for %s", (source) => { - expect(analyzeJavaScriptSemantics(source).resourceOperations).toEqual([]); + expect(ir.resourceOperations).toEqual([]); }); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts index 78fea98456..880aae06ce 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts @@ -155,19 +155,6 @@ describe("JavaScript semantic analysis: calls 2", () => { ]); }); - it("retains every promise operation found in the finite source", () => { - const ir = analyzeJavaScriptSemantics( - ` - Promise.resolve(1); - Promise.resolve(2); - Promise.resolve(3); - `, - ); - - expect(ir.promiseOperations).toHaveLength(3); - expect(ir.coverage.status).toBe("complete"); - }); - it("fingerprints formatting and local-name changes identically", () => { const left = analyzeJavaScriptSemantics(` function calculate(value) { diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts index 9d27e0ed5d..e9f8d8159b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts @@ -1,67 +1,43 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { + analyzeJavaScriptSemantics, + analyzeParsedJavaScriptSemantics, +} from "./javascriptSemanticAnalysis.js"; import { onlyBinding, origin } from "./javascriptSemanticAnalysis.fixture.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { calleeName } from "./javascriptStaticAnalysisHelpers.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; const memberChain = ".next".repeat(12_000); +const ordinaryMemberChain = ".next".repeat(64); -it("retains native addon observations through a deep member initializer", () => { - const analysis = analyzeJavaScriptStaticSource( - `const value = require("./addon.node")${memberChain}.last[""];`, +it("preserves a 12,000-member native import through both analyzers", () => { + const parsed = parseJavaScriptSource( + `const value = require("./addon.node")${memberChain}.last; const ordinary = root.next.last;`, ); - expect(analysis.electron.native_addon_bindings).toEqual([ + if (parsed === null) throw new Error("Expected valid JavaScript"); + + const staticAnalysis = analyzeParsedJavaScriptStaticSource("", parsed); + const ir = analyzeParsedJavaScriptSemantics(parsed); + expect(staticAnalysis.parse_status).toBe("complete"); + expect(staticAnalysis.electron.native_addon_bindings).toEqual([ expect.objectContaining({ specifier: "./addon.node", members: ["last"] }), ]); - expect(analysis.parse_error_count).toBe(0); -}); - -it("keeps a deep non-native initializer outside native addon observations", () => { - const analysis = analyzeJavaScriptStaticSource( - `const value = root${memberChain}.last;`, - ); - expect(analysis.electron.native_addon_bindings).toEqual([]); - expect(analysis.parse_status).toBe("complete"); -}); - -it("retains unknown value diagnostics and references after a deep member initializer", () => { - const ir = analyzeJavaScriptSemantics( - `const value = root${memberChain}.last;`, - ); expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 }); - expect(onlyBinding(ir, "value")).toMatchObject({ - value: { status: "unknown", reason: "Cannot project last from unknown." }, - provenance: { status: "unknown", reason: "Unbound identifier root." }, - }); - expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([ - { name: "root", role: "read" }, - ]); -}); - -it.each([ - `const value = require("./dependency.js")${memberChain}[""].last;`, - `const dependency = require("./dependency.js"); const value = dependency${memberChain}[""].last;`, - `const value = require("./dependency.js")?.next${memberChain}[""].last;`, -])("preserves the full literal module path (case %#)", (source) => { - const ir = analyzeJavaScriptSemantics(source); const path = origin(onlyBinding(ir, "value")); - expect(path.specifier).toBe("./dependency.js"); + expect(path.specifier).toBe("./addon.node"); expect(path.importedPath).toEqual([ - ...(source.includes("?.next") ? ["next"] : []), ...Array.from({ length: 12_000 }, () => "next"), - "", "last", ]); - expect(ir.coverage.status).toBe("complete"); }); it("retains a deep CommonJS re-export path without inventing dynamic module links", () => { const ir = analyzeJavaScriptSemantics(` - module.exports.result = require("./dependency.js")${memberChain}.last; - const dynamic = require("./other.js")${memberChain}[key].last; + module.exports.result = require("./dependency.js")${ordinaryMemberChain}.last; + const dynamic = require("./other.js")${ordinaryMemberChain}[key].last; `); expect(ir.moduleLinks).toEqual([ expect.objectContaining({ @@ -80,7 +56,7 @@ it("retains a deep CommonJS re-export path without inventing dynamic module link it("invalidates a deep mutation while retaining unrelated literal properties", () => { const ir = analyzeJavaScriptSemantics(` const root = { untouched: "retained" }; - root${memberChain}.last = 1; + root${ordinaryMemberChain}.last = 1; const value = root.untouched; `); expect(onlyBinding(ir, "root").value).toEqual({ @@ -98,7 +74,7 @@ it("invalidates a deep mutation while retaining unrelated literal properties", ( }); it("retains the complete location and unresolved target of a deep call", () => { - const callee = `root${memberChain}.last`; + const callee = `root${ordinaryMemberChain}.last`; const ir = analyzeJavaScriptSemantics(`${callee}();`); expect(ir.callSites).toEqual([ expect.objectContaining({ @@ -115,7 +91,7 @@ it("retains the complete location and unresolved target of a deep call", () => { it("retains deep default values without confusing their reads with parameter bindings", () => { const ir = analyzeJavaScriptSemantics( - `export function inspect(value = root${memberChain}.last) { return value; }`, + `export function inspect(value = root${ordinaryMemberChain}.last) { return value; }`, ); expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([ { name: "root", role: "read" }, @@ -126,7 +102,7 @@ it("retains deep default values without confusing their reads with parameter bin it("keeps a shadowed require local through a deep member initializer", () => { const ir = analyzeJavaScriptSemantics( - `function inspect(require) { const value = require("./dependency.js")${memberChain}.last; return value; }`, + `function inspect(require) { const value = require("./dependency.js")${ordinaryMemberChain}.last; return value; }`, ); expect(ir.moduleLinks).toEqual([]); expect(onlyBinding(ir, "value").provenance.origins).toEqual([]); @@ -140,7 +116,7 @@ it.each([ ["root[0].last()", "root.0.last"], ["root[key].last()", "root.[computed@5].last"], ["root?.first.last()", "root.first.last"], - [`root${memberChain}.last()`, `root${memberChain}.last`], + [`root${ordinaryMemberChain}.last()`, `root${ordinaryMemberChain}.last`], ])("preserves exact callee syntax (case %#)", (source, expected) => { const file = parseJavaScriptSource(source); const statement = file?.program.body[0]; diff --git a/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts index 25e02aefad..e6a06e7284 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts @@ -18,6 +18,19 @@ const valueOf = (expression: string) => analyzeJavaScriptSemantics(`const answer = ${expression};`), "answer", ).value; +const valuesOf = (expressions: readonly string[]) => { + const ir = analyzeJavaScriptSemantics( + expressions + .map( + (expression, index) => + `const candidate${String(index)} = ${expression};`, + ) + .join("\n"), + ); + return expressions.map( + (_, index) => topLevelBinding(ir, `candidate${String(index)}`).value, + ); +}; describe("conditional primitive unions", () => { const cases = conditionalCandidates.flatMap((known) => @@ -35,11 +48,11 @@ describe("conditional primitive unions", () => { ]), ); - it.each(cases)("preserves incompatible alternatives in %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + it("preserves incompatible alternatives across conditional expressions", () => { + expect(valuesOf(cases)).toEqual(cases.map(() => incompatibleValue)); }); - it.each([ + const exactCases = [ [ 'condition ? (choice ? "two" : "one") : "three"', { status: "union", values: ["one", "three", "two"] }, @@ -69,8 +82,12 @@ describe("conditional primitive unions", () => { { status: "union", values: [null, "one", "two"] }, ], ["condition ? null : null", { status: "literal", value: null }], - ])("retains exact primitive candidates in %s", (expression, expected) => { - expect(valueOf(expression)).toEqual(expected); + ] as const; + + it("retains exact primitive candidates across conditional expressions", () => { + expect(valuesOf(exactCases.map(([expression]) => expression))).toEqual( + exactCases.map(([, expected]) => expected), + ); }); it("propagates an incompatible nested branch through an outer union", () => { @@ -119,11 +136,11 @@ describe("logical primitive unions", () => { ), ); - it.each(cases)("preserves unresolved alternatives in %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + it("preserves unresolved alternatives across logical expressions", () => { + expect(valuesOf(cases)).toEqual(cases.map(() => incompatibleValue)); }); - it.each([ + const exactCases = [ [ '(choice ? "" : "one") || "two"', { status: "union", values: ["", "one", "two"] }, @@ -138,19 +155,24 @@ describe("logical primitive unions", () => { ], ['"one" || "one"', { status: "literal", value: "one" }], ["null ?? null", { status: "literal", value: null }], - ])( - "retains the conservative primitive union in %s", - (expression, expected) => { - expect(valueOf(expression)).toEqual(expected); - }, - ); + ] as const; - it.each([ + it("retains conservative primitive unions across logical expressions", () => { + expect(valuesOf(exactCases.map(([expression]) => expression))).toEqual( + exactCases.map(([, expected]) => expected), + ); + }); + + const unevaluatedCases = [ 'true ? "one" : missing', '"one" || missing', "false && missing", '"one" ?? missing', - ])("does not introduce short-circuit evaluation for %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + ]; + + it("does not introduce short-circuit evaluation", () => { + expect(valuesOf(unevaluatedCases)).toEqual( + unevaluatedCases.map(() => incompatibleValue), + ); }); }); diff --git a/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts b/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts index a4f3341633..ab64cfe007 100644 --- a/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts +++ b/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts @@ -1,28 +1,8 @@ import { expect, it } from "vitest"; -import { - javaScriptSemanticQueryInputSchema, - javaScriptSemanticQueryResultSchema, -} from "./javascriptSemanticQuerySchemas.js"; +import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js"; -it("accepts long selectors and complete relation and class selections", () => { - const longText = "x".repeat(16_385); - const result = javaScriptSemanticQueryInputSchema.parse({ - seed: { kind: "endpoint", value: longText }, - direction: "forward-influence", - allowed_relations: Array(2).fill("calls"), - expected: { - role: "sink", - classes: Array(25).fill("module"), - }, - }); - - expect(result.seed).toEqual({ kind: "endpoint", value: longText }); - expect(result.allowed_relations).toHaveLength(2); - expect(result.expected?.classes).toHaveLength(25); -}); - -it("rejects the inert source-map authority setting", () => { +it("rejects a source-map authority setting that the query does not consume", () => { expect( javaScriptSemanticQueryInputSchema.safeParse({ seed: { kind: "endpoint", value: "/api" }, @@ -31,36 +11,3 @@ it("rejects the inert source-map authority setting", () => { }).success, ).toBe(false); }); - -it("accepts every reported limitation without a count ceiling", () => { - const limitations = Array.from( - { length: 1_001 }, - (_, index) => `gap-${index}`, - ); - const id = "a".repeat(64); - - const result = javaScriptSemanticQueryResultSchema.parse({ - query_id: `jsrq_${id}`, - source_graph_id: `jsrg_${id}`, - seed: { kind: "literal", value: null }, - direction: "forward-influence", - status: "no-match", - seed_node_ids: [], - nodes: [], - relations: [], - unknowns: [], - expected_match_node_ids: [], - summary: { - total_seed_matches: 0, - traversed_nodes: 0, - traversed_relations: 0, - traversed_functions: 0, - traversed_modules: 0, - relevant_unknowns: 0, - }, - coverage: { status: "complete" }, - limitations, - }); - - expect(result.limitations).toHaveLength(1_001); -}); diff --git a/src/domain/javascript/javascriptSemanticTraversal.test.ts b/src/domain/javascript/javascriptSemanticTraversal.test.ts deleted file mode 100644 index 482afe2b94..0000000000 --- a/src/domain/javascript/javascriptSemanticTraversal.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as t from "@babel/types"; -import { expect, it } from "vitest"; - -import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; - -it("preserves source-tree enter/exit order and detached ancestor snapshots", () => { - const parsed = parseJavaScriptSource("const answer = first + second;"); - if (parsed === null) - throw new TypeError("Expected a parsed JavaScript fixture"); - const visits: string[] = []; - const snapshots: (readonly t.Node[])[] = []; - traverseJavaScriptAst(parsed.program, { - enter: (node, parent, readAncestors) => { - visits.push(`enter:${node.type}`); - const ancestors = readAncestors(); - expect(ancestors.at(-1) ?? null).toBe(parent); - if (t.isIdentifier(node)) snapshots.push(ancestors); - }, - exit: (node) => visits.push(`exit:${node.type}`), - }); - expect(visits).toEqual([ - "enter:Program", - "enter:VariableDeclaration", - "enter:VariableDeclarator", - "enter:Identifier", - "exit:Identifier", - "enter:BinaryExpression", - "enter:Identifier", - "exit:Identifier", - "enter:Identifier", - "exit:Identifier", - "exit:BinaryExpression", - "exit:VariableDeclarator", - "exit:VariableDeclaration", - "exit:Program", - ]); - expect(snapshots.map((nodes) => nodes.map(({ type }) => type))).toEqual([ - ["Program", "VariableDeclaration", "VariableDeclarator"], - [ - "Program", - "VariableDeclaration", - "VariableDeclarator", - "BinaryExpression", - ], - [ - "Program", - "VariableDeclaration", - "VariableDeclarator", - "BinaryExpression", - ], - ]); -}); - -it("skips sparse child slots without disturbing sibling visit order", () => { - const parsed = parseJavaScriptSource("const values = [first, , second];"); - if (parsed === null) - throw new TypeError("Expected a parsed JavaScript fixture"); - const identifiers: string[] = []; - traverseJavaScriptAst(parsed.program, { - enter: (node) => { - if (t.isIdentifier(node)) identifiers.push(node.name); - }, - }); - expect(identifiers).toEqual(["values", "first", "second"]); -}); diff --git a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts index 6e426c7c68..3d03133013 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts @@ -5,6 +5,7 @@ import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; const memberChain = ".next".repeat(12_000); +const ordinaryMemberChain = ".next".repeat(64); it("collects a 12,000-member CommonJS export path without recursion", () => { const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`); @@ -48,8 +49,8 @@ it("keeps left-first and outer-member-first bundler runtime matches", () => { ).toBe("webpackChunkInner"); }); -it("analyzes a 12,000-member bundler runtime chain without recursion", () => { - const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`; +it("recognizes a bundler runtime through nested member names", () => { + const source = `globalThis.webpackChunkApp${ordinaryMemberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`; const analysis = analyzeJavaScriptStaticSource(source); expect(analysis.parse_status).toBe("complete"); expect(analysis.parse_error_count).toBe(0); diff --git a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts index d9ae953805..93a9ddec90 100644 --- a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts @@ -22,12 +22,6 @@ describe("source map directives", () => { expect(sourceMapUrls(source)).toEqual([expected]); }); - it("keeps the comment terminator out of the declared url", () => { - expect(sourceMapUrls("/*# sourceMappingURL=app.js.map */")).toEqual([ - "app.js.map", - ]); - }); - it("retains every directive in a source", () => { expect( sourceMapUrls( @@ -84,30 +78,6 @@ describe("source map comment boundaries", () => { }); describe("source map comment locations and recovery", () => { - it("keeps exact locations and the first occurrence of duplicate URLs", () => { - const line = "//# sourceMappingURL=first.map"; - const block = "/*# sourceMappingURL=second.map"; - const result = analyzeJavaScriptStaticSource( - ['const marker = "😀";', line, `${block} */`, line].join("\r\n"), - ); - expect(result.source_map_urls).toEqual([ - { - declared_url: "first.map", - location: { - start: { line: 2, column: 0 }, - end: { line: 2, column: line.length }, - }, - }, - { - declared_url: "second.map", - location: { - start: { line: 3, column: 0 }, - end: { line: 3, column: block.length }, - }, - }, - ]); - }); - it("keeps real comments when the parser recovers and reports partial syntax", () => { const result = analyzeJavaScriptStaticSource( "let value; let value;\n//# sourceMappingURL=recovered.map", diff --git a/tests/acceptance/applications/applicationEvidenceMcp.test.ts b/tests/acceptance/applications/applicationEvidenceMcp.test.ts index 3fdba32f4d..6b21ac22c9 100644 --- a/tests/acceptance/applications/applicationEvidenceMcp.test.ts +++ b/tests/acceptance/applications/applicationEvidenceMcp.test.ts @@ -1,9 +1,7 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import type { Client } from "@modelcontextprotocol/client"; import { describe, expect, it } from "vitest"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; -import { createServer } from "../../../src/server/createServer.js"; import { JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE, JAVASCRIPT_FEATURE_TRACE_EXAMPLE, @@ -11,39 +9,10 @@ import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, } from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; import { createEvidence } from "../../../src/domain/evidence.js"; -import { observed } from "../../fixtures/analysisExecution.js"; - -interface TestHarness { - readonly client: Client; - readonly session: BinarySession; - readonly close: () => Promise; -} - -async function createTestHarness(): Promise { - const session = createTestBinarySession(() => ({ - execute: () => Promise.resolve(observed(null)), - close: () => Promise.resolve(), - })); - const server = createServer(session, session); - const client = new Client({ - name: "application-workflow-test", - version: "1", - }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await server.connect(serverTransport); - await client.connect(clientTransport); - - return { - client, - session, - close: async () => { - await client.close(); - await server.close(); - await session.close(); - }, - }; -} +import { + createApplicationMcpHarness, + type ApplicationMcpHarness, +} from "./mcpHarness.js"; async function runInlineEvidenceScenarios( client: Client, @@ -103,7 +72,9 @@ async function runInlineEvidenceScenarios( expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2); } -async function runInlineWorkflowScenarios(harness: TestHarness): Promise { +async function runInlineWorkflowScenarios( + harness: ApplicationMcpHarness, +): Promise { const tracedInline = await harness.client.callTool({ name: "trace_application_feature", arguments: { @@ -250,7 +221,7 @@ async function assertRejectedInlineEvidence(client: Client): Promise { describe("application workflow MCP parity", () => { it("traces and compares authenticated graph Evidence in the session", async () => { - const harness = await createTestHarness(); + const harness = await createApplicationMcpHarness(); try { await runInlineEvidenceScenarios(harness.client, harness.session); await runInlineWorkflowScenarios(harness); diff --git a/tests/acceptance/applications/applicationExportShapeMcp.test.ts b/tests/acceptance/applications/applicationExportShapeMcp.test.ts index c104c8012d..5258ef2c4f 100644 --- a/tests/acceptance/applications/applicationExportShapeMcp.test.ts +++ b/tests/acceptance/applications/applicationExportShapeMcp.test.ts @@ -1,15 +1,14 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { describe, expect, it } from "vitest"; -import { copyFile, mkdir, rm } from "node:fs/promises"; +import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createServer } from "../../../src/server/createServer.js"; -import { observed } from "../../fixtures/analysisExecution.js"; import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js"; +import { compareJavaScriptExportShapesEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; +import { createApplicationMcpHarness } from "./mcpHarness.js"; describe("application workflow MCP parity", () => { it("compares exact parser export shapes with inline Evidence", async () => { @@ -40,14 +39,8 @@ describe("application workflow MCP parity", () => { const leftAnalysis = javascriptApplicationAnalysisResultSchema.parse( left.value.normalized_result, ); - const session = createTestBinarySession(() => ({ - execute: () => Promise.resolve(observed(null)), - close: () => Promise.resolve(), - })); - const server = createServer(session, session); - const client = new Client({ name: "export-shape-test", version: "1" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const harness = await createApplicationMcpHarness(); + const { client, session } = harness; const selectors = { left_module_path: "parser.mjs", left_export_name: "default", @@ -55,8 +48,6 @@ describe("application workflow MCP parity", () => { right_export_name: "default", }; try { - await server.connect(serverTransport); - await client.connect(clientTransport); const relativeApplication = await client.callTool({ name: "analyze_javascript_application", arguments: { input_path: relative(process.cwd(), leftRoot) }, @@ -121,10 +112,110 @@ describe("application workflow MCP parity", () => { }, }); } finally { - await client.close(); - await server.close(); - await session.close(); + await harness.close(); await rm(root, { recursive: true, force: true }); } }); }); + +describe("source-produced export comparison inventories", () => { + it("retains large source-produced export inventories through comparison schemas", async () => { + const fields = Array.from( + { length: 64 }, + (_, index) => `field${String(index)}: ${String(index)}`, + ).join(", "); + const returnSites = Array.from( + { length: 33 }, + (_, index) => + `if (value === ${String(index)}) return { type: "variant-${String(index)}", ${fields} };`, + ).join("\n"); + const extraExports = Array.from( + { length: 1_000 }, + (_, index) => + `export const candidate${String(index)} = ${String(index)};`, + ).join("\n"); + const inventorySource = `export default function parse(value) {\n${returnSites}\n throw new Error("no match");\n}\n${extraExports}`; + const [inventoryLeft, inventoryRight] = await Promise.all([ + analyzeSourceEvidence(inventorySource), + analyzeSourceEvidence(inventorySource), + ]); + const variants = compareEvidence(inventoryLeft, inventoryRight); + expect(variants.left).toMatchObject({ status: "selected" }); + expect(variants.coverage).toMatchObject({ + paired_variants: 33, + unpaired_left_variants: 0, + unpaired_right_variants: 0, + left_source_omitted_variants: 0, + right_source_omitted_variants: 0, + left_omitted_fields: 0, + right_omitted_fields: 0, + }); + + const missing = compareEvidence(inventoryLeft, inventoryRight, { + leftExportName: "missing", + }); + expect(missing.left).toMatchObject({ + status: "missing", + omitted_candidates: 0, + }); + expect(missing.left.candidates).toHaveLength(1_001); + + const changedFieldsLeft = Array.from( + { length: 10_001 }, + (_, index) => `field${String(index)}: ${String(index)}`, + ).join(", "); + const changedFieldsRight = Array.from( + { length: 10_001 }, + (_, index) => `field${String(index)}: ${String(index + 1)}`, + ).join(", "); + const [changeLeft, changeRight] = await Promise.all([ + analyzeSourceEvidence( + `export default () => ({ type: "record", ${changedFieldsLeft} });`, + ), + analyzeSourceEvidence( + `export default () => ({ type: "record", ${changedFieldsRight} });`, + ), + ]); + const changes = compareEvidence(changeLeft, changeRight); + expect(changes.changes).toHaveLength(10_001); + expect(changes.summary).toEqual({ + added: 0, + removed: 0, + changed: 10_001, + unknown: 0, + }); + expect(changes.coverage).toMatchObject({ + status: "complete-within-inputs", + omitted_changes: 0, + left_omitted_fields: 0, + right_omitted_fields: 0, + }); + }, 30_000); +}); + +const analyzeSourceEvidence = async (source: string) => { + const root = await createTestTempDirectory("rea-export-shape-inventory-"); + await writeFile(join(root, "parser.mjs"), source); + const analyzed = await analyzeJavaScriptApplication({ input_path: root }); + if (!analyzed.ok) throw analyzed.error; + return analyzed.value; +}; + +const compareEvidence = ( + left: Awaited>, + right: Awaited>, + selectors: { readonly leftExportName?: string } = {}, +) => { + const compared = compareJavaScriptExportShapesEvidence({ + left, + right, + left_module_path: "parser.mjs", + left_export_name: selectors.leftExportName ?? "default", + right_module_path: "parser.mjs", + right_export_name: "default", + }); + if (!compared.ok) throw compared.error; + return javaScriptExportShapeComparisonResultSchema.parse( + compared.value.normalized_result, + ); +}; diff --git a/tests/acceptance/applications/mcpHarness.ts b/tests/acceptance/applications/mcpHarness.ts new file mode 100644 index 0000000000..5f971e4774 --- /dev/null +++ b/tests/acceptance/applications/mcpHarness.ts @@ -0,0 +1,39 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; + +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; +import { observed } from "../../fixtures/analysisExecution.js"; +import { createServer } from "../../../src/server/createServer.js"; + +/** Owned MCP SDK session shared by application workflow scenarios. */ +export interface ApplicationMcpHarness { + readonly client: Client; + readonly session: BinarySession; + readonly close: () => Promise; +} + +/** Connect application workflows through the production MCP registration. */ +export async function createApplicationMcpHarness(): Promise { + const session = createTestBinarySession(() => ({ + execute: () => Promise.resolve(observed(null)), + close: () => Promise.resolve(), + })); + const server = createServer(session, session); + const client = new Client({ + name: "application-workflow-test", + version: "1", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + return { + client, + session, + close: async () => { + await Promise.all([client.close(), server.close(), session.close()]); + }, + }; +}