Files
rea/tests/acceptance/applications/javascriptChildHandleOwnership.test.ts
T
星闌andCodex 6d2eeeaed0 fix(javascript): resolve child ownership at receiver reads (#3819)
* fix(javascript): resolve child ownership at receiver reads

* fix(javascript): preserve deferred field child candidates

---------

Co-authored-by: Codex <codex@openai.com>
2026-10-17 00:22:26 +08:00

144 lines
4.5 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
const analyzeThroughCliAndMcp = async (source: string) => {
const root = await createTestTempDirectory("rea-child-ownership-");
const app = join(root, "app");
const home = join(root, "home");
await mkdir(app);
await mkdir(home);
await writeFile(join(app, "main.mjs"), source);
const { stdout } = await promisify(execFile)(
process.execPath,
[
"scripts/rea.mjs",
"analyze-javascript-application",
app,
"--format",
"json",
],
{
cwd: process.cwd(),
env: { ...process.env, HOME: home },
maxBuffer: 16 * 1024 * 1024,
},
);
const evidence = parseEvidence(JSON.parse(stdout));
const { client, call } = await connectLocalToolsMcp({ HOME: home });
const response = await call("analyze_javascript_application", {
input_path: app,
format: "auto",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
await client.ping();
return evidence;
};
it("preserves child ownership uncertainty after replacement through CLI and MCP", async () => {
const evidence = await analyzeThroughCliAndMcp(
`import { spawn } from 'node:child_process';
let child = spawn('program');
child = { kill() {}, on() {} };
child.kill();
child.on('exit', () => {});
const stable = spawn('program');
stable.kill();
`,
);
const { semantic_graph: graph } =
javascriptApplicationAnalysisResultSchema.parse(evidence.normalized_result);
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const ownership = graph.relations.filter(({ relation }) =>
["forwards-signal", "listens-exit"].includes(relation),
);
expect(
ownership.map((relation) => ({
relation: relation.relation,
resolution: relation.resolution,
sourceLine: nodes.get(relation.source_node_id)?.identity.source_range
?.start.line,
targetLine: nodes.get(relation.target_node_id)?.identity.source_range
?.start.line,
})),
).toEqual(
expect.arrayContaining([
{
relation: "forwards-signal",
resolution: "candidate",
sourceLine: 4,
targetLine: 2,
},
{
relation: "listens-exit",
resolution: "candidate",
sourceLine: 2,
targetLine: 5,
},
{
relation: "forwards-signal",
resolution: "resolved",
sourceLine: 7,
targetLine: 6,
},
]),
);
expect(ownership).toHaveLength(3);
expect(
graph.unknowns.filter(
({ family, reason }) =>
family === "child-process" && reason === "ambiguous-target",
),
).toHaveLength(2);
}, 60000);
it("keeps deferred instance field child ownership uncertain through CLI and MCP", async () => {
const evidence = await analyzeThroughCliAndMcp(
`import { spawn } from 'node:child_process';
let child = spawn('old');
class Stop { field = child.kill(); }
child = spawn('new');
new Stop();
const stable = spawn('stable');
stable.kill();
`,
);
const { semantic_graph: graph } =
javascriptApplicationAnalysisResultSchema.parse(evidence.normalized_result);
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const ownership = graph.relations.filter(
({ relation }) => relation === "forwards-signal",
);
expect(ownership).toHaveLength(3);
expect(
ownership.map((relation) => ({
resolution: relation.resolution,
sourceLine: nodes.get(relation.source_node_id)?.identity.source_range
?.start.line,
targetLine: nodes.get(relation.target_node_id)?.identity.source_range
?.start.line,
})),
).toEqual(
expect.arrayContaining([
{ resolution: "candidate", sourceLine: 3, targetLine: 2 },
{ resolution: "candidate", sourceLine: 3, targetLine: 4 },
{ resolution: "resolved", sourceLine: 7, targetLine: 6 },
]),
);
expect(
graph.unknowns.filter(
({ family, reason }) =>
family === "child-process" && reason === "ambiguous-target",
),
).toHaveLength(1);
}, 60000);