Files
reverse-engineering-anything/tests/boundary/javascript/javascriptSourceAnalysisWorker.test.ts
T
morluto 0f53849160 feat(javascript): make the member-mutation work allowance a runtime setting (#11181)
* feat(javascript): make the member-mutation work allowance a runtime setting

The member-mutation alias-state allowance from #11144 was a fixed constant, so a refused source could not be admitted without a code change.

Register javascript.member_mutation_alias_states (default 524288, 1 to 8388608 alias states) with the launch seed REA_JAVASCRIPT_MEMBER_MUTATION_ALIAS_STATES. The application, web bundle and reference-source boundaries resolve it from the accepted operation snapshot and launch environment, refuse a malformed seed before analysis, and send it to the workers with each source. A refusal names the setting, its effective value, source and range, and the recovery; worker parents rebuild it from the allowance they sent.

Refs #8294

* fix(javascript): keep the setting and recovery of a refused web script

A web bundle script refused by the member-mutation allowance kept only its reason and reported limits, so the public unknown lost the setting, value, source and recovery. Carry the refusal's structured limit and remediation on the per-script result and its javascript_analysis unknown, grouping scripts only when that recovery agrees. Assert structured limit facts and the owning formatter's recovery instead of fixed wording.

* fix(browser): keep only registered setting limits on a refused web script

* feat(javascript): keep syntax facts for a source whose member-mutation phase is refused (#11187)

* feat(javascript): keep syntax facts for a source whose member-mutation phase is refused

A source refused by the member-mutation work allowance during static analysis lost every fact, so the application graph counted it as skipped. Reference-source import failed outright on such a file.

Checkpoint the parser, comment and text facts the static owner can still state, with parse status partial, examination not-examined and refused_phase member-mutations, before reporting the refusal; the semantic phase is not started because it indexes the same mutations. Reference-source import keeps the file's declaration-level imports and records that its CommonJS require calls were not examined.

Refs #8294

* test(javascript): compare the reference-source refusal with its owning formatter

* feat(javascript): keep a refused web script's parse and text facts

A captured script whose member-mutation phase was refused lost every fact in analyze_web_bundle, so it was neither counted as parsed nor kept its vendor markers.

The analyzer now returns the script as unavailable with partial facts: refused phase, parse status and the inferences read from script text alone. Findings gathered before the refusal are dropped, because they may have read an incomplete mutation index. The worker parent states the refusal and its setting recovery from the allowance it sent, through the same helper the isolated analyzer uses for every unavailable script. The bundle counts the script as parsed, keeps its text inferences, and its javascript_analysis unknown carries refused_phase.

Refs #8294

* chore(mcp): record the member-mutation setting in the advertised summary

* fix(javascript): rebuild a parked source worker when the member-mutation allowance changes

A worker parked on a resource scope was reused when heap, timeout and precision matched, but its requests carry the allowance it was built with. After update_analysis_settings accepted a new allowance, the next analysis on that scope kept the old one.

The reuse check now compares the allowance's value and source, so a changed setting gets a worker built with it. The web bundle worker already takes the allowance per script.
2026-11-16 00:05:14 +08:00

1014 lines
35 KiB
TypeScript

import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
import { JavaScriptAnalysisWorker } from "../../../src/javascript/analysis/JavaScriptAnalysisWorker.js";
import { createJavaScriptSourceAnalysis } from "../../../src/artifacts/javascript/JavaScriptSourceAnalysis.js";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { ProcessOwnershipEvidenceOwner } from "../../../src/process/ProcessOwnershipEvidence.js";
import type { OwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { AnalysisResourceConstraintError } from "../../../src/domain/analysisErrorCore.js";
import { JAVASCRIPT_MEMBER_MUTATION_WORK_LIMIT } from "../../../src/domain/javascript/javascriptSemanticWorkAllowance.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import { javaScriptWorkerMemoryLimitBytes } from "../../../src/process/JavaScriptWorkerMemoryLimit.js";
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
const source = (path: string, value: string): JavaScriptArtifactFile => ({
path,
sha256: createHash("sha256").update(value).digest("hex"),
container_sha256: "1".repeat(64),
bytes: Buffer.byteLength(value),
inventory_artifact_id: "fixture-source",
kind: "javascript",
unpacked: false,
text: { included: true, value },
});
/** The Job Object limit the worker requests for its 128 MiB heap budget, if any. */
const processLimit = javaScriptWorkerMemoryLimitBytes(128);
it("reuses one real owned analyzer while retaining distinct source identities and exported returns", async () => {
const launches: number[] = [];
let runtimeRoot: string | undefined;
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
runtimeRoot = request.cwd;
const spawned = await spawnOwnedProviderProcess(request);
const pid = spawned.process.pid;
if (pid === undefined)
throw new Error("Owned analyzer has no observed PID");
launches.push(pid);
return spawned;
},
);
try {
const first = source(
"first.mjs",
"export function create() { return { observed: 7 }; }",
);
const second = source(
"second.mjs",
"export function next() { return { observed: 8 }; }",
);
for (const [index, file] of [first, second].entries()) {
const phases: string[] = [];
const result = await worker.analyze(
file,
{ nodes: 100, unknowns: 100 },
undefined,
async (phase) => {
phases.push(phase);
},
);
if (!result.ok) throw result.error.error;
expect(phases).toEqual([
"parse",
"static",
"semantic",
"projection",
"transfer",
]);
expect(result.value.javascript.parse_status).toBe("complete");
expect(
result.value.module?.callables[0]?.returnSites[0]?.value,
).toMatchObject({
status: "object",
properties: [
{ name: "observed", value: { status: "literal", value: index + 7 } },
],
});
expect(
result.value.projection?.nodes.every(
({ identity }) => identity.artifact_sha256 === file.sha256,
),
).toBe(true);
expect(
result.value.projection?.nodes.some(
({ identity }) => identity.module_path === file.path,
),
).toBe(true);
}
expect(launches).toHaveLength(1);
if (runtimeRoot === undefined)
throw new Error(
"Owned analyzer did not expose its runtime root to launcher",
);
expect(
(await readdir(runtimeRoot)).filter((name) =>
/\.(?:source|static\.json|records)$/u.test(name),
),
).toEqual([]);
} finally {
await worker.close();
}
});
it("applies a changed member-mutation allowance to the next analysis on the same scope", async () => {
const scope = new ArtifactResourceScope();
// Four conditional self-selections before one member write (#8294).
const file = source(
"allowance.js",
[
"function probe(input, env) { var m; if (env.first) m = env.first.info;",
...Array.from(
{ length: 4 },
(_, index) =>
`if (/case${String(index)}/.test(input)) m = m[${String(index % 2)}];`,
),
"m[1] = input.length; return m; } module.exports = probe;",
].join(" "),
);
try {
for (const allowance of [
undefined,
{ aliasStates: 8, source: "runtime" } as const,
undefined,
]) {
const worker = await createJavaScriptSourceAnalysis(
{
heapMb: 128,
timeoutMs: 30_000,
...(allowance === undefined
? {}
: { memberMutationAllowance: allowance }),
},
1,
{ scope },
);
try {
const result = await worker.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (allowance === undefined) {
if (!result.ok) throw result.error.error;
continue;
}
expect(result.ok).toBe(false);
if (result.ok)
throw new Error("Expected the lowered allowance to refuse");
expect(result.error.error).toMatchObject({
limit: {
setting: "javascript.member_mutation_alias_states",
effective_value: 8,
source: "runtime",
},
});
} finally {
await worker.close();
}
}
} finally {
await scope.close();
}
});
it("recovers finite primitive alternatives after changing precision on the same scope", async () => {
const scope = new ArtifactResourceScope();
const expression = Array.from(
{ length: 9 },
(_, index) =>
`(globalThis.choice${String(index)} ? 0 : ${String(2 ** index)})`,
).join(" + ");
const file = source(
"precision.mjs",
`export function choose() { return ${expression}; }`,
);
try {
for (const selected of [undefined, 512, undefined]) {
const worker = await createJavaScriptSourceAnalysis(
{
heapMb: 128,
timeoutMs: 30_000,
...(selected === undefined
? {}
: { semanticPrecision: { primitive_candidates: selected } }),
},
1,
{ scope },
);
try {
const result = await worker.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (!result.ok) throw result.error.error;
const value = result.value.module?.callables[0]?.returnSites[0]?.value;
if (selected === undefined) {
expect(value).toMatchObject({
status: "unknown",
resourceLimit: "primitive-candidates",
});
} else {
expect(value?.status).toBe("union");
if (value?.status !== "union")
throw new Error("Expected all finite alternatives");
expect(value.values).toHaveLength(512);
expect(value.values).toContain(511);
expect(result.value.module?.coverage.precision).toMatchObject({
primitive_candidates: 512,
});
}
} finally {
await worker.close();
}
}
} finally {
await scope.close();
}
});
it("reuses one owned worker across analyses on the same resource scope", async () => {
const scope = new ArtifactResourceScope();
const launches: number[] = [];
const exits = new Map<number, Promise<void>>();
const launcher: typeof spawnOwnedProviderProcess = async (request) => {
const previous = launches.at(-1);
if (previous !== undefined) await exits.get(previous);
const spawned = await spawnOwnedProviderProcess(request);
const pid = spawned.process.pid;
if (pid === undefined)
throw new Error("Owned analyzer has no observed PID");
exits.set(
pid,
new Promise((resolve) => {
spawned.process.once("exit", () => {
resolve();
});
}),
);
launches.push(pid);
return spawned;
};
const limits = { heapMb: 128, timeoutMs: 30_000 };
const first = source("first.mjs", "export const observed = 1;");
const second = source("second.mjs", "export const observed = 2;");
const files = [first, second];
try {
for (const file of files) {
const worker = await createJavaScriptSourceAnalysis(limits, 1, {
scope,
launcher,
});
try {
const result = await worker.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
} finally {
await worker.close();
}
}
expect(launches).toHaveLength(1);
const replaced = await createJavaScriptSourceAnalysis(
{ heapMb: 256, timeoutMs: 30_000 },
1,
{ scope, launcher },
);
try {
const result = await replaced.analyze(first, {
nodes: 100,
unknowns: 100,
});
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
} finally {
await replaced.close();
}
expect(launches).toHaveLength(2);
await scope.close();
const reopened = await createJavaScriptSourceAnalysis(limits, 1, {
scope,
launcher,
});
try {
const result = await reopened.analyze(first, {
nodes: 100,
unknowns: 100,
});
if (!result.ok) throw result.error.error;
} finally {
await reopened.close();
}
expect(launches).toHaveLength(3);
} finally {
await scope.close();
}
});
it("keeps a worker parked while a limit change closes the previous process", async () => {
const scope = new ArtifactResourceScope();
const launches: number[] = [];
const exited = new Set<number>();
const exits = new Map<number, Promise<void>>();
const launcher: typeof spawnOwnedProviderProcess = async (request) => {
const spawned = await spawnOwnedProviderProcess(request);
const pid = spawned.process.pid;
if (pid === undefined)
throw new Error("Owned analyzer has no observed PID");
exits.set(
pid,
new Promise((resolve) => {
spawned.process.once("exit", () => {
exited.add(pid);
resolve();
});
}),
);
launches.push(pid);
return spawned;
};
const limits = { heapMb: 128, timeoutMs: 30_000 };
const file = source("parked.mjs", "export const observed = 1;");
const originalClose = JavaScriptAnalysisWorker.prototype.close;
let releaseClose = (): void => {};
const gate = new Promise<void>((resolve) => {
releaseClose = resolve;
});
let closeCalls = 0;
let markCloseStarted = (): void => {};
const closeStarted = new Promise<void>((resolve) => {
markCloseStarted = resolve;
});
let replacement:
| ReturnType<typeof createJavaScriptSourceAnalysis>
| undefined;
await withVerifierCleanup(async () => {
const initial = await createJavaScriptSourceAnalysis(limits, 1, {
scope,
launcher,
});
const first = await initial.analyze(file, { nodes: 100, unknowns: 100 });
if (!first.ok) throw first.error.error;
await initial.close();
const replacedPid = launches[0];
if (replacedPid === undefined)
throw new Error("Replaced analyzer has no observed PID");
JavaScriptAnalysisWorker.prototype.close = async function (
this: JavaScriptAnalysisWorker,
) {
closeCalls += 1;
if (closeCalls === 1) {
markCloseStarted();
await gate;
}
return originalClose.call(this);
};
replacement = createJavaScriptSourceAnalysis(
{ heapMb: 256, timeoutMs: 30_000 },
1,
{ scope, launcher },
);
await awaitAcknowledgement(closeStarted, replacement, "worker close");
const concurrent = await createJavaScriptSourceAnalysis(limits, 1, {
scope,
launcher,
});
const concurrentResult = await concurrent.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (!concurrentResult.ok) throw concurrentResult.error.error;
await concurrent.close();
const parkedPid = launches[1];
if (parkedPid === undefined)
throw new Error("Parked analyzer has no observed PID");
releaseClose();
const extra = await replacement;
const extraResult = await extra.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (!extraResult.ok) throw extraResult.error.error;
const extraPid = launches[2];
if (extraPid === undefined)
throw new Error("Replacement analyzer has no observed PID");
await extra.close();
await exits.get(replacedPid);
await exits.get(extraPid);
expect(exited.has(replacedPid)).toBe(true);
expect(exited.has(extraPid)).toBe(true);
expect(exited.has(parkedPid)).toBe(false);
await scope.close();
await exits.get(parkedPid);
expect(exited.has(parkedPid)).toBe(true);
}, [
releaseClose,
async () => {
const port = await replacement?.catch(() => undefined);
await port?.close();
},
async () => {
await scope.close();
},
() => {
JavaScriptAnalysisWorker.prototype.close = originalClose;
},
]);
});
it("does not start a replacement when closing the previous worker fails", async () => {
const scope = new ArtifactResourceScope();
const launches: number[] = [];
const exited = new Set<number>();
const exits = new Map<number, Promise<void>>();
const launcher: typeof spawnOwnedProviderProcess = async (request) => {
const previous = launches.at(-1);
if (previous !== undefined) await exits.get(previous);
const spawned = await spawnOwnedProviderProcess(request);
const pid = spawned.process.pid;
if (pid === undefined)
throw new Error("Owned analyzer has no observed PID");
exits.set(
pid,
new Promise((resolve) => {
spawned.process.once("exit", () => {
exited.add(pid);
resolve();
});
}),
);
launches.push(pid);
return spawned;
};
const limits = { heapMb: 128, timeoutMs: 30_000 };
const file = source("replaced.mjs", "export const observed = 1;");
const originalClose = JavaScriptAnalysisWorker.prototype.close;
let closeCalls = 0;
try {
const initial = await createJavaScriptSourceAnalysis(limits, 1, {
scope,
launcher,
});
const first = await initial.analyze(file, { nodes: 100, unknowns: 100 });
if (!first.ok) throw first.error.error;
await initial.close();
const replacedPid = launches[0];
if (replacedPid === undefined)
throw new Error("Replaced analyzer has no observed PID");
JavaScriptAnalysisWorker.prototype.close = async function (
this: JavaScriptAnalysisWorker,
) {
closeCalls += 1;
if (closeCalls === 1) throw new Error("worker still owned");
return originalClose.call(this);
};
await expect(
createJavaScriptSourceAnalysis({ heapMb: 256, timeoutMs: 30_000 }, 1, {
scope,
launcher,
}),
).rejects.toThrow("worker still owned");
expect(launches).toEqual([replacedPid]);
expect(exited.has(replacedPid)).toBe(false);
JavaScriptAnalysisWorker.prototype.close = originalClose;
const replacement = await createJavaScriptSourceAnalysis(
{ heapMb: 256, timeoutMs: 30_000 },
1,
{ scope, launcher },
);
try {
const result = await replacement.analyze(file, {
nodes: 100,
unknowns: 100,
});
if (!result.ok) throw result.error.error;
} finally {
await replacement.close();
}
expect(launches).toHaveLength(2);
expect(exited.has(replacedPid)).toBe(true);
const replacementPid = launches[1];
if (replacementPid === undefined)
throw new Error("Replacement analyzer has no observed PID");
expect(exited.has(replacementPid)).toBe(false);
} finally {
JavaScriptAnalysisWorker.prototype.close = originalClose;
await scope.close();
}
});
it("reports an actual startup deadline and closes its owned resources before subsequent analysis", async () => {
const file = source("app.mjs", "export const observed = 7;");
const expired = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 1 });
try {
const result = await expired.analyze(file, { nodes: 100, unknowns: 100 });
if (result.ok)
throw new Error(
"A one-millisecond fresh process deadline unexpectedly completed",
);
expect(result.error.error).toMatchObject({
_tag: "AnalysisTimeoutError",
timeoutMs: 1,
});
} finally {
await expired.close();
}
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const result = await next.analyze(file, { nodes: 100, unknowns: 100 });
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
} finally {
await next.close();
}
});
it("preserves a failed owned cleanup retry when the next caller cancels before acquisition", async () => {
const controller = new AbortController();
let releaseAllowed = false;
let cleanupAttempts = 0;
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
const launched = await spawnOwnedProviderProcess(request);
return {
...launched,
async cleanup() {
cleanupAttempts += 1;
if (!releaseAllowed) {
if (cleanupAttempts > 1) controller.abort();
throw new Error("Fixture owned cleanup is unavailable");
}
return launched.cleanup === undefined
? cleanupOwnedProcessGroup(launched.ownership)
: launched.cleanup();
},
};
},
);
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const file = source("app.mjs", "export const observed = 7;");
const first = await worker.analyze(file, { nodes: 100, unknowns: 100 });
if (!first.ok) throw first.error.error;
const closing = worker.close();
await expect(closing).rejects.toBeInstanceOf(ProviderCleanupError);
await expect(closing).rejects.toMatchObject({
cause: { message: "Fixture owned cleanup is unavailable" },
});
const interrupted = await next.analyze(
file,
{ nodes: 100, unknowns: 100 },
controller.signal,
);
if (interrupted.ok)
throw new Error("Unreleased ownership must block subsequent analysis");
expect(controller.signal.aborted).toBe(true);
expect(projectAnalysisError(interrupted.error.error)).toMatchObject({
code: "cleanup_incomplete",
});
releaseAllowed = true;
await worker.close();
const recovered = await next.analyze(file, { nodes: 100, unknowns: 100 });
if (!recovered.ok) throw recovered.error.error;
expect(recovered.value.javascript.parse_status).toBe("complete");
} finally {
releaseAllowed = true;
await worker.close();
await next.close();
}
});
it("ends worker stdin and unreferences process handles when cleanup reports inspection uncertainty", async () => {
let stdinEnded = false;
let unreferenced = false;
let permitCleanup = false;
let childStdin: typeof process.stdin | undefined;
let childExit: Promise<void> | undefined;
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
const launched = await spawnOwnedProviderProcess(request);
childExit = new Promise((resolve) => {
launched.process.once("exit", () => resolve());
});
const originalUnref = launched.process.unref;
launched.process.unref = () => {
unreferenced = true;
originalUnref?.call(launched.process);
};
childStdin = launched.process.stdin as unknown as typeof process.stdin;
launched.process.stdin?.once("finish", () => {
stdinEnded = true;
});
return {
...launched,
cleanup: async () =>
permitCleanup
? launched.cleanup === undefined
? cleanupOwnedProcessGroup(launched.ownership)
: launched.cleanup()
: {
cleaned: false,
reason: "process table could not be inspected: spawn EPERM",
},
};
},
);
try {
const file = source("app.mjs", "export const observed = 7;");
const result = await worker.analyze(file, { nodes: 100, unknowns: 100 });
if (!result.ok) throw result.error.error;
const closing = worker.close();
await expect(closing).rejects.toBeInstanceOf(ProviderCleanupError);
await expect(closing).rejects.toMatchObject({
cause: {
message: "process table could not be inspected: spawn EPERM",
},
});
expect(childStdin?.writableEnded || stdinEnded).toBe(true);
expect(unreferenced).toBe(true);
const errorListeners = childStdin?.listenerCount("error");
for (let retry = 0; retry < 3; retry++)
await expect(worker.close()).rejects.toBeInstanceOf(ProviderCleanupError);
expect(childStdin?.listenerCount("error")).toBeLessThanOrEqual(
errorListeners ?? 0,
);
await childExit;
permitCleanup = true;
await worker.close();
} finally {
permitCleanup = true;
await worker.close().catch(() => {});
}
});
it("lets a separate host exit while its live worker cleanup remains incomplete", async () => {
const root = await createTestTempDirectory("rea-worker-incomplete-host-");
const ownershipPath = join(root, "ownership.json");
const workerUrl = new URL(
"../../../dist/javascript/analysis/JavaScriptAnalysisWorker.js",
import.meta.url,
).href;
const launcherUrl = new URL(
"../../../dist/process/ProviderProcess.js",
import.meta.url,
).href;
const script = `
import { writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { JavaScriptAnalysisWorker } from ${JSON.stringify(workerUrl)};
import { spawnOwnedProviderProcess } from ${JSON.stringify(launcherUrl)};
const worker = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 10000 }, async request => {
const entry = pathToFileURL(request.arguments.at(-1)).href;
const launched = await spawnOwnedProviderProcess({ ...request, arguments: [
...request.arguments.slice(0, -1), "--input-type=module", "-e",
"const interval = setInterval(() => {}, 1000); setTimeout(() => clearInterval(interval), 60000); await import(" + JSON.stringify(entry) + ");"
] });
// The launcher is deliberately orphaned when this host exits; cleanup still
// requires its run token and executable identity, rather than its old parent.
// The live ledger belongs to this host and cannot be serialized. Transfer
// only the process facts needed for the independent fixture cleanup.
const { expectedParentPid, evidenceOwner, ...ownership } = launched.ownership;
await writeFile(${JSON.stringify(ownershipPath)}, JSON.stringify(ownership));
return { ...launched, cleanup: async () => ({ cleaned: false, reason: "fixture inspection unavailable" }) };
});
const analyzed = await worker.analyze(${JSON.stringify(source("app.mjs", "export const value = 1;"))}, { nodes: 100, unknowns: 100 });
if (!analyzed.ok) throw analyzed.error.error;
try { await worker.close(); throw new Error("cleanup unexpectedly completed"); }
catch (error) {
if (error.cleanupIncomplete !== true) throw error;
process.stdout.write(JSON.stringify({ code: "cleanup_incomplete", reason: error.cause.message }) + "\\n");
}
`;
try {
const result = await promisify(execFile)(
process.execPath,
["--input-type=module", "-e", script],
{ timeout: 10_000 },
);
expect(JSON.parse(result.stdout)).toMatchObject({
code: "cleanup_incomplete",
reason: "fixture inspection unavailable",
});
} finally {
const ownership = JSON.parse(
await readFile(ownershipPath, "utf8"),
) as OwnedProcessGroup;
expect(
await cleanupOwnedProcessGroup({
...ownership,
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
}),
).toMatchObject({ cleaned: true });
}
});
// The real server reports a parent-declined semantic budget through its
// resource frame.
it("formats a real worker's declined semantic budget once", async () => {
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
const spawned = await spawnOwnedProviderProcess(request);
const stdin = spawned.process.stdin;
if (stdin === null || stdin === undefined)
throw new Error("Owned analyzer has no stdin");
const write = stdin.write.bind(stdin) as (
chunk: string,
callback: (error?: Error | null) => void,
) => boolean;
stdin.write = ((
chunk: string,
callback: (error?: Error | null) => void,
) =>
write(
chunk.replace(
/"semantic_result_budget_bytes":\d+/u,
'"semantic_result_budget_bytes":0',
),
callback,
)) as typeof stdin.write;
return spawned;
},
);
try {
const result = await worker.analyze(
source("main.js", "export const observed = 7;"),
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a declined semantic budget");
const { error } = result.error;
expect(error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
reason: expect.stringMatching(
/result capacity.*static observations.*semantic analysis of main\.js was not started/i,
),
reportedLimits: { semantic_result_budget_bytes: 0 },
});
expect(error.message.match(/Resource constraint during/gu)).toHaveLength(1);
expect(error.message.match(/main\.js:/gu)).toBeNull();
expect(result.error.javascript?.parse_status).toBe("complete");
} finally {
await worker.close();
}
});
// The handshake mirrors the real server: only Node.js reports an enforced V8 heap limit.
const resourceFailureWorker = (
heapLimit: string,
failure: { readonly resource: string; readonly limits: unknown } = {
resource: "memory",
limits: null,
},
) =>
new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 }, (request) =>
spawnOwnedProviderProcess({
...request,
arguments: [
"-e",
`const heap = ${heapLimit};
process.stdout.write(JSON.stringify({ kind: "ready", heap_limit_bytes: heap }) + "\\n");
require("node:readline").createInterface({ input: process.stdin }).once("line", (line) => {
const { id } = JSON.parse(line);
process.stdout.write(JSON.stringify({ kind: "failure", id, reason: "resource", ...${JSON.stringify(failure)}, message: "fixture" }) + "\\n");
});`,
],
}),
);
it("reports the worker heap limit only when the runtime enforces V8 heap flags", async () => {
const worker = resourceFailureWorker(
'process.versions.bun === undefined ? require("node:v8").getHeapStatistics().heap_size_limit : null',
);
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a reported resource failure");
expect(result.error.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
reportedLimits:
process.versions.bun === undefined
? {
worker_old_space_mb: 128,
worker_heap_limit_bytes: expect.any(Number),
worker_timeout_ms: 30_000,
}
: {
worker_heap_limit_bytes: null,
...(processLimit === null
? {}
: { worker_process_memory_limit_bytes: processLimit }),
worker_timeout_ms: 30_000,
},
});
} finally {
await worker.close();
}
});
it("sends the accepted member-mutation allowance and names its setting on refusal", async () => {
// The fixture reports the allowance the parent sent, as the real worker does.
const worker = new JavaScriptAnalysisWorker(
{
heapMb: 128,
timeoutMs: 30_000,
memberMutationAllowance: { aliasStates: 4096, source: "runtime" },
},
(request) =>
spawnOwnedProviderProcess({
...request,
arguments: [
"-e",
`process.stdout.write(JSON.stringify({ kind: "ready", heap_limit_bytes: process.versions.bun === undefined ? require("node:v8").getHeapStatistics().heap_size_limit : null }) + "\\n");
require("node:readline").createInterface({ input: process.stdin }).once("line", (line) => {
const request = JSON.parse(line);
process.stdout.write(JSON.stringify({ kind: "failure", id: request.id, reason: "resource", resource: "cpu", limits: { ${JAVASCRIPT_MEMBER_MUTATION_WORK_LIMIT}: request.member_mutation_allowance.alias_states, sent_source: request.member_mutation_allowance.source }, message: "fixture" }) + "\\n");
});`,
],
}),
);
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a reported resource failure");
const error = result.error.error;
if (!(error instanceof AnalysisResourceConstraintError))
throw new Error("expected a resource constraint");
expect(error).toMatchObject({
resource: "cpu",
retryable: false,
reportedLimits: {
[JAVASCRIPT_MEMBER_MUTATION_WORK_LIMIT]: 4096,
sent_source: "runtime",
},
limit: {
setting: "javascript.member_mutation_alias_states",
effective_value: 4096,
source: "runtime",
},
});
expect(error.fixedAllowance).toBeUndefined();
expect(error.remediationAction).toContain("update_analysis_settings");
} finally {
await worker.close();
}
});
it("rejects a worker heap report that contradicts the launching runtime", async () => {
const worker = resourceFailureWorker(
"process.versions.bun === undefined ? null : 1",
);
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a handshake failure");
expect(result.error.error._tag).toBe("AnalysisOutputError");
} finally {
await worker.close();
}
});
// The launcher runs a real child without a job limit, so any artifact can run
// it, and its handle reports the notification an owned Windows job records
// before exit. Bun exits 3 (panic) or 1 (exception) at a reached job limit.
it.each([true, false])(
"classifies a worker exit by the job memory notification (reached: %s)",
async (reached) => {
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async ({ memoryLimitBytes, ...request }) => {
expect(memoryLimitBytes).toBe(processLimit ?? undefined);
const spawned = await spawnOwnedProviderProcess({
...request,
arguments: [
"-e",
`const heap = process.versions.bun === undefined ? require("node:v8").getHeapStatistics().heap_size_limit : null;
process.stdout.write(JSON.stringify({ kind: "ready", heap_limit_bytes: heap }) + "\\n");
require("node:readline").createInterface({ input: process.stdin }).once("line", () => {
process.stderr.write("panic: out of memory");
process.exit(3);
});`,
],
});
let exited = false;
spawned.process.once("exit", () => {
exited = true;
});
Object.defineProperty(spawned.process, "memoryLimitReached", {
get: () => reached && exited,
});
return spawned;
},
);
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected the worker to fail");
expect(result.error.error).toMatchObject(
reached
? {
_tag: "AnalysisResourceConstraintError",
resource: "memory",
reason: expect.stringContaining("process memory limit"),
reportedLimits:
processLimit === null
? expect.not.objectContaining({
worker_process_memory_limit_bytes: expect.anything(),
})
: { worker_process_memory_limit_bytes: processLimit },
capturedOutput: { stderr: "panic: out of memory" },
}
: {
_tag: "ProviderAdapterError",
diagnostics: { exit_code: 3 },
},
);
} finally {
await worker.close();
}
},
);
it("retains legacy routes and a bounded route-fact prefix inside one checkpoint budget", async () => {
const count = 12000;
const text = Array.from(
{ length: count },
(_, index) => `route('/reports/${index}/${"x".repeat(96)}');`,
).join("\n");
const worker = new JavaScriptAnalysisWorker({
heapMb: 512,
timeoutMs: 30000,
});
try {
const analyzed = await worker.analyze(source("routes.js", text), {
nodes: 1,
unknowns: 8,
});
const facts = analyzed.ok
? analyzed.value.javascript
: analyzed.error.javascript;
expect(
facts,
analyzed.ok ? undefined : analyzed.error.error.message,
).not.toBeNull();
expect(facts!.endpoints).toHaveLength(count);
expect(facts!.routes!.candidates.length).toBeGreaterThan(0);
expect(facts!.routes!.candidates.length).toBeLessThan(count);
expect(facts!.routes!.capacity).toMatchObject({ kind: "result-bytes" });
expect(Buffer.byteLength(JSON.stringify(facts))).toBeLessThanOrEqual(
8 * 1024 * 1024,
);
} finally {
await worker.close();
}
});
it("keeps wrapped route ancestry, navigation and unknown named parents in the real worker", async () => {
const worker = new JavaScriptAnalysisWorker({
heapMb: 128,
timeoutMs: 30000,
});
try {
const analyzed = await worker.analyze(
source(
"routes.ts",
"const detail={_$:'detail'};defineRoutes({reports:{_$:'reports',detail:(detail as typeof detail)}});router.push(({pathname:'/users'} as const));router.push(({pathname:'/settings'} satisfies {pathname:string}));router.addRoute('admin',({path:'settings',component:View} as const));router.addRoute('admin',{path:'relative',children:[{path:'/public',component:View},{path:'relative'}]});defineRoutes({generated:buildRoutes()});",
),
{ nodes: 1, unknowns: 8 },
);
if (!analyzed.ok) throw analyzed.error.error;
expect(
analyzed.value.javascript.routes!.candidates.map(({ path }) => path),
).toEqual([
"/users",
"/settings",
"/reports",
"/reports/detail",
"/public",
]);
expect(analyzed.value.javascript.routes!.unresolved).toHaveLength(3);
expect(analyzed.value.javascript.routes!.unresolved[0]!.reason).toContain(
"parent",
);
} finally {
await worker.close();
}
});