mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.
Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
382 lines
15 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
});
|
|
});
|