mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
test(javascript): consolidate source analysis and export regressions
This commit is contained in:
1 parent
c93422a898
commit
f517a5a1d7
18 files changed
+352
-851
No files matched your search
@@ -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: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
async function createTestHarness(): Promise<TestHarness> {
|
||||
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<void> {
|
||||
async function runInlineWorkflowScenarios(
|
||||
harness: ApplicationMcpHarness,
|
||||
): Promise<void> {
|
||||
const tracedInline = await harness.client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: {
|
||||
@@ -250,7 +221,7 @@ async function assertRejectedInlineEvidence(client: Client): Promise<void> {
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<ReturnType<typeof analyzeSourceEvidence>>,
|
||||
right: Awaited<ReturnType<typeof analyzeSourceEvidence>>,
|
||||
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,
|
||||
);
|
||||
};
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
/** Connect application workflows through the production MCP registration. */
|
||||
export async function createApplicationMcpHarness(): Promise<ApplicationMcpHarness> {
|
||||
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()]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user