diff --git a/src/application/ReconstructionObligationCandidates.ts b/src/application/ReconstructionObligationCandidates.ts index c08ce4bb59..3732a99dc5 100644 --- a/src/application/ReconstructionObligationCandidates.ts +++ b/src/application/ReconstructionObligationCandidates.ts @@ -202,25 +202,25 @@ const deriveProcessCandidate = ( } const { record: evidence, capture } = parsed.value; const observedCases: ReconstructionObligation["observed_cases"] = [ - { - kind: capture.exit.code === 0 ? "positive" : "negative", - evidence_id: evidence.evidence_id, - location: "/normalized_result/exit", - }, + ...(capture.exit.reason !== "exited" || + (capture.exit.signal !== null && capture.exit.signal !== 0) || + capture.exit.code === null + ? [] + : [ + { + kind: + capture.exit.code === 0 + ? ("positive" as const) + : ("negative" as const), + evidence_id: evidence.evidence_id, + location: "/normalized_result/exit", + }, + ]), { kind: "teardown", evidence_id: evidence.evidence_id, location: "/normalized_result/settlement", }, - ...(capture.exit.signal !== null || capture.exit.reason !== "exited" - ? [ - { - kind: "cancellation" as const, - evidence_id: evidence.evidence_id, - location: "/normalized_result/exit", - }, - ] - : []), ]; if (capture.manifest.executable_sha256 === null) { limitations.add( diff --git a/src/application/ReconstructionObligationLedger.closure.test.ts b/src/application/ReconstructionObligationLedger.closure.test.ts index d06b20de14..ec7c1d3e47 100644 --- a/src/application/ReconstructionObligationLedger.closure.test.ts +++ b/src/application/ReconstructionObligationLedger.closure.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; +import { processCaptureSchema } from "../domain/process/processCapture.js"; +import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js"; import { build, @@ -167,6 +169,54 @@ describe("reconstruction obligation ledger Evidence provenance", () => { }); describe("reconstruction obligation ledger case closure", () => { + it("does not turn a timeout with unknown exit code into negative or cancellation cases", () => { + const capture = processEvidence( + processCaptureSchema.parse(FINALIZED_PROCESS_CAPTURE_EXAMPLE), + ); + const ledger = build(request([capture])); + expect( + ledger.obligations[0]?.observed_cases.map(({ kind }) => kind), + ).toEqual(["teardown"]); + expect(ledger.obligations[0]?.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "missing-original-case", + detail: expect.stringContaining("negative"), + }), + expect.objectContaining({ + code: "missing-original-case", + detail: expect.stringContaining("cancellation"), + }), + ]), + ); + }); + + it("distinguishes signal termination from a known nonzero exit", () => { + const signaled = processEvidence({ + exit: { code: null, signal: 15, reason: "exited" }, + }); + const nonzero = processEvidence({ + exit: { code: 2, signal: null, reason: "exited" }, + }); + const ambiguous = processEvidence({ + exit: { code: 2, signal: 15, reason: "exited" }, + }); + expect( + build(request([signaled])).obligations[0]?.observed_cases.map( + ({ kind }) => kind, + ), + ).toEqual(["teardown"]); + expect( + build(request([nonzero])).obligations[0]?.observed_cases.map( + ({ kind }) => kind, + ), + ).toEqual(["negative", "teardown"]); + expect( + build(request([ambiguous])).obligations[0]?.observed_cases.map( + ({ kind }) => kind, + ), + ).toEqual(["teardown"]); + }); it("keeps unobserved original cases open", () => { const capture = processEvidence(); const proof = proofEvidence("packaged-original-gap");