diff --git a/server/src/services/finalized-workspace-recovery-migration.test.ts b/server/src/services/finalized-workspace-recovery-migration.test.ts index 3b9950ccf29..e8c967a1d77 100644 --- a/server/src/services/finalized-workspace-recovery-migration.test.ts +++ b/server/src/services/finalized-workspace-recovery-migration.test.ts @@ -179,6 +179,17 @@ describe.skipIf(!support.supported)("finalized workspace recovery preservation", await migrateRemoteWorkspace({ db, ...f, apply: true, writersStopped: true }); expect(await verifiedWorkspaceRepairReceipt(db, (await preserved(f)).run)).toBeNull(); }); + it("preserves an older finalized reference with omitted optional fields", async () => { + const f = await fixture(true); + const { resourceDisposition: _disposition, ...legacy } = f.reference; + await db.update(heartbeatRuns).set({ runnerProfileJson: { nativeWorkspaceSync: { + ...legacy, schema: "paperclip.native-workspace-sync/v1", + } } }).where(eq(heartbeatRuns.id, f.runId)); + await migrateRemoteWorkspace({ db, ...f, apply: true, writersStopped: true }); + const inspected = await inspectWorkspaceRecoverySource(db, { ...f, actionId: f.action.id }); + expect(inspected.sources).toEqual([expect.objectContaining({ preservationOnly: true, + reference: { ...legacy, schema: "paperclip.native-workspace-sync/v1", resourceDisposition: null } })]); + }); 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(); diff --git a/server/src/services/legacy-workspace-recovery-migration.ts b/server/src/services/legacy-workspace-recovery-migration.ts index 63917ddb149..5fd3426503c 100644 --- a/server/src/services/legacy-workspace-recovery-migration.ts +++ b/server/src/services/legacy-workspace-recovery-migration.ts @@ -2,7 +2,7 @@ import { isDeepStrictEqual } from "node:util"; import { and, eq, sql } from "drizzle-orm"; import { environmentLeases, executionWorkspaces, heartbeatRuns, issues, workspaceOperations, type Db } from "@paperclipai/db"; import { readRemoteWorkspaceAuthority } from "@paperclipai/shared"; -import type { NativeWorkspaceSyncReference } from "./native-runtime/native-workspace-sync.js"; +import { readNativeWorkspaceSyncReference, type NativeWorkspaceSyncReference } from "./native-runtime/native-workspace-sync.js"; export type LegacyWorkspaceRecoverySource = { runId: string; issueId: string; reference: NativeWorkspaceSyncReference | null; @@ -77,7 +77,7 @@ export async function readMigratedLegacyWorkspaceSource(db: Pick, const runMarker = run?.runnerProfileJson?.[input.reference?.state === "finalized" ? "finalizedWorkspaceRecoveryMigration" : "legacyWorkspaceRecoveryMigration"] as { workspaceId?: unknown } | undefined; if (input.reference?.state === "finalized" && runMarker == null) return null; - if (input.reference?.state === "finalized" && !isDeepStrictEqual(input.reference, run?.runnerProfileJson?.nativeWorkspaceSync)) + if (input.reference?.state === "finalized" && !isDeepStrictEqual(input.reference, readNativeWorkspaceSyncReference(run?.runnerProfileJson?.nativeWorkspaceSync))) throw new Error("legacy_workspace_migration_receipt_invalid"); const workspaceId = input.reference?.workspaceId ?? runMarker?.workspaceId; if (typeof workspaceId !== "string") return null;