Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticChildProcesses.test.ts
T
morlutoandCursor Agent a896e35c47 fix(javascript): preserve semantic effects and graph evidence (#9900)
* fix(javascript): preserve semantic facts across writes and implicit effects

* fix(javascript): preserve bundler identities and helper bindings

* fix(javascript): retain semantic graph evidence and coverage

* test(javascript): include the request link in endpoint traces

Response consumers now also link the originating request to the response, so an endpoint trace reports both that association and the response's json consumer.

* fix(javascript): keep source accessors known after a copied value escapes

An unknown computed getter makes accessor lookup consider every property.
A spread copy already records a value escape, and that escape was treated
as an unknown getter on the source, so the next read escaped the whole
object. Accessor lookup still fails closed for unknown writes and calls.

* fix(javascript): keep Object.defineProperty identified at its own call

The call that escapes Object was treated as a prior replacement of
defineProperty, so a descriptor value never replaced the exported sender
and bundled IPC paths were dropped.

* fix(javascript): keep Vite preload calls that precede reassignment

Helper identity used every write to the binding, so a later assignment
erased a call that had already read the canonical helper. Use the binding
value at the call when that order is known.

* fix(javascript): apply Vite cache mutations before a later reassignment

A later helper write made the whole-binding initializer check ignore an earlier cache mutation. Identity at that mutation uses the same ordered read as the call.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-11 17:33:47 +08:00

283 lines
8.9 KiB
TypeScript

import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
const analyze = (source: string) =>
analyzeJavaScriptSemantics(
`import { spawn } from 'node:child_process';\n${source}`,
);
it.each(
["on", "once", "addListener"].flatMap((method) =>
["", ", 1", ", null", ", {}", ", void 0"].map((listener) => ({
method,
listener,
})),
),
)(
"does not invent a child $method registration with rejected listener $listener",
({ method, listener }) => {
const ir = analyze(
`const child = spawn('worker'); child.${method}('exit'${listener});`,
);
expect(ir.childProcessSpawns).toHaveLength(1);
expect(ir.childProcessInteractions).toEqual([]);
expect(ir.eventOperations).toEqual([]);
},
);
it("keeps interactions before replacement resolved and later ownership uncertain", () => {
const ir = analyze(`
let child = spawn('old');
child.kill();
child = { kill() {}, on() {} };
child.kill();
child.on('exit', done);
const stable = spawn('stable'); stable.kill();
`);
const [old, stable] = ir.childProcessSpawns;
expect(ir.childProcessInteractions).toMatchObject([
{ linkedProcessIds: [old!.processId], resolution: "complete" },
{ linkedProcessIds: [old!.processId], resolution: "partial" },
{ linkedProcessIds: [old!.processId], resolution: "partial" },
{ linkedProcessIds: [stable!.processId], resolution: "complete" },
]);
});
it("selects a new spawned child at each straight-line receiver read", () => {
const ir = analyze(`
let child = spawn('old'); child.kill();
child = spawn('new'); child.once('exit', done); child.kill();
`);
const [old, replacement] = ir.childProcessSpawns;
expect(ir.childProcessInteractions).toMatchObject([
{ linkedProcessIds: [old!.processId], resolution: "complete" },
{ linkedProcessIds: [replacement!.processId], resolution: "complete" },
{ linkedProcessIds: [replacement!.processId], resolution: "complete" },
]);
});
it("selects child ownership before and after a replacement in one expression", () => {
const ir = analyze(`
let child = spawn('old');
(child.kill(), child = spawn('new'));
child.kill();
`);
expect(ir.childProcessInteractions).toMatchObject(
ir.childProcessSpawns.map(({ processId }) => ({
linkedProcessIds: [processId],
resolution: "complete",
})),
);
});
it.each([
"class Stop { field = child.kill(); }",
"class Stop { #field = child.kill(); }",
"class Stop { ['field'] = child.kill(); }",
"const Stop = class { field = (0, child.kill()); };",
"class Stop { field = class { [child.kill()] = 0; }; }",
"class Stop { stop() { child.kill(); } }",
"class Stop { field = () => child.kill(); }",
])(
"retains both child candidates across a deferred read: %s",
(declaration) => {
const ir = analyze(`
let child = spawn('old');
${declaration}
child = spawn('new');
new Stop();
`);
expect(ir.childProcessSpawns).toHaveLength(2);
expect(ir.childProcessInteractions[0]?.linkedProcessIds).toHaveLength(2);
expect(ir.childProcessInteractions).toEqual([
expect.objectContaining({
linkedProcessIds: expect.arrayContaining(
ir.childProcessSpawns.map(({ processId }) => processId),
),
resolution: "partial",
}),
]);
},
);
it("does not prove an assignment-only child precedes an instance field read", () => {
const ir = analyze(`
let child;
child = spawn('assigned');
class Stop { field = child.kill(); }
new Stop();
`);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
},
]);
});
it.each([
"class Stop { static field = child.kill(); }",
"class Stop { static #field = child.kill(); }",
"class Stop { [child.kill()] = 0; }",
])("resolves reads evaluated at class definition: %s", (declaration) => {
const ir = analyze(`
let child = spawn('old');
${declaration}
child = spawn('new');
new Stop();
`);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "complete",
},
]);
});
it.each(["const", "let"])(
"preserves a stable %s child across an instance field read",
(kind) => {
const ir = analyze(`
${kind} child = spawn('stable');
class Stop { field = child.kill(); }
new Stop();
`);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "complete",
},
]);
},
);
it("retains a saved alias at its capture and marks a late alias uncertain", () => {
const ir = analyze(`
let child = spawn('old');
const saved = child; const savedAgain = saved;
child = { kill() {}, on() {} };
const late = child;
savedAgain.kill(); saved.on('exit', done); late.kill();
`);
const child = ir.childProcessSpawns[0]!;
expect(ir.childProcessInteractions).toMatchObject([
{ linkedProcessIds: [child.processId], resolution: "complete" },
{ linkedProcessIds: [child.processId], resolution: "complete" },
{ linkedProcessIds: [child.processId], resolution: "partial" },
]);
});
it.each([
"if (flag) child = replacement; child.kill();",
"function replace() { child = replacement; } replace(); child.kill();",
"function use() { child.kill(); } child = replacement; use();",
"child &&= replacement; child.kill();",
"child += replacement; child.kill();",
"child++; child.kill();",
"({ child } = replacement); child.kill();",
"for (child of replacements) {} child.kill();",
"while (flag) { child.kill(); child = replacement; }",
"(child = replacement, child.kill());",
])("keeps uncertain child replacement explicit: %s", (body) => {
const ir = analyze(`let child = spawn('old'); ${body}`);
expect(ir.childProcessInteractions).toHaveLength(1);
expect(ir.childProcessInteractions[0]).toMatchObject({
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
});
});
it("does not resolve an interaction before its only spawn assignment", () => {
const ir = analyze(`let child; child.kill(); child = spawn('later');`);
expect(ir.childProcessInteractions).toMatchObject([
{ resolution: "partial" },
]);
});
it("keeps conditional spawn assignment and alias cycles uncertain", () => {
const ir = analyze(`
let child;
if (flag) child = spawn('possible');
let alias = child; child = alias;
alias.kill();
`);
expect(ir.childProcessInteractions).toHaveLength(1);
expect(ir.childProcessInteractions[0]).toMatchObject({
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
});
});
it("preserves direct and stable captured children without admitting unrelated receivers", () => {
const ir = analyze(`
spawn('direct').kill();
const child = spawn('captured'); function stop() { child.kill(); }
{ const child = { kill() {} }; child.kill(); }
`);
expect(ir.childProcessInteractions).toHaveLength(2);
expect(
ir.childProcessInteractions.every(
({ resolution }) => resolution === "complete",
),
).toBe(true);
});
it("keeps a default parameter child uncertain when a caller can supply another handle", () => {
const ir = analyze(`
function stop(child = spawn('fallback')) {
child.kill(); child.on('exit', done);
}
stop({ kill() {}, on() {} });
`);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
},
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
},
]);
});
it("preserves the admitted scalar identity of an awaited spawn result", () => {
const ir = analyze(`
const child = await spawn('stable'); child.kill();
`);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "complete",
},
]);
});
it.each([
"let child; function launch() { child = spawn('later'); } const alias = child; alias.kill();",
"let child; { const alias = child; alias.kill(); } child = spawn('later');",
"{ const alias = child; alias.kill(); } var child = spawn('later');",
])("keeps initialization across different bodies uncertain: %s", (source) => {
const ir = analyze(source);
expect(ir.childProcessInteractions).toMatchObject([
{
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
resolution: "partial",
},
]);
});
it("resolves an unconditional assignment before the read in the same function body", () => {
const ir = analyze(`function stop() {
let child; child = spawn('first'); child.kill();
child = spawn('second'); child.kill();
}`);
expect(ir.childProcessInteractions).toMatchObject(
ir.childProcessSpawns.map(({ processId }) => ({
linkedProcessIds: [processId],
resolution: "complete",
})),
);
});