fix(recovery): require complete repair for migrated file allocations

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-04 22:02:43 -06:00
1 parent af702fe2f7
commit c569ab3ecb
2 files changed
+18 -1

No files matched your search

@@ -162,6 +162,23 @@ describe.skipIf(!support.supported)("finalized workspace recovery preservation",
await migrateRemoteWorkspace({ db, ...f, apply: true, writersStopped: true });
expect(await verifiedWorkspaceRepairReceipt(db, (await preserved(f)).run)).toBe(operationId);
});
it("does not treat one old copyback as repair of several declared allocations", async () => {
const f = await fixture(true), otherLeaseId = randomUUID();
const [lease] = await db.select().from(environmentLeases).where(eq(environmentLeases.id, f.reference.leaseId));
await db.insert(environmentLeases).values({ ...lease, id: otherLeaseId, providerLeaseId: randomUUID() });
await db.update(issueRecoveryActions).set({ evidence: { ...f.action.evidence,
systemRecovery: { ...createSystemRecovery(f.runId), state: "complete" },
workspaceRecovery: { ...(f.action.evidence.workspaceRecovery as object), status: "recovered" },
historicalWorkspaceSource: { schema: "paperclip.historical-workspace-source.v1", runId: f.runId,
leaseIds: [f.reference.leaseId, otherLeaseId] },
} }).where(eq(issueRecoveryActions.id, f.action.id));
await db.insert(workspaceOperations).values({ companyId: f.companyId, issueId: f.issueId,
heartbeatRunId: f.runId, phase: "workspace_recovery", status: "succeeded", metadata: {
recoveryActionId: f.action.id, leaseId: f.reference.leaseId, providerLeaseId: f.reference.providerLeaseId,
descriptorSha256: f.reference.descriptorSha256, finalHostSha256: f.reference.finalHostSha256 } });
await migrateRemoteWorkspace({ db, ...f, apply: true, writersStopped: true });
expect(await verifiedWorkspaceRepairReceipt(db, (await preserved(f)).run)).toBeNull();
});
it("requires the exact receipt and leaves ordinary finalized references alone", async () => {
const f = await fixture(true);
expect(await readMigratedLegacyWorkspaceSource(db, { ...f, reference: f.reference })).toBeNull();
@@ -108,7 +108,7 @@ export async function verifiedWorkspaceRepairReceipt(db: Db, run: Pick<typeof he
if (receiptId) return receiptId;
// A completed legacy copyback is still a valid receipt after its host
// files move. Migration must not erase that independent recovery proof.
if (reference?.state !== "finalized") return null;
if (reference?.state !== "finalized" || allocations.length !== 1 || allocations[0].leaseId !== reference.leaseId) return null;
}
}
if (typeof issueId !== "string" || (!remote && (!reference || reference.state !== "finalized" || !reference.finalHostSha256))) return null;