Files
PaperClipAI/server/src/__tests__/native-workspace-sync.test.ts
T
DottaandPaperclip d66a2de539 Fence repository acquisition by durable physical root
Preserve empty-inventory admission and immutable recovery. Reserve managed or anonymous roots before acquisition and fence legacy recovery using its verified host binding.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-11 07:06:19 -05:00

382 lines
15 KiB
TypeScript

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,
readNativeWorkspaceSyncLocalCwd,
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);
await expect(readNativeWorkspaceSyncLocalCwd({ runId, companyId: "company", workspaceId: "workspace", reference })).resolves.toBe(workspace);
await expect(readNativeWorkspaceSyncLocalCwd({ runId, companyId: "other-company", workspaceId: "workspace", reference })).rejects.toThrow(/binding_mismatch/);
await expect(readNativeWorkspaceSyncLocalCwd({ runId, companyId: "company", workspaceId: "other-workspace", reference })).rejects.toThrow(/binding_mismatch/);
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: <TResult1 = unknown, TResult2 = never>(
onfulfilled?:
((value: unknown[]) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?:
((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
) => Promise.resolve(values).then(onfulfilled, onrejected),
};
return query;
};
let persistedLeaseMetadata: Record<string, unknown> | null = null;
const db = {
select: () =>
rows([{ runnerProfileJson: { nativeWorkspaceSync: reference } }]),
transaction: async (callback: (tx: unknown) => Promise<unknown>) =>
callback({
select: () => rows([{ metadata: { retained: true } }]),
update: () => ({
set: (value: { metadata: Record<string, unknown> }) => ({
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,
},
});
});
});