Files
PaperClipAI/server/src/services/remote-workspace-snapshot-storage.test.ts
T
DottaandPaperclip 6cafcaf89b fix(workspaces): recover unpublished archived Git uploads (#19459)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspace imports preserve existing files before the server
changes workspace authority.
> - Archived Git imports reconstruct saved files and Git state in a
private repository.
> - A failed upload can leave a pending snapshot journal with no object
in storage.
> - A later reconstruction can have different Git administrative bytes
for the same saved logical source.
> - This pull request lets that import record a new unpublished capture
after it proves that prior objects are absent.
> - The benefit is safe retry through the original import journal while
source checks and atomic publication remain in place.

## Linked Issues or Issue Description

Refs #19396. Refs #19409.

A snapshot upload can fail before object persistence. The caller then
removes its private archive. A retry reconstructs the same saved Git
workspace. If its administrative bytes differ, the snapshot owner
rejects the retry with
`remote_workspace_snapshot_operation_content_changed`. The pending
journal remains blocked even though no snapshot object or remote
authority was published.

## What Changed

- Add an optional unpublished recapture capability to snapshot storage.
Bind it to the original source operation. Existing callers keep the
exact manifest check.
- Enable this capability only for archived Git reconstruction. The
migration owner keeps the source recipe, task binding, ownership, and
publication checks.
- Verify every prior object before new capture admission. Unknown or
corrupt objects stop the retry. Reuse an available prior object,
including an object that appears while the replacement upload runs.
- Compare the observed upload journal under the snapshot row lock before
appending an attempt. Keep network I/O outside that lock. Preserve all
prior attempt metadata.
- Keep the first atomic publication as the winner. A losing uploader
verifies and returns that saved reference.
- Add real PostgreSQL, filesystem, and Git regression tests. Document
the retry behavior.

## Verification

- Baseline `70d23612dd9176cae8ee6b2b6dcf2f6eade9a42a`: the new real Git
fixture fails with
`remote_workspace_snapshot_operation_content_changed`. It fails the
first upload before persistence, removes the old source cache, and
advances unrelated remote refs. The saved task commit and restore point
stay fixed.
- Final frozen source: 20 tests pass in a fresh process. This includes
the real Git regression and all snapshot storage cases. Tests cover
prior object recovery, corrupt and unknown objects, normal caller
mismatch rejection, operation binding, stale journal admission, and late
upload publication in both orders.
- Reproduce the final focused check from `server/`:

  ```sh
node ../node_modules/vitest/vitest.mjs run --config vitest.config.ts
src/services/remote-workspace-snapshot-storage.test.ts
src/services/remote-workspace-migration.test.ts -t
'failed-upload-retry|snapshot storage' --reporter=dot
  ```

- An earlier six-suite run had 201 passing cases and one failing
late-object case. That run started before the final prior-object recheck
was added. The failing case passes in the fresh final run above. This
earlier run is not a clean full-family result for the final source.
- That run skipped 16 lifecycle cases because
`PAPERCLIP_REMOTE_WORKSPACE_LIFECYCLE_TEST_DATABASE_URL` was unset.
Migration, storage, reservation, and concurrency tests used real
disposable embedded PostgreSQL.
- `git diff --check` passes. Full repository typecheck passed before
rebasing. Full test, build, and current-head CI results are pending.

## Risks

- The optional capability permits different physical captures. The
migration owner must prove that the logical source and current ownership
are unchanged. Ordinary mutable workspace uploads keep the previous
mismatch guard.
- An old upload can complete after the last object check. The first
authenticated atomic publication wins. A late upload cannot replace its
object or snapshot reference. Every candidate remains in the retention
journal.
- Unknown storage results and journal changes fail closed. They can
require another canonical retry. This change adds no retry loop and
changes no database schema.

## Model Used

OpenAI Codex, GPT-6 family. The worker used reasoning, tool use, code
editing, and local test execution. The worker runtime did not expose an
exact model ID or context window.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 08:04:35 -06:00

290 lines
20 KiB
TypeScript

import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Readable } from "node:stream";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { applyPendingMigrations, closeRegisteredClients, createDb, executionWorkspaceSnapshots } from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../__tests__/helpers/embedded-postgres.js";
import { createLocalDiskStorageProvider } from "../storage/local-disk-provider.js";
import { createStorageService } from "../storage/service.js";
import type { StorageService } from "../storage/types.js";
import { captureRemoteWorkspaceDirectory, restoreRemoteWorkspaceArchive } from "./remote-workspace-snapshot-archive.js";
import { beginWorkspaceSnapshot, getWorkspaceSnapshot, loadWorkspaceSnapshotArchive, recoverWorkspaceSnapshotArchive, uploadWorkspaceSnapshot } from "./remote-workspace-snapshot-storage.js";
const externalDatabaseUrl = process.env.PAPERCLIP_TEST_DATABASE_URL;
if (externalDatabaseUrl && !/^remote_workspace_[a-z_]*test$/.test(new URL(externalDatabaseUrl).pathname.slice(1))) {
throw new Error("Workspace storage tests require a disposable remote_workspace_*test database");
}
const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPostgresTestSupport();
const suite = support.supported ? describe : describe.skip;
if (!support.supported) console.warn(`Remote workspace snapshot DB tests unavailable: ${support.reason}`);
suite("remote workspace snapshot storage", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>, db: ReturnType<typeof createDb>;
let root: string, storage: StorageService;
beforeAll(async () => {
if (externalDatabaseUrl) { await applyPendingMigrations(externalDatabaseUrl); db = createDb(externalDatabaseUrl); }
else { database = await startEmbeddedPostgresTestDatabase("workspace-snapshot-"); db = createDb(database.connectionString); }
}, 90_000);
afterAll(async () => { if (externalDatabaseUrl) await closeRegisteredClients(externalDatabaseUrl); else await database?.cleanup(); });
beforeEach(async () => {
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "41".repeat(32));
root = await fs.mkdtemp(path.join(os.tmpdir(), "pc-workspace-store-test-"));
storage = createStorageService(createLocalDiskStorageProvider(path.join(root, "objects")));
});
afterEach(async () => { vi.unstubAllEnvs(); await db.delete(executionWorkspaceSnapshots); await fs.rm(root, { recursive: true, force: true }); });
async function fixture() {
const companyId = randomUUID(), workspaceId = randomUUID(), operationId = randomUUID();
const cwd = path.join(root, "source"), archivePath = path.join(root, "archive.pcws"); await fs.mkdir(cwd);
await fs.writeFile(path.join(cwd, "saved.txt"), "acknowledged workspace content\n");
const captured = await captureRemoteWorkspaceDirectory({ cwd, archivePath });
const row = await beginWorkspaceSnapshot(db, { companyId, workspaceId, operationId, generation: 1, epoch: 3 });
const upload = { db, storage, snapshotId: row.id, companyId, workspaceId, archivePath, manifestDigest: captured.manifestDigest };
return { companyId, workspaceId, operationId, cwd, archivePath, captured, row, upload };
}
it("streams encrypted saved data through the real ledger and filesystem object backend", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload);
expect(ref.byteSize).toBeGreaterThan(f.captured.byteSize);
const object = await storage.getObject(f.companyId, ref.objectKey); const parts: Buffer[] = [];
for await (const part of object.stream) parts.push(part);
expect(Buffer.concat(parts).includes(Buffer.from("acknowledged workspace content"))).toBe(false);
await fs.rm(f.cwd, { recursive: true }); await fs.rm(f.archivePath);
const restoredArchive = path.join(root, "restored.pcws"), targetDir = path.join(root, "restored");
await loadWorkspaceSnapshotArchive({ storage, reference: ref, targetPath: restoredArchive });
await restoreRemoteWorkspaceArchive({ archivePath: restoredArchive, targetDir, expectedManifestDigest: ref.manifestDigest });
expect(await fs.readFile(path.join(targetDir, "saved.txt"), "utf8")).toBe("acknowledged workspace content\n");
const [row] = await db.select().from(executionWorkspaceSnapshots).where(eq(executionWorkspaceSnapshots.id, ref.id));
expect(row?.status).toBe("uploaded"); // The owning service still controls atomic publication.
});
it("reconciles an upload whose reply was lost after object persistence, without the old scratch file", async () => {
const f = await fixture(); const put = storage.putFile.bind(storage); let loseReply = true;
storage.putFile = async input => { const result = await put(input); if (loseReply) { loseReply = false; throw new Error("lost upload reply"); } return result; };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("lost upload reply");
await fs.rm(f.archivePath);
const ref = await uploadWorkspaceSnapshot(f.upload);
expect(ref.manifestDigest).toBe(f.captured.manifestDigest);
expect(await getWorkspaceSnapshot(db, { ...f, snapshotId: ref.id })).toEqual(ref);
const [row] = await db.select().from(executionWorkspaceSnapshots).where(eq(executionWorkspaceSnapshots.id, ref.id));
expect((row?.metadata?.uploads as unknown[]).length).toBe(1);
});
it("retains an unsuccessful operation and rejects a content-changing retry", async () => {
const f = await fixture(); storage.putFile = async () => { throw new Error("storage unavailable"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("storage unavailable");
const [row] = await db.select().from(executionWorkspaceSnapshots).where(eq(executionWorkspaceSnapshots.id, f.row.id));
expect(row?.status).toBe("pending"); expect((row?.metadata?.uploads as unknown[]).length).toBe(1);
await expect(uploadWorkspaceSnapshot({ ...f.upload, manifestDigest: "b".repeat(64) })).rejects.toThrow("operation_content_changed");
});
it("recaptures only an unavailable unpublished operation while retaining every attempt", async () => {
const f = await fixture(), put = storage.putFile.bind(storage);
storage.putFile = async () => { throw new Error("upload_part InternalError"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("upload_part InternalError");
const [pending] = await db.select().from(executionWorkspaceSnapshots);
const original = (pending!.metadata!.uploads as Record<string, unknown>[])[0]!;
await fs.rm(f.archivePath);
await fs.writeFile(path.join(f.cwd, "saved.txt"), "equivalent logical source, different physical capture\n");
const captured = await captureRemoteWorkspaceDirectory({ cwd: f.cwd, archivePath: f.archivePath });
const retry = { ...f.upload, manifestDigest: captured.manifestDigest, unpublishedRecapture: { sourceOperationId: f.operationId } };
await expect(uploadWorkspaceSnapshot({ ...retry, unpublishedRecapture: { sourceOperationId: randomUUID() } })).rejects.toThrow("operation_binding_changed");
storage.putFile = put;
const ref = await uploadWorkspaceSnapshot(retry);
expect(ref.manifestDigest).toBe(captured.manifestDigest);
const [saved] = await db.select().from(executionWorkspaceSnapshots);
expect(saved!.metadata!.uploads).toEqual([original, expect.objectContaining({ objectKey: ref.objectKey, manifestDigest: captured.manifestDigest })]);
// The capability does not reopen a published snapshot.
await expect(uploadWorkspaceSnapshot({ ...retry, manifestDigest: f.captured.manifestDigest })).rejects.toThrow("operation_content_changed");
});
it("prefers an available prior capture over a different recapture", async () => {
const f = await fixture(), put = storage.putFile.bind(storage);
storage.putFile = async input => { await put(input); throw new Error("lost upload reply"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("lost upload reply");
await fs.rm(f.archivePath);
const ref = await uploadWorkspaceSnapshot({ ...f.upload, manifestDigest: "b".repeat(64), unpublishedRecapture: { sourceOperationId: f.operationId } });
expect(ref.manifestDigest).toBe(f.captured.manifestDigest);
expect((await db.select().from(executionWorkspaceSnapshots))[0]!.metadata!.uploads).toHaveLength(1);
});
it("rechecks prior objects after encryption before recording a recapture", async () => {
const f = await fixture(), put = storage.putFile.bind(storage), head = storage.headObject.bind(storage);
let savedInput!: Parameters<StorageService["putFile"]>[0];
storage.putFile = async input => {
const parts: Buffer[] = [];
for await (const bytes of input.body) parts.push(Buffer.from(bytes));
savedInput = { ...input, body: Buffer.concat(parts) };
throw new Error("upload interrupted before persistence");
};
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("upload interrupted before persistence");
let heads = 0;
storage.headObject = async (...args) => { if (++heads === 2) await put(savedInput); return head(...args); };
storage.putFile = put;
const ref = await uploadWorkspaceSnapshot({ ...f.upload, manifestDigest: "b".repeat(64), unpublishedRecapture: { sourceOperationId: f.operationId } });
expect(ref.manifestDigest).toBe(f.captured.manifestDigest);
expect(heads).toBe(2);
expect((await db.select().from(executionWorkspaceSnapshots))[0]!.metadata!.uploads).toHaveLength(1);
});
it("refuses stale recapture admission when another attempt enters the ledger", async () => {
const f = await fixture(), head = storage.headObject.bind(storage);
storage.putFile = async () => { throw new Error("upload unavailable"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("upload unavailable");
let ready!: () => void, release!: () => void;
const paused = new Promise<void>(resolve => { ready = resolve; }), resume = new Promise<void>(resolve => { release = resolve; });
let heads = 0;
storage.headObject = async (...args) => { if (++heads === 1) { ready(); await resume; } return head(...args); };
const stale = uploadWorkspaceSnapshot({ ...f.upload, manifestDigest: "b".repeat(64), unpublishedRecapture: { sourceOperationId: f.operationId } });
try {
await paused;
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("upload unavailable");
release();
await expect(stale).rejects.toThrow("upload_attempts_changed");
const [saved] = await db.select().from(executionWorkspaceSnapshots);
expect(saved).toMatchObject({ status: "pending", objectKey: null });
expect(saved!.metadata!.uploads).toHaveLength(2);
} finally { release(); await Promise.allSettled([stale]); }
});
it.each(["unknown", "corrupt"])("refuses recapture when a prior object is %s", async state => {
const f = await fixture(), put = storage.putFile.bind(storage);
storage.putFile = async input => { await put(input); throw new Error("lost upload reply"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("lost upload reply");
if (state === "unknown") storage.headObject = async () => { throw new Error("HEAD unavailable"); };
else {
const [pending] = await db.select().from(executionWorkspaceSnapshots);
const [attempt] = pending!.metadata!.uploads as { objectKey: string }[];
await fs.writeFile(path.join(root, "objects", attempt!.objectKey), "corrupt object");
}
await expect(uploadWorkspaceSnapshot({ ...f.upload, manifestDigest: "b".repeat(64), unpublishedRecapture: { sourceOperationId: f.operationId } }))
.rejects.toThrow(state === "unknown" ? "HEAD unavailable" : "object_hash_mismatch");
const [saved] = await db.select().from(executionWorkspaceSnapshots);
expect(saved).toMatchObject({ status: "pending", objectKey: null });
expect(saved!.metadata!.uploads).toHaveLength(1);
});
it.each(["original", "recapture", "original-with-lost-reply"])("keeps a %s winner when another physical capture finishes late", async winner => {
const f = await fixture(), put = storage.putFile.bind(storage);
const deferred = () => {
let resolve!: () => void;
const promise = new Promise<void>(ready => { resolve = ready; });
return { resolve, promise };
};
const originalReady = deferred(), originalRelease = deferred(), recaptureReady = deferred(), recaptureRelease = deferred();
let calls = 0;
storage.putFile = async input => {
const first = ++calls === 1;
if (first) { originalReady.resolve(); await originalRelease.promise; }
else { recaptureReady.resolve(); await recaptureRelease.promise; }
const saved = await put(input);
if (first && winner === "original-with-lost-reply") throw new Error("late original upload reply lost");
return saved;
};
const unpublishedRecapture = { sourceOperationId: f.operationId };
const original = uploadWorkspaceSnapshot({ ...f.upload, unpublishedRecapture });
let recapture: ReturnType<typeof uploadWorkspaceSnapshot> | undefined;
try {
await originalReady.promise;
const archivePath = path.join(root, "recaptured.pcws");
await fs.writeFile(path.join(f.cwd, "saved.txt"), "different physical capture\n");
const captured = await captureRemoteWorkspaceDirectory({ cwd: f.cwd, archivePath });
recapture = uploadWorkspaceSnapshot({ ...f.upload, archivePath, manifestDigest: captured.manifestDigest, unpublishedRecapture });
await recaptureReady.promise;
(winner === "recapture" ? recaptureRelease : originalRelease).resolve();
if (winner === "original-with-lost-reply") {
await expect(original).rejects.toThrow("late original upload reply lost");
recaptureRelease.resolve();
}
const saved = await (winner === "original" ? original : recapture);
expect(saved.manifestDigest).toBe(winner === "recapture" ? captured.manifestDigest : f.captured.manifestDigest);
originalRelease.resolve(); recaptureRelease.resolve();
if (winner !== "original-with-lost-reply") expect(await original).toEqual(saved);
expect(await recapture).toEqual(saved);
expect(await getWorkspaceSnapshot(db, { ...f, snapshotId: f.row.id })).toEqual(saved);
expect((await db.select().from(executionWorkspaceSnapshots))[0]!.metadata!.uploads).toHaveLength(2);
} finally { originalRelease.resolve(); recaptureRelease.resolve(); await Promise.allSettled([original, recapture]); }
});
it("authenticates a lost-reply archive without publishing its pending upload", async () => {
const f = await fixture(), put = storage.putFile.bind(storage);
storage.putFile = async input => { await put(input); throw new Error("lost upload reply"); };
await expect(uploadWorkspaceSnapshot(f.upload)).rejects.toThrow("lost upload reply");
await fs.rm(f.archivePath);
const targetPath = path.join(root, "recovered.pcws");
const ref = await recoverWorkspaceSnapshotArchive({ ...f.upload, targetPath });
expect(ref?.manifestDigest).toBe(f.captured.manifestDigest);
expect((await db.select().from(executionWorkspaceSnapshots))[0]?.status).toBe("pending");
expect(await getWorkspaceSnapshot(db, { ...f, snapshotId: f.row.id })).toBeNull();
await restoreRemoteWorkspaceArchive({ archivePath: targetPath, targetDir: path.join(root, "restored"), expectedManifestDigest: ref!.manifestDigest });
expect(await fs.readFile(path.join(root, "restored", "saved.txt"), "utf8")).toBe("acknowledged workspace content\n");
await expect(recoverWorkspaceSnapshotArchive({ ...f.upload, companyId: randomUUID(), targetPath: path.join(root, "wrong-company.pcws") }))
.rejects.toThrow("remote_workspace_snapshot_missing");
});
it("keeps operation identity and company/workspace scope exact", async () => {
const f = await fixture();
expect((await beginWorkspaceSnapshot(db, { ...f, generation: 1, epoch: 3 })).id).toBe(f.row.id);
await expect(beginWorkspaceSnapshot(db, { ...f, generation: 1, epoch: 4 })).rejects.toThrow("operation_binding_changed");
await uploadWorkspaceSnapshot(f.upload);
expect(await getWorkspaceSnapshot(db, { ...f, snapshotId: f.row.id, companyId: randomUUID() })).toBeNull();
expect(await getWorkspaceSnapshot(db, { ...f, snapshotId: f.row.id, workspaceId: randomUUID() })).toBeNull();
});
it("rejects object corruption and leaves the requested plaintext path unpublished", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload);
const objectPath = path.join(root, "objects", ref.objectKey), object = await fs.open(objectPath, "r+");
try {
const byte = Buffer.alloc(1);
expect((await object.read(byte, 0, 1, 15)).bytesRead).toBe(1);
byte[0] = byte[0]! ^ 1;
await object.write(byte, 0, 1, 15);
} finally { await object.close(); }
const targetPath = path.join(root, "corrupted.pcws");
await expect(loadWorkspaceSnapshotArchive({ storage, reference: ref, targetPath })).rejects.toThrow("object_hash_mismatch");
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("rejects a foreign wrapped key even when ciphertext metadata is unchanged", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload);
const foreignId = randomUUID();
await expect(loadWorkspaceSnapshotArchive({ storage, reference: { ...ref, id: foreignId, objectKey: ref.objectKey.replace(ref.id, foreignId) },
targetPath: path.join(root, "foreign.pcws") })).rejects.toThrow("key_binding_invalid");
});
it("does not overwrite an existing caller file", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload), targetPath = path.join(root, "existing.pcws");
await fs.writeFile(targetPath, "existing");
await expect(loadWorkspaceSnapshotArchive({ storage, reference: ref, targetPath })).rejects.toMatchObject({ code: "EEXIST" });
expect(await fs.readFile(targetPath, "utf8")).toBe("existing");
});
it("does not start object retrieval for an already canceled snapshot load", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload);
const getObject = vi.spyOn(storage, "getObject");
const controller = new AbortController(), reason = new Error("request already canceled");
controller.abort(reason);
const targetPath = path.join(root, "canceled.pcws");
const error = await loadWorkspaceSnapshotArchive({ storage, reference: ref, targetPath, signal: controller.signal }).catch(error => error);
expect(getObject).not.toHaveBeenCalled();
expect(error).toBe(reason);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("cancels an active object body and removes private decryption scratch before rejecting", async () => {
const f = await fixture(), ref = await uploadWorkspaceSnapshot(f.upload);
const ciphertext = await fs.readFile(path.join(root, "objects", ref.objectKey));
const controller = new AbortController();
let produced = 0, closed = false;
const body = new Readable({
highWaterMark: 1,
read() {
if (produced === 2) { controller.abort(new Error("client disconnected")); return; }
this.push(ciphertext.subarray(produced, ++produced));
},
destroy(error, callback) { closed = true; callback(error); },
});
storage.getObject = vi.fn(async (_companyId, _objectKey, options) => {
expect(options?.signal).toBe(controller.signal);
return { stream: body, contentLength: ciphertext.length };
});
const scratchRoot = path.join(root, "private-scratch"); await fs.mkdir(scratchRoot);
const temp = vi.spyOn(os, "tmpdir").mockReturnValue(scratchRoot);
const targetPath = path.join(root, "interrupted.pcws");
try {
await expect(loadWorkspaceSnapshotArchive({ storage, reference: ref, targetPath, signal: controller.signal }))
.rejects.toMatchObject({ name: "AbortError" });
expect(closed).toBe(true); expect(body.destroyed).toBe(true);
expect(produced).toBeGreaterThan(0); expect(produced).toBeLessThan(ciphertext.length);
const atRejection = produced; await new Promise(resolve => setImmediate(resolve)); expect(produced).toBe(atRejection);
expect(await fs.readdir(scratchRoot)).toEqual([]);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally { temp.mockRestore(); }
});
});