* fix(javascript): resolve child ownership at receiver reads * fix(javascript): preserve deferred field child candidates --------- Co-authored-by: Codex <codex@openai.com>
144 lines
4.5 KiB
TypeScript
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);
|