import { createHash } from "node:crypto"; import { expect, it } from "vitest"; import { AnalysisCancelledError, AnalysisOutputError, } from "../../../src/domain/analysisErrorCore.js"; import { JavaScriptAnalysisWorker } from "../../../src/javascript/analysis/JavaScriptAnalysisWorker.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js"; const sourceText = "export const observed = 1;"; const file: JavaScriptArtifactFile = { path: "app.mjs", sha256: createHash("sha256").update(sourceText).digest("hex"), container_sha256: "1".repeat(64), bytes: Buffer.byteLength(sourceText), inventory_artifact_id: "fixture-source", kind: "javascript", unpacked: false, text: { included: true, value: sourceText }, }; const ready = 'process.stdout.write(JSON.stringify({kind:"ready",heap_limit_bytes:require("node:v8").getHeapStatistics().heap_size_limit})+"\\n");'; const phaseAfterRequest = 'process.stdin.once("data", chunk => { const request=JSON.parse(String(chunk)); process.stdout.write(JSON.stringify({kind:"phase",id:request.id,phase:"parse"})+"\\n");'; const staticCheckpointFacts = { parse_status: "complete", parse_error_count: 0, visited_ast_nodes: 1, references: [], endpoints: [], storage: [], bundler_registrations: [], role_paths: [], source_map_urls: [], vendors: [], electron: { browser_windows: [], context_bridge_apis: [], ipc: [], sender_validations: [], utility_processes: [], native_addon_bindings: [], }, limitations: [], }; it.each([ { memoryLimitReached: false, reason: { requested: "interrupt after worker exit" }, }, { memoryLimitReached: false, reason: null }, { memoryLimitReached: true, reason: { requested: "interrupt after memory limit" }, }, { memoryLimitReached: true, reason: null }, ])( "keeps an owned worker exit primary and retains later abort ($memoryLimitReached, $reason)", async ({ memoryLimitReached, reason }) => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => process.exit(7));`, ], }); let exited = false; launched.process.once("exit", () => { exited = true; setImmediate(() => controller.abort(reason)); }); Object.defineProperty(launched.process, "memoryLimitReached", { get: () => memoryLimitReached && exited, }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.reason).toBe(reason); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error).toMatchObject( memoryLimitReached ? { _tag: "AnalysisResourceConstraintError", resource: "memory" } : { _tag: "ProviderAdapterError", diagnostics: { exit_code: 7 }, }, ); expect(result.error.error.secondaryFailures).toHaveLength(1); expect(result.error.error.secondaryFailures[0]).toBeInstanceOf( AnalysisCancelledError, ); expect(result.error.error.secondaryFailures[0]?.cause).toBe(reason); } } finally { await worker.close(); } }, ); it.each([ { label: "an AnalysisOutputError instance", reason: new AnalysisOutputError("fixture", "opaque caller value"), }, { label: "explicit null", reason: null }, ])( "preserves caller cancellation after the request barrier with $label", async ({ reason }) => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} ${phaseAfterRequest} setTimeout(() => process.exit(7), 500); });`, ], }); let acknowledged = false; launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes('"kind":"phase"') && !acknowledged) { acknowledged = true; setImmediate(() => controller.abort(reason)); } }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(reason); } } finally { await worker.close(); } }, ); it("clears a consumed resource frame before the next reusable worker job", async () => { const controller = new AbortController(); let launchCount = 0; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { launchCount += 1; const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} let count=0; require("node:readline").createInterface({input:process.stdin}).on("line", line => { const {id}=JSON.parse(line); const first=++count===1; const response={kind:"failure",id,reason:first?"resource":"output",resource:first?"memory":null,limits:null,message:first?"first resource response":"second job output response"}; process.stdout.write(JSON.stringify(response)+"\\n"); if(!first)setInterval(()=>{},1000); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes("second job output response")) setTimeout(() => controller.abort("abort after second response"), 5); }); return launched; }, ); try { const first = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(first.ok).toBe(false); if (!first.ok) expect(first.error.error).toMatchObject({ _tag: "AnalysisResourceConstraintError", reason: "first resource response", }); const second = await worker.analyze( { ...file, path: "next.mjs" }, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(second.ok).toBe(false); if (!second.ok) expect(second.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "next.mjs during source-transfer: second job output response", }); expect(launchCount).toBe(1); } finally { await worker.close(); } }); it("keeps coalesced static and resource frames in FIFO order and reuses the worker", async () => { const encodedStatic = JSON.stringify(staticCheckpointFacts); let launchCount = 0; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { launchCount += 1; return spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const fs=require("node:fs"); const {createHash}=require("node:crypto"); const staticFacts=JSON.parse(${JSON.stringify(encodedStatic)}); require("node:readline").createInterface({input:process.stdin}).on("line", line => { const request=JSON.parse(line); if(!request.file)return; const text=JSON.stringify(staticFacts); fs.writeFileSync(request.id+".static.json",text); const frames=[{kind:"static",id:request.id,bytes:Buffer.byteLength(text),sha256:createHash("sha256").update(text).digest("hex")},{kind:"failure",id:request.id,reason:"resource",resource:"memory",limits:null,message:"fixture resource after static"}]; process.stdout.write(frames.map(frame=>JSON.stringify(frame)).join("\\n")+"\\n"); });`, ], }); }, ); try { for (const path of ["first.mjs", "second.mjs"]) { const result = await worker.analyze( { ...file, path }, { nodes: 100, unknowns: 100 }, ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error).toMatchObject({ _tag: "AnalysisResourceConstraintError", reason: "fixture resource after static", }); expect(result.error.javascript?.parse_status).toBe("complete"); } } expect(launchCount).toBe(1); } finally { await worker.close(); } }); it("retains coalesced static evidence when abort follows a resource frame", async () => { const controller = new AbortController(); const encodedStatic = JSON.stringify(staticCheckpointFacts); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const fs=require("node:fs"); const {createHash}=require("node:crypto"); const staticFacts=JSON.parse(${JSON.stringify(encodedStatic)}); require("node:readline").createInterface({input:process.stdin}).on("line", line => { const request=JSON.parse(line); if(!request.file)return; const text=JSON.stringify(staticFacts); fs.writeFileSync(request.id+".static.json",text); const frames=[{kind:"static",id:request.id,bytes:Buffer.byteLength(text),sha256:createHash("sha256").update(text).digest("hex")},{kind:"failure",id:request.id,reason:"resource",resource:"memory",limits:null,message:"resource failure before abort"}]; process.stdout.write(frames.map(frame=>JSON.stringify(frame)).join("\\n")+"\\n"); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes("resource failure before abort")) setTimeout(() => controller.abort(null), 5); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error).toMatchObject({ _tag: "AnalysisResourceConstraintError", reason: "resource failure before abort", }); expect(result.error.javascript?.parse_status).toBe("complete"); } } finally { await worker.close(); } }); it("selects output EOF before a later abort when the current job has no terminal frame", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => process.exit(0));`, ], }); launched.process.once("close", () => { queueMicrotask(() => controller.abort({ requested: "abort after EOF" }), ); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "JavaScript worker closed output before the operation received its terminal response", }); } finally { await worker.close(); } }); it("selects startup EOF before an abort queued by the same close event", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: ["-e", "process.exit(0);"], }); launched.process.once("close", () => { queueMicrotask(() => controller.abort({ requested: "abort after startup EOF" }), ); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "JavaScript worker closed output before the operation received its terminal response", }); } finally { await worker.close(); } }); it("keeps a parsed terminal failure primary over startup-close abort", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", chunk => { const id=JSON.parse(String(chunk)).id; process.stdout.write(JSON.stringify({kind:"failure",id,reason:"output",resource:null,limits:null,message:"terminal failure before EOF"})+"\\n",()=>process.exit(0)); });`, ], }); launched.process.once("close", () => { queueMicrotask(() => controller.abort({ requested: "abort after terminal EOF" }), ); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "app.mjs during source-transfer: terminal failure before EOF", }); } finally { await worker.close(); } }); it.each([false, true])( "admits a valid complete response with static facts before EOF cancellation (%s)", async (abortAfterTerminal) => { const controller = new AbortController(); const reason = { requested: "abort after valid terminal frame" }; const encodedStatic = JSON.stringify(staticCheckpointFacts); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const fs=require("node:fs"); const {createHash}=require("node:crypto"); const staticFacts=JSON.parse(${JSON.stringify(encodedStatic)}); let id; require("node:readline").createInterface({input:process.stdin}).on("line", line => { const request=JSON.parse(line); if(request.file){ id=request.id; const text=JSON.stringify(staticFacts); fs.writeFileSync(id+".static.json",text); process.stdout.write(JSON.stringify({kind:"static",id,bytes:Buffer.byteLength(text),sha256:createHash("sha256").update(text).digest("hex")})+"\\n"); } else { process.stdout.write(JSON.stringify({kind:"complete",id,descriptor:null})+"\\n",()=>process.exit(0)); } });`, ], }); if (abortAfterTerminal) launched.process.once("close", () => queueMicrotask(() => controller.abort(reason)), ); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(abortAfterTerminal); if (abortAfterTerminal) { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(reason); expect(result.error.javascript?.parse_status).toBe("complete"); } } else { expect(result).toMatchObject({ ok: true, value: { javascript: { parse_status: "complete" } }, }); } } finally { await worker.close(); } }, ); it.each([ { abortAfterComplete: false, reason: undefined }, { abortAfterComplete: true, reason: { requested: "after protocol failure" } }, { abortAfterComplete: true, reason: null }, ])( "keeps a complete-before-static protocol failure primary and retains later abort ($abortAfterComplete, $reason)", async ({ abortAfterComplete, reason }) => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} require("node:readline").createInterface({input:process.stdin}).once("line", line => { const {id}=JSON.parse(line); process.stdout.write(JSON.stringify({kind:"complete",id,descriptor:null})+"\\n"); setInterval(()=>{},1000); });`, ], }); if (abortAfterComplete) { const stdout = launched.process.stdout; let readySeen = false; stdout?.on("data", (chunk: Buffer | string) => { if (!readySeen && String(chunk).includes('"kind":"ready"')) { readySeen = true; stdout.on("data", (responseChunk: Buffer | string) => { if (String(responseChunk).includes('"kind":"complete"')) queueMicrotask(() => controller.abort(reason)); }); } }); } return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(abortAfterComplete); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "JavaScript worker completed before a static checkpoint", }); if (abortAfterComplete) { expect(result.error.error.secondaryFailures).toHaveLength(1); expect(result.error.error.secondaryFailures[0]).toBeInstanceOf( AnalysisCancelledError, ); expect(result.error.error.secondaryFailures[0]?.cause).toBe(reason); } else { expect(result.error.error.secondaryFailures).toHaveLength(0); } } } finally { await worker.close(); } }, ); it("includes the owned process output-loss marker in captured truncation", async () => { const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => process.exit(0));`, ], }); Object.defineProperty(launched.process, "outputIncomplete", { value: true, }); return launched; }, ); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisOutputError"); expect(result.error.error.capturedOutput).toMatchObject({ truncated: true, }); } } finally { await worker.close(); } }); it("selects a control-frame budget failure observed before caller abort", async () => { const controller = new AbortController(); let stdoutBytes = 0; let abortQueued = false; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} ${phaseAfterRequest} process.stdout.write("x".repeat(1_100_000)+"\\n", () => process.exit(7)); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { stdoutBytes += Buffer.byteLength(chunk); if (stdoutBytes > 65_536 && !abortQueued) { abortQueued = true; setTimeout(() => controller.abort("late caller abort"), 20); } }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(abortQueued).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "JavaScript worker control frame is unterminated or oversized", }); } finally { await worker.close(); } }); it("keeps abort-first cancellation when the owned worker exits during stop", async () => { const controller = new AbortController(); const reason = new AnalysisOutputError("fixture", "opaque abort value"); let exitedDuringStop = false; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.on("SIGTERM",()=>process.exit(7)); ${phaseAfterRequest} setInterval(()=>{},1000); });`, ], }); let acknowledged = false; launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes('"kind":"phase"') && !acknowledged) { acknowledged = true; setImmediate(() => controller.abort(reason)); } }); launched.process.once("exit", () => { exitedDuringStop = controller.signal.aborted; }); return { process: launched.process, ownership: launched.ownership }; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(exitedDuringStop).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(reason); expect(result.error.error.secondaryFailures).toHaveLength(0); } } finally { await worker.close(); } }); it("bounds exit-output drain when a descendant holds the inherited pipe", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const {spawn}=require("node:child_process"); spawn(process.execPath,["-e","setInterval(()=>{},1000)"],{stdio:"inherit"}); process.stdin.once("data",()=>process.exit(7));`, ], }); launched.process.once("exit", () => { setImmediate(() => controller.abort("abort after observed exit")); }); return launched; }, ); const started = performance.now(); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(result.ok).toBe(false); expect(performance.now() - started).toBeLessThan(2_000); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "ProviderAdapterError", diagnostics: { exit_code: 7 }, }); } finally { await worker.close(); } }); it("admits a final valid control frame written after the worker exit event", async () => { const frameWasWrittenAfterExit: boolean[] = []; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const response = { kind: "failure", id: "", reason: "output", resource: null, limits: null, message: "final response after child exit", }; const script = `${ready} const {spawn}=require("node:child_process"); process.stdin.once("data", chunk => { const response=${JSON.stringify(response)}; response.id=JSON.parse(String(chunk)).id; const frame=JSON.stringify(response)+"\\n"; const lateScript="setTimeout(()=>process.stdout.write("+JSON.stringify(frame)+"),50); setInterval(()=>{},1000)"; spawn(process.execPath,["-e",lateScript],{stdio:["ignore","inherit","ignore"]}); process.exit(7); });`; const launched = await spawnOwnedProviderProcess({ ...request, arguments: ["-e", script], }); let exited = false; launched.process.once("exit", () => { exited = true; }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes("final response after child exit")) frameWasWrittenAfterExit.push(exited); }); return launched; }, ); const started = performance.now(); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(frameWasWrittenAfterExit).toEqual([true]); expect(performance.now() - started).toBeLessThan(2_000); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: expect.stringContaining("final response after child exit"), }); } finally { await worker.close(); } }); it("retains the owned worker's V8 heap exhaustion classification", async () => { const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => { process.stderr.write("FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory\\n"); process.exit(7); });`, ], }), ); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisResourceConstraintError", resource: "memory", }); } finally { await worker.close(); } }); it("captures late stderr completely when the exited worker's inherited pipes close", async () => { const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const {spawn}=require("node:child_process"); process.stdin.once("data", () => { spawn(process.execPath,["-e","setTimeout(()=>{process.stderr.write('late stderr tail');process.exit(0)},80)"],{stdio:["ignore","inherit","inherit"]}); process.exit(7); });`, ], }), ); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("ProviderAdapterError"); expect(result.error.error.capturedOutput).toMatchObject({ stderr: "late stderr tail", truncated: false, }); } } finally { await worker.close(); } }); it("marks worker diagnostics incomplete when caller abort interrupts inherited-pipe drain", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const {spawn}=require("node:child_process"); process.stdin.once("data", () => { spawn(process.execPath,["-e","setTimeout(()=>{process.stderr.write('late stderr tail');process.exit(0)},1000)"],{stdio:["ignore","inherit","inherit"]}); process.exit(7); });`, ], }); launched.process.once("exit", () => { setImmediate(() => controller.abort("abort during output finalization"), ); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("ProviderAdapterError"); expect(result.error.error.capturedOutput).toMatchObject({ stderr: "", truncated: true, }); } } finally { await worker.close(); } }); it("does not classify a live worker's heap-related stderr as OOM", async () => { const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", chunk => { const id=JSON.parse(String(chunk)).id; process.stderr.write("heap limit is configured\\n"); process.stdout.write(JSON.stringify({kind:"phase",id,phase:"parse"})+"\\n"+JSON.stringify({kind:"failure",id,reason:"output",resource:null,limits:null,message:"fixture output failure"})+"\\n",()=>process.exit(0)); });`, ], }), ); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "app.mjs during parse: fixture output failure", }); } finally { await worker.close(); } }); it("keeps cancellation after successful exit while a final response is pending", async () => { const controller = new AbortController(); const reason = new AnalysisOutputError( "fixture", "abort after successful exit", ); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const {spawn}=require("node:child_process"); process.stdin.once("data", chunk => { const id=JSON.parse(String(chunk)).id; const frame=JSON.stringify({kind:"failure",id,reason:"output",resource:null,limits:null,message:"late healthy response"})+"\\n"; const lateScript="setTimeout(()=>process.stdout.write("+JSON.stringify(frame)+"),500)"; spawn(process.execPath,["-e",lateScript],{stdio:["ignore","inherit","ignore"]}); process.exit(0); });`, ], }); launched.process.once("exit", () => { setImmediate(() => controller.abort(reason)); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(reason); } } finally { await worker.close(); } }); it("latches an owned stdin error observed before caller abort", async () => { const controller = new AbortController(); const abortReason = { requested: "cancel after input failure" }; const inputFailure = new Error("fixture stdin failure"); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => setInterval(()=>{},1000));`, ], }); let injected = false; launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (!injected && String(chunk).includes('"kind":"ready"')) { injected = true; setImmediate(() => { launched.process.stdin?.emit("error", inputFailure); controller.abort(abortReason); }); } }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisOutputError"); expect(result.error.error.cause).toBe(inputFailure); } } finally { await worker.close(); } }); it("keeps a failed exit primary over a later request-stream error", async () => { const controller = new AbortController(); const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => process.exit(7));`, ], }); launched.process.once("exit", () => { queueMicrotask(() => { launched.process.stdin?.emit("error", new Error("late EPIPE")); controller.abort("late abort"); }); }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "ProviderAdapterError", diagnostics: { exit_code: 7, signal: null }, }); } finally { await worker.close(); } }); it("keeps a failed exit primary over malformed descendant control output", async () => { const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} const {spawn}=require("node:child_process"); process.stdin.once("data", () => { spawn(process.execPath,["-e","setTimeout(()=>process.stdout.write('not-json\\\\n'),50);setInterval(()=>{},1000)"],{stdio:["ignore","inherit","ignore"]}); process.exit(7); });`, ], }), ); try { const result = await worker.analyze(file, { nodes: 100, unknowns: 100 }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "ProviderAdapterError", diagnostics: { exit_code: 7 }, }); } finally { await worker.close(); } }); it("preserves a complete current-job failure parsed before caller abort", async () => { const controller = new AbortController(); let completeFrameSeen = false; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", chunk => { const id=JSON.parse(String(chunk)).id; process.stdout.write(JSON.stringify({kind:"phase",id,phase:"parse"})+"\\n"); setTimeout(() => process.stdout.write(JSON.stringify({kind:"failure",id,reason:"output",resource:null,limits:null,message:"complete current-job failure"})+"\\n"),1); setInterval(()=>{},1000); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes("complete current-job failure")) { completeFrameSeen = true; setTimeout(() => controller.abort("abort after parsed failure"), 5); } }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(completeFrameSeen).toBe(true); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.error).toMatchObject({ _tag: "AnalysisOutputError", reason: "app.mjs during parse: complete current-job failure", }); } finally { await worker.close(); } }); it("does not let a wrong-job failure frame override later caller abort", async () => { const controller = new AbortController(); let completeFrameSeen = false; const reason = { requested: "stop after unrelated frame" }; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", () => { const id="00000000-0000-4000-8000-000000000000"; process.stdout.write(JSON.stringify({kind:"failure",id,reason:"output",resource:null,limits:null,message:"unrelated job failure"})+"\\n"); setInterval(()=>{},1000); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes("unrelated job failure")) { completeFrameSeen = true; setTimeout(() => controller.abort(reason), 5); } }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(completeFrameSeen).toBe(true); expect(controller.signal.aborted).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(reason); } } finally { await worker.close(); } }); it.each([ { reason: "output" as const, resource: null, limits: null, }, { reason: "resource" as const, resource: "memory" as const, limits: { worker_heap_bytes: 1 }, }, ])( "keeps abort-first cancellation when a matching $reason frame arrives afterward", async (failure) => { const controller = new AbortController(); let failureFrameSeenAfterAbort = false; const worker = new JavaScriptAnalysisWorker( { heapMb: 128, timeoutMs: 30_000 }, async (request) => { const launched = await spawnOwnedProviderProcess({ ...request, arguments: [ "-e", `${ready} process.stdin.once("data", chunk => { const id=JSON.parse(String(chunk)).id; process.stdout.write(JSON.stringify({kind:"phase",id,phase:"parse"})+"\\n"); const failure=${JSON.stringify(failure)}; failure.kind="failure"; failure.id=id; setTimeout(() => process.stdout.write(JSON.stringify(failure)+"\\n"),20); setInterval(()=>{},1000); });`, ], }); launched.process.stdout?.on("data", (chunk: Buffer | string) => { if (String(chunk).includes('"kind":"phase"')) controller.abort(null); if ( controller.signal.aborted && String(chunk).includes('"kind":"failure"') ) failureFrameSeenAfterAbort = true; }); return launched; }, ); try { const result = await worker.analyze( file, { nodes: 100, unknowns: 100 }, controller.signal, ); expect(controller.signal.aborted).toBe(true); expect(controller.signal.reason).toBe(null); expect(failureFrameSeenAfterAbort).toBe(true); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.error._tag).toBe("AnalysisCancelledError"); expect(result.error.error.cause).toBe(null); } } finally { await worker.close(); } }, );