diff --git a/src/browser/history/CaptureFailures.test.ts b/src/browser/history/CaptureFailures.test.ts index 71026ed6cf..a3df176485 100644 --- a/src/browser/history/CaptureFailures.test.ts +++ b/src/browser/history/CaptureFailures.test.ts @@ -7,6 +7,83 @@ import { historicalCaptureFailure } from "./CaptureFailures.js"; import { redactExplicitFailure } from "../../domain/explicitSensitiveFailure.js"; import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; +it.each([ + { label: "POSIX SIGABRT", exitCode: null, signal: "SIGABRT" }, + { label: "Windows exit 134", exitCode: 134, signal: null }, +] as const)( + "reports HAR heap exhaustion for $label", + ({ exitCode, signal }) => { + const input = inspectWebNetworkCaptureInputSchema.parse({ + capture_path: "/selected/input.har", + format: "har", + }); + const stderr = + "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory\r\n"; + const cause = new OwnedCommandFailure("process", "Decoder failed", { + stdout: { text: "", bytes: 0 }, + stderr: { text: stderr, bytes: Buffer.byteLength(stderr) }, + exitCode, + signal, + }); + const projected = analysisErrorProjectionSchema.parse( + projectAnalysisError(historicalCaptureFailure(input, cause, "decoder")), + ); + expect(projected).toMatchObject({ + code: "resource_constraint", + retryable: false, + message: expect.stringContaining("fixed V8 heap"), + remediation: { + action: expect.stringContaining( + "smaller capture exported by its producer", + ), + }, + details: { + resource: "memory", + reported_limits: { + observed_exit_code: exitCode, + observed_signal: signal, + }, + captured_output: { stdout: "", stderr }, + }, + }); + expect(JSON.stringify(projected)).not.toContain("rea doctor"); + }, +); + +it.each([ + { exitCode: 134, signal: null, stderr: "Fatal decoder crash" }, + { + exitCode: 2, + signal: null, + stderr: "FATAL ERROR: JavaScript heap out of memory", + }, + { + exitCode: null, + signal: "SIGTERM", + stderr: "FATAL ERROR: JavaScript heap out of memory", + }, +] as const)( + "preserves unrelated decoder failures %#", + ({ exitCode, signal, stderr }) => { + const input = inspectWebNetworkCaptureInputSchema.parse({ + capture_path: "/selected/input.har", + format: "har", + }); + const cause = new OwnedCommandFailure("process", "Decoder failed", { + stdout: { text: "", bytes: 0 }, + stderr: { text: stderr, bytes: Buffer.byteLength(stderr) }, + exitCode, + signal, + }); + expect( + projectAnalysisError(historicalCaptureFailure(input, cause, "decoder")), + ).toMatchObject({ + code: "execution_failure", + details: { diagnostics: { exit_code: exitCode, signal, stderr } }, + }); + }, +); + it.each(["before open", "during read"])( "preserves observed capture changes %s as retryable acquisition integrity failures", (stage) => { diff --git a/src/browser/history/CaptureFailures.ts b/src/browser/history/CaptureFailures.ts index 14b9858e71..13a9a781c5 100644 --- a/src/browser/history/CaptureFailures.ts +++ b/src/browser/history/CaptureFailures.ts @@ -61,7 +61,8 @@ export const historicalCaptureFailure = ( phase === "decoder" && input.format === "har" && cause.reason === "process" && - cause.snapshot?.signal === "SIGABRT" && + (cause.snapshot?.signal === "SIGABRT" || + (cause.snapshot?.signal === null && cause.snapshot.exitCode === 134)) && /FATAL ERROR:[^\r\n]*heap out of memory/u.test(cause.snapshot.stderr.text) ) return captureMemoryFailure( @@ -182,7 +183,10 @@ export const captureMemoryFailure = ( } : {}), ...(cause instanceof OwnedCommandFailure - ? { observed_signal: cause.snapshot?.signal ?? null } + ? { + observed_exit_code: cause.snapshot?.exitCode ?? null, + observed_signal: cause.snapshot?.signal ?? null, + } : {}), }, { diff --git a/tests/acceptance/tools/historicalCaptureResources.test.ts b/tests/acceptance/tools/historicalCaptureResources.test.ts index b0e07f353d..1a623aff60 100644 --- a/tests/acceptance/tools/historicalCaptureResources.test.ts +++ b/tests/acceptance/tools/historicalCaptureResources.test.ts @@ -70,7 +70,8 @@ it("reports real HAR heap exhaustion through CLI and MCP and preserves server us resource: "memory", reported_limits: { old_generation_heap_mib: 192, - observed_signal: "SIGABRT", + observed_exit_code: process.platform === "win32" ? 134 : null, + observed_signal: process.platform === "win32" ? null : "SIGABRT", }, captured_output: { stderr: expect.stringContaining("heap out of memory"),