import { mkdtemp, readdir, rm, mkdir, writeFile, readFile, appendFile, symlink, stat } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { directorySnapshotSha256, serializeDirectorySnapshot, captureDirectorySnapshot, disposeDirectorySnapshot, } from "@paperclipai/adapter-utils/workspace-restore-merge"; import { classifyNativeWorkspaceInbound, nativeWorkspaceSyncInternals, readNativeWorkspaceSyncReference, resumeNativeWorkspaceSync, prepareNativeWorkspaceSync, cleanupNativeWorkspaceSync, } from "../services/native-runtime/native-workspace-sync.js"; const digest = "a".repeat(64); describe("native workspace sync durable metadata", () => { const originalPaperclipHome = process.env.PAPERCLIP_HOME; const originalPaperclipInstanceId = process.env.PAPERCLIP_INSTANCE_ID; const cleanupDirs: string[] = []; afterEach(async () => { if (originalPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = originalPaperclipHome; if (originalPaperclipInstanceId === undefined) delete process.env.PAPERCLIP_INSTANCE_ID; else process.env.PAPERCLIP_INSTANCE_ID = originalPaperclipInstanceId; await Promise.all( cleanupDirs .splice(0) .map((directory) => rm(directory, { recursive: true, force: true })), ); }); it("classifies fresh, warm, replacement, and same-run recovery inputs", () => { expect( classifyNativeWorkspaceInbound({ kind: "new_run", acquisition: "created", hasPriorStamp: false, }), ).toBe("host_current"); expect( classifyNativeWorkspaceInbound({ kind: "new_run", acquisition: "resumed", hasPriorStamp: true, }), ).toBe("adopt_remote"); expect( classifyNativeWorkspaceInbound({ kind: "new_run", acquisition: "resumed", hasPriorStamp: false, }), ).toBe("host_current"); expect( classifyNativeWorkspaceInbound({ kind: "new_run", acquisition: "replacement", hasPriorStamp: true, }), ).toBe("host_current"); expect( classifyNativeWorkspaceInbound({ kind: "existing_run", restartRecovery: true, sameProviderLease: true, }), ).toBe("adopt_remote"); expect( classifyNativeWorkspaceInbound({ kind: "existing_run", restartRecovery: true, sameProviderLease: false, }), ).toBe("durable_seed"); expect( classifyNativeWorkspaceInbound({ kind: "existing_run", restartRecovery: false, sameRunRecovery: true, sameProviderLease: true, }), ).toBe("adopt_remote"); expect(() => classifyNativeWorkspaceInbound({ kind: "existing_run", restartRecovery: false, sameProviderLease: true, }), ).toThrow("native_workspace_sync_unexpected_existing_descriptor"); }); it("refuses a different sandbox before preparing restart workspace sync", async () => { const select = vi.fn(); await expect(prepareNativeWorkspaceSync({ db: { select } as never, runId: "run", companyId: "company", workspaceId: "workspace", workspaceLocalDir: "/tmp/local-workspace", lease: { id: "lease", providerLeaseId: "replacement" } as never, target: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "daytona", sandboxLeaseAcquisition: { outcome: "replacement", providerLeaseId: "replacement" } } as never, restartRecovery: { kind: "reattach_remote_runner", runId: "run", leaseOwner: "controller", controllerGeneration: 2, providerAttempt: 1, restartKind: "graceful", recoveryRequestId: null, remote: { providerLeaseId: "original", remoteCwd: "/workspace" } }, })).rejects.toThrow("native_remote_recovery_lease_mismatch"); expect(select).not.toHaveBeenCalled(); }); it("reads backward-compatible references and the resource disposition", () => { const base = { schema: "paperclip.native-workspace-sync/v1", state: "prepared", descriptorSha256: digest, baselineSha256: digest, finalHostSha256: null, workspaceId: "workspace-1", leaseId: "lease-1", providerLeaseId: "sandbox-1", remoteCwd: "/workspace", }; expect(readNativeWorkspaceSyncReference(base)).toEqual({ ...base, resourceDisposition: null, }); expect( readNativeWorkspaceSyncReference({ ...base, resourceDisposition: "keep_running", }), ).toEqual({ ...base, resourceDisposition: "keep_running" }); expect( readNativeWorkspaceSyncReference({ ...base, resourceDisposition: "delete_everything", }), ).toBeNull(); }); it("rejects traversal before constructing a durable state path", () => { expect(() => nativeWorkspaceSyncInternals.descriptorPath("../run", digest), ).toThrow("native_workspace_sync_invalid_run_id"); expect(() => nativeWorkspaceSyncInternals.descriptorPath("run-1", "../descriptor"), ).toThrow("native_workspace_sync_descriptor_digest_invalid"); }); it.each(["corrupt", "symlink", "foreign"] as const)("persists compact v2 manifests and rejects %s recovery storage", async (tamper) => { const paperclipHome = await mkdtemp(path.join(os.tmpdir(), "paperclip-native-manifest-")); cleanupDirs.push(paperclipHome); process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = "manifest-test"; const workspace = path.join(paperclipHome, "workspace"); await mkdir(workspace); await writeFile(path.join(workspace, "private-filename-雪"), "baseline"); const baseline = await captureDirectorySnapshot(workspace, { diskBacked: true }); const initial = serializeDirectorySnapshot(baseline); if (initial.version !== 2) throw new Error("Expected a disk baseline"); const scratch = initial.entries.filePath; const runId = "manifest-run"; const stateDir = path.dirname(nativeWorkspaceSyncInternals.durableSeedPaths(runId).workspaceArchivePath); await mkdir(stateDir, { recursive: true }); await nativeWorkspaceSyncInternals.persistSnapshotManifests(runId, { baseline, gitSnapshot: null }); await expect(stat(scratch)).rejects.toMatchObject({ code: "ENOENT" }); const serialized = serializeDirectorySnapshot(baseline); if (serialized.version !== 2) throw new Error("Expected v2"); const descriptor = { schema: "paperclip.native-workspace-sync/v2" as const, binding: { runId, companyId: "company", workspaceId: "workspace", leaseId: "lease", providerLeaseId: "sandbox", localCwd: workspace, remoteCwd: "/workspace" }, state: "prepared" as const, baselineSha256: directorySnapshotSha256(baseline), baseline: serialized, gitSnapshot: null, seed: null, createdAt: new Date().toISOString(), finalizedAt: null, finalHostSha256: null, resourceDisposition: null, }; let reference = await nativeWorkspaceSyncInternals.writeDescriptor(descriptor); const raw = await readFile(nativeWorkspaceSyncInternals.descriptorPath(runId, reference.descriptorSha256), "utf8"); expect(raw.length).toBeLessThan(4096); expect(raw).not.toContain("private-filename"); const recovered = await nativeWorkspaceSyncInternals.readDescriptor({ runId, reference }); expect(recovered.baseline.entries.get("private-filename-雪")).toMatchObject({ kind: "file" }); await disposeDirectorySnapshot(recovered.baseline); if (tamper === "corrupt") { await appendFile(serialized.entries.filePath, "corrupt"); } else if (tamper === "symlink") { const copy = path.join(paperclipHome, "copied.sqlite"); await writeFile(copy, await readFile(serialized.entries.filePath)); await rm(serialized.entries.filePath); await symlink(copy, serialized.entries.filePath); } else { descriptor.baseline.entries.filePath = path.join(paperclipHome, "foreign.sqlite"); reference = await nativeWorkspaceSyncInternals.writeDescriptor(descriptor); } await expect(nativeWorkspaceSyncInternals.readDescriptor({ runId, reference })).rejects.toThrow(); await cleanupNativeWorkspaceSync(runId); await expect(stat(stateDir)).rejects.toMatchObject({ code: "ENOENT" }); }); it.each([false, true])("writes one immutable descriptor when the same state is replayed (multiple repositories: %s)", async (multipleRepositories) => { const paperclipHome = await mkdtemp( path.join(os.tmpdir(), "paperclip-native-workspace-sync-"), ); cleanupDirs.push(paperclipHome); process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = "descriptor-test"; const baseline = { exclude: [".paperclip-runtime"], entries: new Map([ [ "continuity.txt", { kind: "file" as const, mode: 0o644, hash: digest }, ], ]), }; const descriptor = { schema: "paperclip.native-workspace-sync/v1" as const, binding: { runId: "run-idempotent", companyId: "company-1", workspaceId: "workspace-1", leaseId: "lease-1", providerLeaseId: "sandbox-1", localCwd: path.join(paperclipHome, "workspace"), remoteCwd: "/workspace", }, state: "prepared" as const, baselineSha256: directorySnapshotSha256(baseline), baseline: serializeDirectorySnapshot(baseline), gitSnapshot: multipleRepositories ? { headCommit: "a".repeat(40), branchName: "main", overlayPaths: [], deletedPaths: [], ignoredPaths: [], repositories: [{ path: ".paperclip-repositories/backend", snapshot: { headCommit: "b".repeat(40), branchName: "backend-work", overlayPaths: ["dirty.txt"], deletedPaths: [], ignoredPaths: ["secret.txt"], } }], } : null, seed: null, createdAt: "2026-01-01T00:00:00.000Z", finalizedAt: null, finalHostSha256: null, resourceDisposition: "keep_running" as const, }; const first = await nativeWorkspaceSyncInternals.writeDescriptor(descriptor); const second = await nativeWorkspaceSyncInternals.writeDescriptor(descriptor); expect(second).toEqual(first); const files = await readdir( path.dirname( nativeWorkspaceSyncInternals.descriptorPath( descriptor.binding.runId, first.descriptorSha256, ), ), ); expect(files.filter((file) => file.endsWith(".json"))).toEqual([ `descriptor.${first.descriptorSha256}.json`, ]); await expect( nativeWorkspaceSyncInternals.readDescriptor({ runId: descriptor.binding.runId, reference: first, }), ).resolves.toMatchObject({ descriptor }); }); it("repairs finalized remote and lease stamps after an interrupted commit", async () => { const paperclipHome = await mkdtemp( path.join(os.tmpdir(), "paperclip-native-workspace-sync-repair-"), ); cleanupDirs.push(paperclipHome); process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = "descriptor-repair-test"; const baseline = { exclude: [".paperclip-runtime"], entries: new Map([ [ "continuity.txt", { kind: "file" as const, mode: 0o644, hash: digest }, ], ]), }; const finalHostSha256 = "b".repeat(64); const descriptor = { schema: "paperclip.native-workspace-sync/v1" as const, binding: { runId: "run-finalized-repair", companyId: "company-1", workspaceId: "workspace-1", leaseId: "lease-1", providerLeaseId: "sandbox-1", localCwd: path.join(paperclipHome, "workspace"), remoteCwd: "/workspace", }, state: "finalized" as const, baselineSha256: directorySnapshotSha256(baseline), baseline: serializeDirectorySnapshot(baseline), gitSnapshot: null, seed: null, createdAt: "2026-01-01T00:00:00.000Z", finalizedAt: "2026-01-01T00:01:00.000Z", finalHostSha256, resourceDisposition: "keep_running" as const, }; const reference = await nativeWorkspaceSyncInternals.writeDescriptor(descriptor); const rows = (values: unknown[]) => { const query = { from: () => query, where: () => query, for: () => query, limit: () => query, then: ( onfulfilled?: ((value: unknown[]) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ) => Promise.resolve(values).then(onfulfilled, onrejected), }; return query; }; let persistedLeaseMetadata: Record | null = null; const db = { select: () => rows([{ runnerProfileJson: { nativeWorkspaceSync: reference } }]), transaction: async (callback: (tx: unknown) => Promise) => callback({ select: () => rows([{ metadata: { retained: true } }]), update: () => ({ set: (value: { metadata: Record }) => ({ where: async () => { persistedLeaseMetadata = value.metadata; }, }), }), }), }; const execute = vi .fn() .mockResolvedValueOnce({ timedOut: false, exitCode: 1, stdout: "" }) .mockResolvedValueOnce({ timedOut: false, exitCode: 0, stdout: "" }); await expect( resumeNativeWorkspaceSync({ db: db as never, runId: descriptor.binding.runId, target: { kind: "remote", transport: "sandbox", remoteCwd: descriptor.binding.remoteCwd, sandboxLeaseAcquisition: { providerLeaseId: descriptor.binding.providerLeaseId, }, runner: { execute }, } as never, }), ).resolves.toBe(true); expect(execute).toHaveBeenCalledTimes(2); expect(persistedLeaseMetadata).toMatchObject({ retained: true, nativeWorkspaceSync: { schema: "paperclip.native-workspace-stamp/v1", workspaceId: descriptor.binding.workspaceId, providerLeaseId: descriptor.binding.providerLeaseId, remoteCwd: descriptor.binding.remoteCwd, hostSha256: finalHostSha256, finalizedRunId: descriptor.binding.runId, }, }); }); });