fix(reconstruction): preserve unknown process case coverage (#9599)

fix(reconstruction): avoid inferring unobserved process cases
This commit is contained in:
morluto authored and GitHub committed 2026-11-09 18:28:13 +08:00
1 parent f20f05536e
commit 38be5fd9f2
2 files changed
+64 -14

No files matched your search

@@ -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(
@@ -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");