Files
PaperClipAI/packages/adapter-utils/src/workspace-restore-diagnostics.test.ts
T
Devin FoleyandClaude Opus 5.5 7f5d0793bd fix(daytona): record a bounded errno when a sandbox command fails (#19297)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Daytona sandbox provider copies the agent's workspace back to
the host after each turn (syncOut)
> - When the sandbox `tar` fails in the `archive_create` step, the
restore diagnostic records only `exitCode: 2` and `errorCode: "unknown"`
> - The command output is not sent across the plugin RPC boundary, so an
operator cannot tell a full disk from a permission error or a path that
is too long
> - This pull request maps the system error text at the end of a failed
sandbox command's output line to the existing allowlisted `errorCode`
> - The benefit is that the next failure records the real errno, and the
output text stays private

## Linked Issues or Issue Description

Refs #18172
Refs #18212

**What happened?**
A hosted Daytona run failed its workspace restore. The `archive_create`
step exited 2 on all three attempts. The run log recorded
`{"step":"workspace_transfer","transferStep":"archive_create","transferFailureKind":"command_failed","exitCode":2,"errorCode":"unknown"}`.
Two causes are possible. The sandbox disk was full, or a path was longer
than PATH_MAX, for example from deeply nested GitHub helper config
directories before #18212. The diagnostic cannot tell these causes
apart.

**Expected behavior**
A failed sandbox command records a bounded errno such as `ENOSPC`,
`EACCES` or `ENAMETOOLONG` when its output shows one. The output text
itself must not be recorded.

**Steps to reproduce**
1. Run `performSyncOut` against a sandbox whose `tar -c` exits 2 with
`tar: ./x: Cannot open: Permission denied`.
2. Read the environment sync diagnostic from the thrown error.
3. See that `errorCode` is `unknown`.

**Paperclip version or commit**
`master` at 3fca8a1.

## What Changed

- `packages/plugins/sandbox-providers/daytona/src/file-sync.ts`:
`assertSandboxCommandOkWithOutput` maps the strerror suffix of a failed
command's output to an allowlisted errno. It reads only the last 16 KiB
in memory. A line must end with `: <message>`. Fatal causes are checked
first (ENOSPC, EIO, ENAMETOOLONG, EACCES, EPERM, ENOTDIR, EISDIR,
ENOENT), so a warning about a vanished file does not hide the error that
stopped `tar`. The error message, capacity fallback (#18172), restore
and retain-on-failure behavior do not change.
- `packages/plugins/sdk/src/environment-sync-error.ts` and
`packages/adapter-utils/src/workspace-restore-diagnostics.ts`: add
`ENAMETOOLONG` to both allowlists. Both sides still validate
independently.
- Tests: 7 new Daytona cases, plus one allowlist case each in the SDK
and adapter-utils.

## Verification

- `vitest run --config
packages/plugins/sandbox-providers/daytona/vitest.config.ts
packages/plugins/sandbox-providers/daytona/src/file-sync-diagnostics.test.ts`:
18 passed. Without the `file-sync.ts` change, the 7 new cases fail with
`errorCode: "unknown"`.
- `packages/plugins/sdk`: `vitest run
tests/environment-sync-error.test.ts`: 20 passed.
- `packages/adapter-utils`: `vitest run
src/workspace-restore-diagnostics.test.ts`: 50 passed.
- `tsc --noEmit` passes for the SDK and adapter-utils. `git diff
--check` is clean.
- I did not run the Daytona package typecheck or its full suite locally
because the sandbox disk was full. CI covers them.

## Risks

- Low risk. The change adds a diagnostic field only. Control flow, error
messages and the capacity fallback regex do not change.
- Localized `tar` output does not match, so it stays `unknown`. That is
the current behavior.
- `ENAMETOOLONG` is a new allowlisted code on both sides. It is a fixed
string and carries no path or output text.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in an agent harness.

## 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
- [ ] 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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:34:41 -08:00

337 lines
20 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { classifyWorkspaceRestoreFailure } from "./workspace-restore-merge.js";
import {
withWorkspaceRestoreDiagnosticCapture, getWorkspaceRestoreDiagnostic, preserveWorkspaceRestoreErrorDiagnostic, recordWorkspaceRestoreDiagnostic,
sanitizeWorkspaceRestoreDiagnostic, withWorkspaceRestoreDiagnostics, withWorkspaceRestoreStep, withWorkspaceRestoreGitCommand,
type WorkspaceRestoreDiagnostic,
} from "./workspace-restore-diagnostics.js";
describe("workspace restore diagnostics", () => {
it.each([new Error("shared nested failure"), "shared primitive failure"])("selects an unlabelled nested failure without retaining a later task's step (%s)", async (error) => {
const sink = vi.fn();
const snapshots: Array<WorkspaceRestoreDiagnostic | undefined> = [];
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => {
await Promise.allSettled([
withWorkspaceRestoreDiagnostics("workspace", async () => { throw error; }, sink, (value) => { snapshots[0] = value; }),
withWorkspaceRestoreDiagnostics("asset", () => withWorkspaceRestoreStep("asset_restore", async () => { throw error; }), sink,
(value) => { snapshots[1] = value; }),
]);
recordWorkspaceRestoreDiagnostic(error, snapshots[0]);
throw error;
}, sink)).rejects.toBe(error);
expect(snapshots[1]?.step).toBe("asset_restore");
expect(getWorkspaceRestoreDiagnostic(error)).toEqual(typeof error === "object" ? { phase: "workspace", errorCode: "unknown" } : undefined);
expect(sink).toHaveBeenCalledExactlyOnceWith('[paperclip] Workspace restore diagnostic: {"phase":"workspace","errorCode":"unknown"}\n');
});
it("keeps logging when a diagnostic consumer fails", async () => {
const error = new Error("restore failed");
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => { throw error; }, sink,
() => { throw new Error("consumer failed"); })).rejects.toBe(error);
expect(sink).toHaveBeenCalledTimes(1);
});
it("retains a nested step and bounded Git fields across an existing wrapper without a cause", async () => {
const source = Object.assign(new Error("private command/path"), { code: 1, stderr: "private stderr" });
const wrapper = new Error("existing wrapper");
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("directory_merge", async () => {
try {
await withWorkspaceRestoreStep("git_integration", async () => { throw source; });
} catch (error) { throw preserveWorkspaceRestoreErrorDiagnostic(wrapper, error); }
}))).rejects.toBe(wrapper);
expect(wrapper).not.toHaveProperty("cause");
expect(Object.keys(wrapper)).toEqual([]);
expect(getWorkspaceRestoreDiagnostic(wrapper)).toEqual({ phase: "workspace", step: "git_integration", errorCode: "unknown", exitCode: 1 });
});
it("attributes a reused error to the later failed retry step", async () => {
const error = new Error("same failure");
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => {
await withWorkspaceRestoreStep("git_import", async () => { throw error; }).catch(() => {});
await withWorkspaceRestoreStep("git_export", async () => { throw error; });
})).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)?.step).toBe("git_export");
await expect(withWorkspaceRestoreDiagnostics("asset", async () => { throw error; })).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toEqual({ phase: "asset", errorCode: "unknown" });
});
it("isolates concurrent steps even when their errors share identity", async () => {
const error = new Error("shared error");
const sink = vi.fn();
await Promise.allSettled([
withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_import", async () => { throw error; }), sink),
withWorkspaceRestoreDiagnostics("asset", () => withWorkspaceRestoreStep("asset_restore", async () => { throw error; }), sink),
]);
expect(sink.mock.calls.map(([line]) => JSON.parse(line.split(": ")[1]))).toEqual(expect.arrayContaining([
{ phase: "workspace", step: "git_import", errorCode: "unknown" },
{ phase: "asset", step: "asset_restore", errorCode: "unknown" },
]));
});
it("starts a fresh scope for later work inherited from a completed async context", async () => {
const error = new Error("later failure");
let release!: () => void;
const gate = new Promise<void>((resolve) => { release = resolve; });
let later!: Promise<unknown>;
const sink = vi.fn();
await withWorkspaceRestoreDiagnostics("workspace", async () => {
later = gate.then(() => withWorkspaceRestoreDiagnostics("asset", () =>
withWorkspaceRestoreStep("asset_restore", async () => { throw error; }), sink)).catch((value) => value);
}, sink);
release();
expect(await later).toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toEqual({ phase: "asset", step: "asset_restore", errorCode: "unknown" });
expect(sink).toHaveBeenCalledTimes(1);
});
it("leaves successful restores silent", async () => {
const sink = vi.fn();
expect(await withWorkspaceRestoreDiagnostics("workspace", async () => 42, sink)).toBe(42);
expect(sink).not.toHaveBeenCalled();
});
it("records bounded cause fields without copying messages or arbitrary codes", async () => {
const sink = vi.fn();
const error = Object.assign(new Error("private path and credential"), {
code: "private-code", url: "https://private.invalid/secret", body: "private body",
cause: Object.assign(new Error("private cause"), { code: "ECONNRESET", statusCode: 503, exitCode: 7 }),
});
await expect(withWorkspaceRestoreDiagnostics("asset", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink).toHaveBeenCalledExactlyOnceWith(
'[paperclip] Workspace restore diagnostic: {"phase":"asset","errorCode":"ECONNRESET","httpStatus":503,"exitCode":7}\n',
);
});
it.each([null, "private string", { code: "token", status: "401", exitCode: Infinity }, { status: 200, exitCode: -1 }])(
"omits unrecognized diagnostic values (%j)", async (error) => {
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("asset", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink).toHaveBeenCalledExactlyOnceWith(
'[paperclip] Workspace restore diagnostic: {"phase":"asset","errorCode":"unknown"}\n',
);
},
);
it("keeps an allowlisted ENAMETOOLONG code", async () => {
const sink = vi.fn();
const error = Object.assign(new Error("private"), { code: "ENAMETOOLONG" });
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink.mock.calls[0]?.[0]).toContain('"errorCode":"ENAMETOOLONG"');
});
it("bounds cause traversal even when it cycles", async () => {
const error = Object.assign(new Error("private"), { cause: null as unknown, code: "ENOENT" });
error.cause = error;
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("asset", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink.mock.calls[0]?.[0]).toContain('"errorCode":"ENOENT"');
});
it.each(["EACCES", "WORKSPACE_RESTORE_UNSAFE_ARCHIVE"])("preserves %s when logging fails", async (code) => {
const error = Object.assign(new Error("private"), { code });
const classification = classifyWorkspaceRestoreFailure(error);
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => { throw error; }, async () => { throw new Error("sink failed"); }))
.rejects.toBe(error);
expect(classifyWorkspaceRestoreFailure(error)).toBe(classification);
});
it("keeps the restore error even if a provider diagnostic getter throws", async () => {
const error = Object.defineProperty(Object.assign(new Error("original"), {
statusCode: 503, cause: { code: "ECONNRESET" },
}), "code", { get() { throw new Error("getter failed"); } });
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("asset", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink).toHaveBeenCalledExactlyOnceWith(
'[paperclip] Workspace restore diagnostic: {"phase":"asset","errorCode":"ECONNRESET","httpStatus":503}\n',
);
});
it("uses valid fallback statuses when preferred fields are invalid", async () => {
const error = { status: "failed", statusCode: 503, exitCode: "failed", code: 7 };
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => { throw error; }, sink)).rejects.toBe(error);
expect(sink).toHaveBeenCalledExactlyOnceWith(
'[paperclip] Workspace restore diagnostic: {"phase":"workspace","errorCode":"unknown","httpStatus":503,"exitCode":7}\n',
);
});
it("logs nested failures once while preserving independent concurrent and later diagnostics", async () => {
const error = Object.assign(new Error("nested failure"), { code: "ENOENT" });
const sink = vi.fn();
const fail = async () => { await Promise.resolve(); throw error; };
const results = await Promise.allSettled([
withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreDiagnostics("workspace", fail, sink), sink),
withWorkspaceRestoreDiagnostics("asset", fail, sink),
]);
expect(results).toEqual([{ status: "rejected", reason: error }, { status: "rejected", reason: error }]);
expect(sink).toHaveBeenCalledTimes(2);
expect(sink.mock.calls.map(([line]) => JSON.parse(line.split(": ")[1]).phase).sort()).toEqual(["asset", "workspace"]);
await expect(withWorkspaceRestoreDiagnostics("workspace", fail, sink)).rejects.toBe(error);
expect(sink).toHaveBeenCalledTimes(3);
});
});
describe("Git integration diagnostic privacy and attribution", () => {
async function capture(command: Parameters<typeof withWorkspaceRestoreGitCommand>[0], error: unknown) {
let receipt: WorkspaceRestoreDiagnostic | undefined;
const originalProperties = error && typeof error === "object" ? Object.getOwnPropertyDescriptors(error) : undefined;
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", () =>
withWorkspaceRestoreGitCommand(command, async () => { throw error; })), undefined,
(value) => { receipt = value; })).rejects.toBe(error);
if (originalProperties) expect(Object.getOwnPropertyDescriptors(error)).toEqual(originalProperties);
return receipt;
}
it.each([
["merge_tree", { code: 1, stdout: "a".repeat(40) + "\nprivate-file", stderr: "private-conflict-body" }, "merge_conflict"],
["merge_tree", { code: 1 }, "unknown"],
["merge_tree", { code: 1, stderr: `merge-tree: ${"a".repeat(40)} - not something we can merge\n` }, "invalid_object"],
["merge_tree", { code: 1, signal: "SIGTERM" }, "unknown"],
["merge_tree", { code: 1, killed: true }, "unknown"],
["merge_tree", { code: 128, stderr: "fatal: Not a valid object name private-object" }, "invalid_object"],
["merge_tree", { code: 128, stderr: `merge-tree: ${"a".repeat(40)} - not something we can merge\n` }, "invalid_object"],
["merge_base", { code: 128, stderr: "fatal: Not a valid commit name private-object" }, "invalid_object"],
["rev_parse", { code: 128, stderr: "fatal: bad object private-object" }, "invalid_object"],
["update_ref", { code: 128, stderr: `fatal: update_ref failed for ref 'private-ref': cannot lock ref 'private-ref': is at ${"a".repeat(40)} but expected ${"b".repeat(40)}\n` }, "ref_conflict"],
["update_ref", { code: 128, stderr: "fatal: Unable to create 'private-path.lock': File exists." }, "unknown"],
["update_ref", { code: "EACCES", stderr: "private-path" }, "permission_denied"],
["commit_tree", { code: "EPERM" }, "permission_denied"],
["commit_tree", { code: 1, stderr: "Permission denied private-path" }, "unknown"],
["symbolic_ref", { code: 1 }, "unknown"],
["merge_base", { code: 1 }, "unknown"],
["log", { code: 128, stderr: "localized or unrecognized private-text" }, "unknown"],
["log", { code: 128, stderr: "x".repeat(16 * 1024) + "\nfatal: bad object private-object" }, "unknown"],
["merge_tree", "private-string", "unknown"],
] as const)("classifies only supported %s evidence (%j)", async (command, error, kind) => {
const receipt = await capture(command, error);
expect(receipt).toMatchObject({ gitCommand: command, gitFailureKind: kind });
expect(JSON.stringify(receipt)).not.toContain("private-");
expect(Object.keys(receipt!).sort()).toEqual([
"phase", "step", "errorCode", "gitCommand", "gitFailureKind",
...(typeof error === "object" && typeof error.code === "number" ? ["exitCode"] : []),
].sort());
});
it("treats throwing stderr getters as unknown and never replaces the original error", async () => {
const error = Object.defineProperty(new Error("private-original"), "stderr", { get() { throw new Error("private-getter"); } });
expect(await capture("commit_tree", error)).toEqual({ phase: "workspace", step: "git_integration",
errorCode: "unknown", gitCommand: "commit_tree", gitFailureKind: "unknown" });
});
it("retains a nested command across wrappers and nested task diagnostics", async () => {
const source = Object.assign(new Error("private-source"), { code: 1, stdout: "a".repeat(40) + "\n" });
const wrapper = new Error("private-wrapper");
const sink = vi.fn();
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("directory_merge", () =>
withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", async () => {
try { await withWorkspaceRestoreGitCommand("merge_tree", async () => { throw source; }); }
catch (error) { throw preserveWorkspaceRestoreErrorDiagnostic(wrapper, error); }
}), sink)), sink)).rejects.toBe(wrapper);
expect(getWorkspaceRestoreDiagnostic(wrapper)).toEqual({ phase: "workspace", step: "git_integration",
errorCode: "unknown", exitCode: 1, gitCommand: "merge_tree", gitFailureKind: "merge_conflict" });
expect(wrapper).not.toHaveProperty("cause");
expect(sink).toHaveBeenCalledTimes(1);
expect(sink.mock.calls[0][0]).not.toContain("private-");
});
it("keeps a nested identity probe failure instead of relabelling it as its ref transaction", async () => {
const error = Object.assign(new Error("private-probe"), { code: 128, stderr: "private-probe-output" });
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", () =>
withWorkspaceRestoreGitCommand("update_ref", () =>
withWorkspaceRestoreGitCommand("symbolic_ref", async () => { throw error; }))))).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toEqual({ phase: "workspace", step: "git_integration",
errorCode: "unknown", exitCode: 128, gitCommand: "symbolic_ref", gitFailureKind: "unknown" });
});
it("does not retain a handled command's label on a later failed step or retry", async () => {
const error = Object.assign(new Error("same error"), { code: 1 });
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => {
await withWorkspaceRestoreStep("git_integration", () =>
withWorkspaceRestoreGitCommand("merge_tree", async () => { throw error; })).catch(() => {});
await withWorkspaceRestoreStep("index_reset", async () => { throw error; });
})).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toEqual({ phase: "workspace", step: "index_reset", errorCode: "unknown", exitCode: 1 });
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", async () => {
await withWorkspaceRestoreGitCommand("merge_tree", async () => { throw error; }).catch(() => {});
await withWorkspaceRestoreGitCommand("update_ref", async () => { throw error; });
}))).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toMatchObject({ gitCommand: "update_ref", gitFailureKind: "unknown" });
});
it("retains the selected parallel task's command when two errors share identity", async () => {
const error = Object.assign(new Error("shared"), { code: 1, stdout: "a".repeat(40) + "\n" });
const snapshots: WorkspaceRestoreDiagnostic[] = [];
await expect(withWorkspaceRestoreDiagnostics("workspace", async () => {
await Promise.allSettled((["merge_tree", "update_ref"] as const).map((command, index) =>
withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", () =>
withWorkspaceRestoreGitCommand(command, async () => { throw error; })), undefined,
(receipt) => { snapshots[index] = receipt; })));
recordWorkspaceRestoreDiagnostic(error, snapshots[0]);
throw error;
})).rejects.toBe(error);
expect(snapshots.map(value => value.gitCommand)).toEqual(["merge_tree", "update_ref"]);
expect(getWorkspaceRestoreDiagnostic(error)).toMatchObject({ gitCommand: "merge_tree", gitFailureKind: "merge_conflict" });
});
it("leaves successful or handled commands silent and preserves values", async () => {
const sink = vi.fn();
const value = {};
expect(await withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", async () => {
await withWorkspaceRestoreGitCommand("symbolic_ref", async () => { throw { code: 1 }; }).catch(() => {});
return withWorkspaceRestoreGitCommand("update_ref", async () => value);
}), sink)).toBe(value);
expect(sink).not.toHaveBeenCalled();
const error = new Error("outside restore");
await expect(withWorkspaceRestoreGitCommand("log", async () => { throw error; })).rejects.toBe(error);
expect(getWorkspaceRestoreDiagnostic(error)).toBeUndefined();
});
it.each([
{ phase: "asset", step: "git_integration", gitCommand: "merge_tree" },
{ phase: "workspace", step: "index_reset", gitCommand: "merge_tree" },
{ phase: "workspace", step: "git_integration", gitCommand: "private-command" },
])("does not decode Git metadata outside the closed integration contract (%j)", (value) => {
const diagnostic = sanitizeWorkspaceRestoreDiagnostic({ ...value, gitFailureKind: "merge_conflict" });
expect(diagnostic).not.toHaveProperty("gitCommand");
expect(diagnostic).not.toHaveProperty("gitFailureKind");
});
it("revalidates persisted command and failure labels without copying extra fields", () => {
expect(sanitizeWorkspaceRestoreDiagnostic({ phase: "workspace", step: "git_integration", gitCommand: "log",
gitFailureKind: "private-reason", stderr: "private-text", args: ["private-args"] })).toEqual({
phase: "workspace", step: "git_integration", errorCode: "unknown", gitCommand: "log", gitFailureKind: "unknown",
});
});
});
it("isolates wrapped transfer evidence across settlements reusing a frozen error", async () => {
const shared = Object.freeze(new Error("private-message"));
let resume!: () => void;
const gate = new Promise<void>(resolve => { resume = resolve; });
let started!: () => void;
const ready = new Promise<void>(resolve => { started = resolve; });
const first = withWorkspaceRestoreDiagnosticCapture(async () => {
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("workspace_transfer", async () => {
preserveWorkspaceRestoreErrorDiagnostic(shared, {}, { transferStep: "archive_create" });
started(); await gate; throw shared;
}))).rejects.toBe(shared);
return getWorkspaceRestoreDiagnostic(shared);
});
await ready;
const second = await withWorkspaceRestoreDiagnosticCapture(async () => {
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("workspace_transfer", async () => {
throw preserveWorkspaceRestoreErrorDiagnostic(shared, {}, { transferStep: "archive_download" });
}))).rejects.toBe(shared);
return getWorkspaceRestoreDiagnostic(shared);
});
resume();
expect((await first)?.transferStep).toBe("archive_create");
expect(second?.transferStep).toBe("archive_download");
await withWorkspaceRestoreDiagnosticCapture(async () => {
await expect(withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("workspace_transfer", async () => { throw shared; }))).rejects.toBe(shared);
expect(getWorkspaceRestoreDiagnostic(shared)?.transferStep).toBeUndefined();
});
});