Files
rea/tests/acceptance/applications/javascriptReplacedGlobalEffectsCli.test.ts
T
amyanger 7436ac8e20 fix(javascript): drop built-in effects for visibly replaced globals (#4565)
The semantic collectors admitted Node timers, fetch, WebSocket, JSON.parse
and global coercions from the identifier spelling alone, so a source that
replaced those globals (`setTimeout = () => null`) still produced resolved
built-in effects. Require the existing isUnmodifiedGlobalReference guard,
which the static analyzer already uses for fetch and WebSocket, before
admitting these effects.

The guard treats any visible possible write as a replacement without
evaluating order or reachability, so computed global writes and guarded
polyfills also remove the resolved effect. The workflow docs now say so.

Fixes #3367
2026-10-23 02:28:53 +08:00

71 lines
2.2 KiB
TypeScript

import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const EFFECT_RELATIONS = [
"schedules-timer",
"constructs-request",
"parses",
"coerces",
];
const SOURCE = `function work() {}
setTimeout(work, 100);
fetch('https://example.invalid');
new WebSocket('wss://example.invalid');
JSON.parse('null');
String(work);
`;
const REPLACEMENTS = `setTimeout = () => null;
fetch = () => null;
WebSocket = class {};
JSON.parse = () => null;
String = () => '';
`;
cliTest.for(["CLI", "stdio MCP"] as const)(
"does not resolve built-in effects after visible global replacements through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-replaced-globals-");
await writeFixtureFiles(root, {
"replaced/main.mjs": REPLACEMENTS + SOURCE,
"control/main.mjs": SOURCE,
});
const effects = async (directory: string) => {
const graph = (
await analyzeJavaScriptDirectory(surface, cli, `${root}/${directory}`)
).semantic_graph;
if (graph === null) throw new Error("Expected a semantic graph");
return {
relations: graph.relations
.filter(({ relation }) => EFFECT_RELATIONS.includes(relation))
.map(({ relation, resolution }) => `${relation}:${resolution}`)
.sort(),
boundaryUnknowns: graph.unknowns
.filter(({ family }) => family === "boundary")
.map(({ detail }) => detail),
};
};
// The replaced JSON.parse remains only the generic `.parse` candidate.
await expect(effects("replaced")).resolves.toEqual({
relations: ["parses:candidate"],
boundaryUnknowns: ["Static parse boundary kind is a candidate."],
});
await expect(effects("control")).resolves.toEqual({
relations: [
"coerces:resolved",
"constructs-request:resolved",
"constructs-request:resolved",
"parses:resolved",
"schedules-timer:resolved",
],
boundaryUnknowns: [],
});
},
);