rea/tests/acceptance/applications/javascriptBuiltinDefaultsCl...

403 lines
14 KiB
TypeScript

import { execFile } from "node:child_process";
import { 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 execute = promisify(execFile);
it("preserves earlier and sibling effects without inventing replaced cached-module or process effects", async () => {
const root = await createTestTempDirectory("rea-builtin-identity-");
await writeFile(
join(root, "app.cjs"),
`
throw new Error('parse only');
const a = require('node:fs'); const b = require('fs');
b.readFileSync('before.json'); const stream = b.createReadStream('before.json'); stream.destroy();
patch(a); b.readFileSync('wrong-after.json');
const proc = require('node:process'); proc.env = {}; proc.argv = []; const mode = proc.env.MODE; const arg = proc.argv[2];
`,
);
await writeFile(
join(root, "sibling.mjs"),
`import fs from 'node:fs'; fs.writeFileSync = custom; fs.readFileSync('sibling.json'); fs.readFileSync = custom;`,
);
await writeFile(
join(root, "captures.cjs"),
`const fs = require('node:fs');
const saved = require('fs').readFileSync;
const { readFileSync: savedDestructured } = fs;
fs.readFileSync('before-write.json'); fs.readFileSync = custom;
const { readFileSync: late } = require('fs');
const lateMember = require('fs').readFileSync;
saved('saved.json'); savedDestructured('saved-destructured.json');
late('wrong-late.json'); lateMember('wrong-member.json');`,
);
await writeFile(
join(root, "direct.cjs"),
`require('node:fs').readFileSync('direct.json');
const fs = require('fs'); fs.readFileSync('later.json');
require('fs/promises').open('before');
fs.promises.open = custom; require('node:fs/promises').open('wrong');`,
);
await writeFile(
join(root, "loader.cjs"),
`require = () => ({ readFileSync() {} });
const fs = require('node:fs'); fs.readFileSync('wrong-loader.json');
require('node:fs').readFileSync('wrong-direct-loader.json');`,
);
await writeFile(
join(root, "imports.ts"),
`import fs = require('node:fs'); const other = require('fs');
const read = other.readFileSync; observe(read); read('escaped.json');
fs.readFileSync = custom; other.readFileSync('wrong-import-equals.json');`,
);
await writeFile(
join(root, "late-loader.cjs"),
`const fs=require('fs'); fs.readFileSync('loader-before.json');
require=replacement; fs.readFileSync('loader-after.json');
require('fs').readFileSync('wrong-late-loader.json');`,
);
await writeFile(
join(root, "replacement.ts"),
`import fs = require('fs'); const other=require('fs'); fs={}; fs.readFileSync=custom; other.readFileSync('replacement-kept.json');`,
);
await writeFile(
join(root, "dynamic.cjs"),
`const fs=require('fs'); fs.hook=custom; fs[key](); fs.readFileSync('wrong-dynamic.json');`,
);
await writeFile(
join(root, "wrapped.cjs"),
`const fs=require('fs'); let assigned; const saved=(0, fs.readFileSync); fs.readFileSync=custom; const late=(assigned=fs.readFileSync); saved('wrapped-kept.json'); late('wrong-wrapped.json');`,
);
await writeFile(
join(root, "global-process.cjs"),
`process.env={MODE:'local'}; const proc=require('process'); const mode=proc.env.MODE;`,
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(
result.semantic_graph.nodes
.filter(({ kind }) => kind === "config-source")
.map(({ properties }) => properties.key)
.sort(),
).toEqual([
"before-write.json",
"before.json",
"direct.json",
"escaped.json",
"later.json",
"loader-after.json",
"loader-before.json",
"replacement-kept.json",
"saved-destructured.json",
"saved.json",
"sibling.json",
"wrapped-kept.json",
]);
expect(
result.semantic_graph.nodes.filter(({ kind }) => kind === "resource"),
).toHaveLength(2);
expect(
result.semantic_graph.relations.filter(
({ relation }) => relation === "releases",
),
).toHaveLength(1);
const { client, call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "auto",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
await client.ping();
}, 60000);
it("excludes monkey-patched CommonJS effects from CLI and MCP evidence", async () => {
const root = await createTestTempDirectory("rea-builtin-mutated-");
await writeFile(
join(root, "app.cjs"),
`
throw new Error("parse only");
const fs = require("node:fs"); fs.readFileSync = custom; fs.createReadStream = custom;
const cp = require("node:child_process"); cp.spawn = custom;
const timers = require("node:timers"); patch(timers);
const http = require("node:http"); delete http.get;
fs.readFileSync("config.json"); const stream = fs.createReadStream("config.json"); stream.destroy();
const child = cp.spawn("tool", []); child.kill();
const timer = timers.setTimeout(callback, 1); clearTimeout(timer);
http.get("http://example.test");
`,
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(
result.semantic_graph.nodes.filter(({ kind }) =>
[
"config-source",
"resource",
"child-process",
"timer",
"request",
].includes(kind),
),
).toEqual([]);
expect(
result.semantic_graph.relations.filter(({ relation }) =>
["releases", "forwards-signal", "cancels-timer"].includes(relation),
),
).toEqual([]);
const { client, call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "auto",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
await client.ping();
}, 60000);
it("retains CommonJS builtin effects and handle links through CLI and MCP", async () => {
const root = await createTestTempDirectory("rea-builtin-commonjs-");
await writeFile(
join(root, "app.cjs"),
`
throw new Error("This source must only be parsed, never executed");
const fs = require("node:fs");
const {spawn: launch} = require("node:child_process");
const timers = require("timers");
const {get} = require("http");
const proc = require("node:process");
const text = fs.readFileSync("config.json", "utf8");
const stream = fs.createReadStream("config.json"); stream.destroy();
const child = launch("tool", []); child.kill("SIGTERM");
const timer = timers.setTimeout(callback, 10); timers.clearTimeout(timer);
get("http://example.test");
const mode = proc.env.MODE;
const absent = require("node:fs").default; absent.readFileSync("wrong-default");
const external = require("./fs.js"); external.readFileSync("wrong-external");
function shadowed(require) { const fs = require("node:fs"); fs.readFileSync("wrong"); }
`,
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
const nodes = result.semantic_graph.nodes;
expect(
nodes.filter(({ kind }) =>
[
"config-source",
"resource",
"child-process",
"timer",
"request",
].includes(kind),
),
).toHaveLength(6);
expect(nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "config-source",
properties: expect.objectContaining({ key: "config.json" }),
}),
expect.objectContaining({
kind: "config-source",
properties: expect.objectContaining({ key: "MODE" }),
}),
...["createReadStream", "spawn", "setTimeout", "get"].map((method) =>
expect.objectContaining({
properties: expect.objectContaining({ method }),
}),
),
]),
);
const relations = result.semantic_graph.relations;
for (const relation of ["releases", "forwards-signal", "cancels-timer"])
expect(relations).toEqual(
expect.arrayContaining([
expect.objectContaining({ relation, resolution: "resolved" }),
]),
);
const { client, call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "auto",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
await client.ping();
}, 60000);
it.each([
"import fs from 'node:fs';",
"import {default as fs} from 'node:fs';",
])(
"retains filesystem effects in the public CLI with %s",
async (declaration) => {
const root = await createTestTempDirectory("rea-builtin-default-cli-");
await writeFile(
join(root, "app.js"),
`${declaration} const text=fs.readFileSync('config.json','utf8'); const stream=fs.createReadStream('config.json'); stream.destroy();`,
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
);
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
);
expect(result.semantic_graph.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "config-source",
properties: expect.objectContaining({ key: "config.json" }),
}),
expect.objectContaining({
kind: "resource",
properties: expect.objectContaining({ method: "createReadStream" }),
}),
]),
);
expect(result.semantic_graph.relations).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "releases",
resolution: "resolved",
}),
]),
);
},
20_000,
);
it("keeps only direct logical defaults and actual argv elements in the public CLI graph", async () => {
const root = await createTestTempDirectory("rea-builtin-default-cli-");
const manyDefaults = Array.from(
{ length: 400 },
(_, index) =>
`const value${index} = process.env.KEY_${index} ?? "fallback-${index}";`,
).join("\n");
await writeFile(
join(root, "app.js"),
[
`const chained = process.env.CHAINED ?? "fallback" ?? "unreachable";
const wrapped = ((process.env.WRAPPED)) ?? "wrapped";
const argument = process.argv[2];
const argumentLength = process.argv.length;
const namedArgument = process.argv["02"];
const negativeArgument = process.argv["-1"];
const method = process.argv.map;
const lastArrayIndex = process.argv[4294967294];
const outOfRangeArgument = process.argv[4294967295];
const dynamicArgument = process.argv[process.env.ARGV_INDEX];
function shadowed(process) { return process.argv[3]; }`,
manyDefaults,
].join("\n"),
);
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
);
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
);
const configSources = result.semantic_graph.nodes.filter(
({ kind }) => kind === "config-source",
);
const defaults = configSources.filter(
({ properties }) => properties.source_kind === "default",
);
expect(defaults).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
key: "CHAINED",
value: "fallback",
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
key: "WRAPPED",
value: "wrapped",
}),
}),
]),
);
expect(
defaults.filter(({ properties }) => properties.key === "CHAINED"),
).toHaveLength(1);
expect(defaults).toHaveLength(402);
const defaultsByKey = new Map(
defaults.map(({ properties }) => [properties.key, properties.value]),
);
for (let index = 0; index < 400; index += 1)
expect(defaultsByKey.get(`KEY_${index}`)).toBe(`fallback-${index}`);
const argvSources = configSources.filter(
({ properties }) => properties.source_kind === "argv",
);
expect(argvSources).toHaveLength(3);
expect(argvSources).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({ key: "2" }),
}),
expect.objectContaining({
properties: expect.objectContaining({ key: "4294967294" }),
}),
expect.objectContaining({
properties: expect.objectContaining({ key: null }),
}),
]),
);
expect(
result.semantic_graph.relations
.filter(({ relation }) => relation === "reads-argv")
.map(({ resolution }) => resolution),
).toEqual(expect.arrayContaining(["resolved", "candidate"]));
expect(
result.semantic_graph.relations.filter(
({ relation, source_node_id, target_node_id }) => {
if (relation !== "overrides") return false;
const source = result.semantic_graph.nodes.find(
({ node_id }) => node_id === source_node_id,
);
const target = result.semantic_graph.nodes.find(
({ node_id }) => node_id === target_node_id,
);
return (
source?.properties.key === "CHAINED" &&
target?.properties.key === "CHAINED"
);
},
),
).toHaveLength(1);
}, 20_000);