mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
71 lines
6.4 KiB
TypeScript
71 lines
6.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { runnerMatrix } from "./catalog.js";
|
|
import { FINALIZATION_PAYLOAD_BYTES, finalizationResultDigest, parseFinalizationProof, gradeFinalizationRecovery, type FinalizationRecoveryObservation } from "./finalization-recovery.js";
|
|
|
|
function fixture(): FinalizationRecoveryObservation {
|
|
const binding = { companyId: "company", issueId: "task", runId: "run" }, observedAt = "2026-10-21T12:00:00.000Z";
|
|
return { binding,
|
|
before: { resultDigest: "a".repeat(64), acceptedResults: 1, providerTurns: 1, operationIds: ["unfinished"], artifactId: "original", artifactSha256: "b".repeat(64) },
|
|
crash: { ...binding, nativePhase: "workspace_finalizing", observedAt },
|
|
final: { runIds: ["run"], committed: true, issueDone: true, resultDigest: "a".repeat(64), acceptedResults: 1, providerTurns: 1,
|
|
resumedOperations: [{ resumedFromOperationId: "unfinished", startedAt: "2026-10-21T12:00:01.000Z" }], artifactId: "original", artifactSha256: "b".repeat(64),
|
|
byteSize: FINALIZATION_PAYLOAD_BYTES, actualSha256: "c".repeat(64), expectedSha256: "c".repeat(64), proofReference: "d".repeat(64),
|
|
finalDeliveryCount: 1, deliveryReceiptMatches: true, visibleDelivery: true, pendingInteractions: 0 } };
|
|
}
|
|
const passes = (value: FinalizationRecoveryObservation) => gradeFinalizationRecovery(value).every(check => check.passed);
|
|
|
|
describe("finalization restart without provider replay", () => {
|
|
it("accepts only the unchanged result and artifact with real interrupted-operation recovery", () => expect(passes(fixture())).toBe(true));
|
|
it.each([
|
|
["no saved result", (v: FinalizationRecoveryObservation) => { v.before = null; }],
|
|
["no crash receipt", (v: FinalizationRecoveryObservation) => { v.crash = null; }],
|
|
["foreign crash company", (v: FinalizationRecoveryObservation) => { v.crash!.companyId = "foreign"; }],
|
|
["foreign crash task", (v: FinalizationRecoveryObservation) => { v.crash!.issueId = "foreign"; }],
|
|
["foreign crash run", (v: FinalizationRecoveryObservation) => { v.crash!.runId = "foreign"; }],
|
|
["crash during provider work", (v: FinalizationRecoveryObservation) => { v.crash!.nativePhase = "running"; }],
|
|
["missing operation boundary", (v: FinalizationRecoveryObservation) => { v.before!.operationIds = []; }],
|
|
["completed before crash", (v: FinalizationRecoveryObservation) => { v.final!.resumedOperations = []; }],
|
|
["unrelated recovered operation", (v: FinalizationRecoveryObservation) => { v.final!.resumedOperations[0]!.resumedFromOperationId = "foreign"; }],
|
|
["recovery predates crash", (v: FinalizationRecoveryObservation) => { v.final!.resumedOperations[0]!.startedAt = "2026-10-21T11:00:00.000Z"; }],
|
|
["invalid crash clock", (v: FinalizationRecoveryObservation) => { v.crash!.observedAt = "unknown"; }],
|
|
["uncommitted result", (v: FinalizationRecoveryObservation) => { v.final!.committed = false; }],
|
|
["unfinished task", (v: FinalizationRecoveryObservation) => { v.final!.issueDone = false; }],
|
|
["new run", (v: FinalizationRecoveryObservation) => { v.final!.runIds.push("retry"); }],
|
|
["substituted run", (v: FinalizationRecoveryObservation) => { v.final!.runIds = ["retry"]; }],
|
|
["changed saved result", (v: FinalizationRecoveryObservation) => { v.final!.resultDigest = "e".repeat(64); }],
|
|
["provider turn replay", (v: FinalizationRecoveryObservation) => { v.final!.providerTurns++; }],
|
|
["result accepted again", (v: FinalizationRecoveryObservation) => { v.final!.acceptedResults++; }],
|
|
["substituted artifact", (v: FinalizationRecoveryObservation) => { v.final!.artifactId = "new"; }],
|
|
["changed artifact bytes", (v: FinalizationRecoveryObservation) => { v.final!.artifactSha256 = "e".repeat(64); }],
|
|
["incomplete workspace copy", (v: FinalizationRecoveryObservation) => { v.final!.byteSize--; }],
|
|
["changed workspace bytes", (v: FinalizationRecoveryObservation) => { v.final!.actualSha256 = "e".repeat(64); }],
|
|
["missing original reference", (v: FinalizationRecoveryObservation) => { v.final!.proofReference = ""; }],
|
|
["duplicate final reply", (v: FinalizationRecoveryObservation) => { v.final!.finalDeliveryCount = 2; }],
|
|
["wrong delivery receipt", (v: FinalizationRecoveryObservation) => { v.final!.deliveryReceiptMatches = false; }],
|
|
["hidden result", (v: FinalizationRecoveryObservation) => { v.final!.visibleDelivery = false; }],
|
|
["unanswered interaction", (v: FinalizationRecoveryObservation) => { v.final!.pendingInteractions = 1; }],
|
|
] as const)("rejects %s", (_name, change) => { const value = fixture(); change(value); expect(passes(value)).toBe(false); });
|
|
it("hashes JSON results independent of object-key order without normalizing changed content", () => {
|
|
expect(finalizationResultDigest({ summary: "ready", continuation: { kind: "same_agent", key: "one" } }))
|
|
.toBe(finalizationResultDigest({ continuation: { key: "one", kind: "same_agent" }, summary: "ready" }));
|
|
expect(finalizationResultDigest({ summary: "ready" })).not.toBe(finalizationResultDigest({ summary: "changed" }));
|
|
});
|
|
it("retains only the exact bounded synthetic proof and excludes unexpected fields from diagnostics", () => {
|
|
const proof = { reference: "a".repeat(64), byteSize: FINALIZATION_PAYLOAD_BYTES, sha256: "b".repeat(64) };
|
|
expect(parseFinalizationProof(proof)).toEqual(proof);
|
|
for (const value of [null, [], {}, { ...proof, byteSize: 1 }, { ...proof, reference: "unknown" }, { ...proof, sha256: "unknown" },
|
|
{ ...proof, "sensitive-field-name": "sensitive-field-value" }]) {
|
|
expect(() => parseFinalizationProof(value)).toThrow("Original proof does not match the requested file contract");
|
|
try { parseFinalizationProof(value); } catch (error) { expect(String(error)).not.toContain("sensitive-field"); }
|
|
}
|
|
});
|
|
it("registers only two explicit remote development cells and requires a real fault", () => {
|
|
const cells = runnerMatrix.filter(cell => cell.suite.id === "finalization-recovery");
|
|
expect(cells).toHaveLength(2);
|
|
expect(new Set(cells.map(cell => cell.profile.model))).toEqual(new Set(["gpt-6.1-sol", "claude-opus-5-5"]));
|
|
expect(cells.every(cell => cell.environment.id === "daytona" && cell.suite.manualOnly && cell.suite.groups.length === 0 &&
|
|
cell.suite.definitionMetadata?.stagingQualification === false && cell.suite.definitionMetadata?.unexercisedFaultIsFailure === true &&
|
|
cell.task.automaticRetryPolicy === "single_attempt")).toBe(true);
|
|
});
|
|
});
|