Files
PaperClipAI/packages/adapter-utils/src/command-managed-runtime.test.ts
T
DottaandPaperclip 2083bf6f9a feat(connections): add AgentMail inboxes and email tasks (#13256)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Connections give agents controlled access to external services.
> - Experimental channels already map conversations to tasks and durable
work queues.
> - Email needs inbox ownership, recipient envelopes, delivery records,
and explicit sends.
> - This pull request adds AgentMail to that infrastructure and keeps
the provider key in the server vault.
> - Agents can receive and send email from local or sandbox execution
while the board follows each conversation in its task.

## Linked Issues or Issue Description

**Problem or motivation**

Agents need dedicated email addresses. Incoming email should become
assigned work. Internal task comments and progress must never become
outgoing email by accident.

**Proposed solution**

Add experimental AgentMail connections, an inbox assignment wizard,
durable email intake and publication, task email cards, and
authenticated API, CLI, and native runtime actions. Agents use Paperclip
credentials to request sends. Paperclip owns the provider key and
enforces access and task authority.

**Alternatives considered**

A general mailbox MCP connector does not provide durable task binding or
publication boundaries. A separate mailbox application duplicates task
collaboration. The board instead directs the agent through the normal
task conversation.

**Roadmap alignment**

This extends the existing experimental connections and task
infrastructure. Product scope and interaction design were reviewed with
the maintainer. Related connection authority work: #11831 and #11818.
The duplicate search found no competing task-based AgentMail
integration.

## What Changed

- Add AgentMail catalog data, shared contracts, company-scoped email
records, and an additive migration.
- Add vaulted setup, inbox assignment, access grants, trust guidance,
and provider-side allowlist guidance.
- Support WebSocket and signed-webhook intake through a shared durable
pipeline, deduplication, catch-up, and task wakeups.
- Queue explicit new conversations and replies with immutable send
intents, idempotency, delivery state, and uncertain-send resolution.
- Show inbound and outbound email cards in normal task conversations.
Keep internal messages internal.
- Add task-scoped CLI actions and the sandbox callback routes required
for Daytona execution.
- Provide a dedicated AgentMail skill automatically only to agents with
active authorized inbox assignments. Keep email instructions out of the
universal Paperclip skill.
- Advertise connector-owned `agentmail_inboxes`,
`agentmail_read_thread`, `agentmail_send`, and `agentmail_delivery`
tools only in eligible native sessions. Recheck live authority on
execution.
- Isolate Codex CLI connector skills by agent and skill revision.
Deliver the assigned skill in the run prompt for adapters that use
shared skill directories, including resumed turns. Keep automatic skills
out of manual persistent sync. Show them as read-only and document the
pattern in the connector playbook.
- Fix AgentMail health checks that entered local-stdio validation and
optional missing Codex credential cleanup in sandboxes.
- Add API, pipeline, authorization, sandbox, browser, and Storybook
coverage.

## Verification

- Live AgentMail testing covered WebSocket intake, signed webhooks,
restart catch-up, and a full receive → task → Daytona Codex CLI →
explicit reply → Delivered round trip. The reply was verified in the
other inbox. The normal task composer also initiated an outgoing email
child task.
- The connector-skill change was verified in the browser: AgentMail
appears once as an automatic, read-only skill with its assigned address.
Disabling experimental chat connections removes it; re-enabling restores
it. A regression test covers assignment data arriving after library
data.
- Connector regression coverage passed 178 runtime utility, email
integration, skill-route, and heartbeat tests. All 17 Codex execution
tests passed, including per-agent skill isolation, model identity,
revision changes, removal, and prompt delivery without shared skill
files.
- After rebasing onto master, all 44 focused email, heartbeat, and
native-authority tests passed. All 313 native-session executor tests
passed. The UI regression suite passed all 3 tests. These test sets
overlap earlier focused runs.
- Full workspace typecheck and build passed after the rebase. Token
gates passed. Earlier focused Playwright task/setup coverage and the
Storybook build also passed.
- Native connector tool execution uses deterministic integration tests.
Live Daytona qualification used the Codex CLI adapter; the new
shared-home prompt fallback has deterministic coverage.
- The full repository suite is run by CI. The earlier unsharded local
full-suite attempt was stopped after the equivalent CI suites passed and
is not reported as a completed local run. Greptile reviewed
`7e57dc267a8446d3c906e3cc5b8abc94fb8860eb` at 5/5 with no unresolved
threads. All server, workspace, serialized server, and browser suites
passed in CI. The build job hit a five-second timeout in a runner
transport test; both variants and the full 80-test file passed locally
with unchanged timeouts. The build passed on retry on the same commit
without code or timeout changes. All required CI gates, including the
final `ci / verify` and `ci / e2e` summaries, are green on
`7e57dc267a8446d3c906e3cc5b8abc94fb8860eb`.

## Risks

- Email from external senders can start normal agent work. Setup
recommends a low-trust agent and AgentMail sender controls. Sender
addresses never grant board membership.
- Provider timeouts can leave uncertain sends. Retries retain their
idempotency key; expired windows require reconciliation or operator
resolution.
- Connector skills and native tools are assignment-dependent and require
current access. Revocation denies retained calls; assignment changes
select a new runtime context.
- Activation remains behind the experimental-channel setting. The native
runner path has deterministic coverage; live Daytona qualification used
the Codex CLI adapter.
- Schema changes are additive. Inbox ownership is unique across
companies. Disconnect preserves provider inboxes and task history.

## Model Used

OpenAI GPT-6 (Codex). Used reasoning, repository tools, code execution,
and browser testing. The exact deployment model ID and context-window
size were not exposed in this session.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-09-11 16:56:38 -05:00

1075 lines
47 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile as execFileCallback, spawn } from "node:child_process";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import {
createCommandManagedRuntimeClient,
prepareCommandManagedRuntime,
type CommandManagedDuplexChannel,
type CommandManagedRuntimeRunner,
} from "./command-managed-runtime.js";
import type { SandboxSyncOperation } from "./sandbox-managed-runtime.js";
import type { RunProcessResult } from "./server-utils.js";
/**
* An in-memory fake `CommandManagedDuplexChannel`. It holds no real process; a
* write echoes straight to the one registered data listener, so a test proves
* the channel contract carries a `Uint8Array` chunk with no string coercion in
* between. The fake never exits on its own; a test calls the listener it
* registers with `onExit` only when it needs one.
*/
function createFakeEchoDuplexChannel(): CommandManagedDuplexChannel {
let dataListener: ((chunk: Uint8Array) => void) | null = null;
return {
write(data: Uint8Array): void {
dataListener?.(data);
},
onData(listener: (chunk: Uint8Array) => void): void {
dataListener = listener;
},
onExit(): void {},
stop(): void {},
close: async (): Promise<void> => {},
};
}
const execFile = promisify(execFileCallback);
interface SpawnRunnerHandle {
runner: CommandManagedRuntimeRunner;
calls: Array<{ command: string; args?: string[]; cwd?: string; stdin?: string }>;
}
// A runner that actually executes the shell scripts (piping stdin through a real
// pipe so multi-MB payloads work) and replays stdout through onLog in several
// chunks so the streaming readFile byte-counter is exercised.
function makeSpawnRunner(options: {
supportsSingleStreamStdinProgress?: boolean;
maxStdoutBytes?: number;
} = {}): SpawnRunnerHandle {
const calls: Array<{ command: string; args?: string[]; cwd?: string; stdin?: string }> = [];
const runner: CommandManagedRuntimeRunner = {
supportsSingleStreamStdinProgress: options.supportsSingleStreamStdinProgress,
execute: async (input) =>
await new Promise<RunProcessResult>((resolve) => {
calls.push({
command: input.command,
args: input.args,
cwd: input.cwd,
stdin: input.stdin,
});
const startedAt = new Date().toISOString();
const command =
input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
const child = spawn(command, input.args ?? [], {
cwd: input.cwd,
env: { ...process.env, ...input.env },
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.on("error", () => {
resolve({ exitCode: 127, signal: null, timedOut: false, stdout, stderr, pid: null, startedAt });
});
child.on("close", async (code) => {
if (
options.maxStdoutBytes != null &&
Buffer.byteLength(stdout, "utf8") > options.maxStdoutBytes
) {
resolve({
exitCode: 1,
signal: null,
timedOut: false,
stdout,
stderr: `stdout exceeded ${options.maxStdoutBytes} bytes`,
pid: child.pid ?? null,
startedAt,
});
return;
}
if (input.onLog && stdout.length > 0) {
const chunkSize = Math.max(1, Math.ceil(stdout.length / 4));
for (let offset = 0; offset < stdout.length; offset += chunkSize) {
await input.onLog("stdout", stdout.slice(offset, offset + chunkSize));
}
}
resolve({
exitCode: code ?? 0,
signal: null,
timedOut: false,
stdout,
stderr,
pid: child.pid ?? null,
startedAt,
});
});
if (input.stdin != null) child.stdin.write(input.stdin);
child.stdin.end();
}),
};
return { runner, calls };
}
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
return Uint8Array.from(buffer).buffer;
}
function shellQuoteForTest(value: string): string {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
async function withBase64StringByteLimit<T>(limitBytes: number, fn: () => Promise<T>): Promise<T> {
const originalToString = Buffer.prototype.toString;
Buffer.prototype.toString = function patchedToString(
this: Buffer,
encoding?: BufferEncoding,
start?: number,
end?: number,
) {
if (encoding === "base64" && this.byteLength > limitBytes) {
throw new Error(`test guard: attempted to base64-encode ${this.byteLength} bytes at once`);
}
return originalToString.call(this, encoding, start, end);
} as typeof Buffer.prototype.toString;
try {
return await fn();
} finally {
Buffer.prototype.toString = originalToString;
}
}
describe("command managed runtime", () => {
const cleanupDirs: string[] = [];
afterEach(async () => {
while (cleanupDirs.length > 0) {
const dir = cleanupDirs.pop();
if (!dir) continue;
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
}
});
it("reports a missing sandbox file as ENOENT without masking command failures", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-remote-missing-"));
try {
const { runner } = makeSpawnRunner();
const client = createCommandManagedRuntimeClient({ runner, commandCwd: root, timeoutMs: 5000 });
const missingPath = path.join(root, "auth.json");
await expect(client.readFile(missingPath)).rejects.toMatchObject({ code: "ENOENT", path: missingPath });
await writeFile(missingPath, "present");
const failedClient = createCommandManagedRuntimeClient({
commandCwd: root, timeoutMs: 5000,
runner: { ...runner, execute: async (input) => input.args?.some((arg) => arg.startsWith("wc -c"))
? { exitCode: 1, signal: null, timedOut: false, stdout: "", stderr: "transport read failed", pid: null, startedAt: new Date().toISOString() }
: runner.execute(input) },
});
await expect(failedClient.readFile(missingPath)).rejects.toThrow("transport read failed");
await expect(client.readFile(missingPath)).resolves.toEqual(Buffer.from("present"));
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("keeps the runtime overlay out of sandbox workspace sync by default", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-"));
cleanupDirs.push(rootDir);
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
await mkdir(path.join(localWorkspaceDir, ".paperclip-runtime"), { recursive: true });
await mkdir(remoteWorkspaceDir, { recursive: true });
await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8");
await writeFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "{\"keep\":true}\n", "utf8");
const calls: Array<{
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
stdin?: string;
timeoutMs?: number;
}> = [];
const runner = {
execute: async (input: {
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
stdin?: string;
timeoutMs?: number;
}): Promise<RunProcessResult> => {
calls.push({ ...input });
const startedAt = new Date().toISOString();
const env = {
...process.env,
...input.env,
};
const command =
input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
const args = [...(input.args ?? [])];
if (
input.stdin != null &&
(input.command === "sh" || input.command === "bash") &&
(args[0] === "-c" || args[0] === "-lc") &&
typeof args[1] === "string"
) {
env.PAPERCLIP_TEST_STDIN = input.stdin;
args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`;
}
try {
const result = await execFile(command, args, {
cwd: input.cwd,
env,
maxBuffer: 32 * 1024 * 1024,
timeout: input.timeoutMs,
});
return {
exitCode: 0,
signal: null,
timedOut: false,
stdout: result.stdout,
stderr: result.stderr,
pid: null,
startedAt,
};
} catch (error) {
const err = error as NodeJS.ErrnoException & {
stdout?: string;
stderr?: string;
code?: string | number | null;
signal?: NodeJS.Signals | null;
killed?: boolean;
};
return {
exitCode: typeof err.code === "number" ? err.code : null,
signal: err.signal ?? null,
timedOut: Boolean(err.killed && input.timeoutMs),
stdout: err.stdout ?? "",
stderr: err.stderr ?? "",
pid: null,
startedAt,
};
}
},
};
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "claude",
workspaceLocalDir: localWorkspaceDir,
});
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n");
await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).rejects
.toMatchObject({ code: "ENOENT" });
// The single-stream upload pipes the tarball through exactly one stdin-backed
// process (the speed fix); nothing else streams stdin.
expect(calls.filter((call) => call.stdin != null).length).toBe(1);
await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true });
await writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8");
await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "remote-state.json"), "{\"remote\":true}\n", "utf8");
await prepared.restoreWorkspace();
await expect(readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe("remote workspace\n");
await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).resolves
.toBe("{\"keep\":true}\n");
await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "remote-state.json"), "utf8")).rejects
.toMatchObject({ code: "ENOENT" });
// Restore streams the download through `base64`/onLog (no stdin), so the only
// stdin-backed call remains the single upload from prepare.
expect(calls.filter((call) => call.stdin != null).length).toBe(1);
});
it("stages runtime assets without replacing or restoring an in-place workspace", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-assets-only-"));
cleanupDirs.push(rootDir);
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
const localHomeDir = path.join(rootDir, "local-home");
await mkdir(localWorkspaceDir, { recursive: true });
await mkdir(remoteWorkspaceDir, { recursive: true });
await mkdir(localHomeDir, { recursive: true });
await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8");
await writeFile(path.join(remoteWorkspaceDir, "README.md"), "authoritative workspace\n", "utf8");
await writeFile(path.join(localHomeDir, "auth.json"), '{"token":"host"}\n', "utf8");
const { runner } = makeSpawnRunner();
let restoredAuth = "";
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
syncWorkspace: false,
assets: [
{
key: "home",
localDir: localHomeDir,
restore: async ({ assetDir, readFile }) => {
restoredAuth = (await readFile(path.join(assetDir, "auth.json"))).toString("utf8");
},
},
],
});
expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
expect(prepared.assetDirs.home).toBe(path.join(remoteWorkspaceDir, ".paperclip-runtime", "codex", "home"));
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe(
"authoritative workspace\n",
);
await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8")).resolves.toBe(
'{"token":"host"}\n',
);
await writeFile(path.join(prepared.assetDirs.home, "auth.json"), '{"token":"remote"}\n', "utf8");
await prepared.restoreWorkspace();
expect(restoredAuth).toBe('{"token":"remote"}\n');
await expect(readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe(
"local workspace\n",
);
});
it("stages each additional project into an isolated dir on the base64/tar transport, one failure skipped", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-additional-"));
cleanupDirs.push(rootDir);
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
await mkdir(localWorkspaceDir, { recursive: true });
await mkdir(remoteWorkspaceDir, { recursive: true });
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
const goodOne = path.join(rootDir, "src-one");
const goodTwo = path.join(rootDir, "src-two");
await mkdir(goodOne, { recursive: true });
await mkdir(path.join(goodTwo, "nested"), { recursive: true });
await writeFile(path.join(goodOne, "one.txt"), "one body\n", "utf8");
await writeFile(path.join(goodTwo, "nested", "two.txt"), "two body\n", "utf8");
// The `makeSpawnRunner` runner exposes no native syncIn, so staging rides the
// base64/tar fallback. The middle source points at a missing directory, so
// its tar build fails; failure isolation skips only it.
const { runner } = makeSpawnRunner();
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "claude",
workspaceLocalDir: localWorkspaceDir,
additionalSources: [
{ localPath: goodOne, projectId: "one", ignoreResolution: { kind: "other" } },
{ localPath: path.join(rootDir, "missing"), projectId: "broken", ignoreResolution: { kind: "other" } },
{ localPath: goodTwo, projectId: "two", ignoreResolution: { kind: "other" } },
],
});
const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "claude");
expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["one", "two"]);
expect(prepared.additionalSourceDirs.one).toBe(path.posix.join(runtimeRootDir, "project-one"));
expect(prepared.additionalSourceDirs.two).toBe(path.posix.join(runtimeRootDir, "project-two"));
expect(prepared.additionalSourceDirs.broken).toBeUndefined();
// Each healthy project's tree materialized in its OWN dir (nested files kept).
await expect(readFile(path.join(prepared.additionalSourceDirs.one, "one.txt"), "utf8")).resolves.toBe("one body\n");
await expect(readFile(path.join(prepared.additionalSourceDirs.two, "nested", "two.txt"), "utf8")).resolves.toBe(
"two body\n",
);
// The broken project's dir was never created.
await expect(readFile(path.join(runtimeRootDir, "project-broken"), "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
});
it("keeps adapter detection on the profile-backed shell path", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-detect-"));
cleanupDirs.push(rootDir);
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
await mkdir(localWorkspaceDir, { recursive: true });
await mkdir(remoteWorkspaceDir, { recursive: true });
const { runner, calls } = makeSpawnRunner();
await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "claude",
workspaceLocalDir: localWorkspaceDir,
installCommand: "echo install",
detectCommand: "sh",
});
// The detection probe must be the first shell invocation, so a CLI on the
// sandbox default PATH is discoverable before we decide whether to install.
expect(calls[0]?.args?.join(" ")).toContain("command -v 'sh'");
// Detection succeeds here, so the install command must be skipped entirely;
// the remaining calls are workspace staging, never the install command.
expect(calls.some((call) => call.args?.join(" ").includes("echo install"))).toBe(false);
});
it("runs setup commands from a stable root cwd when staging into a nested remote workspace dir", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-nested-"));
cleanupDirs.push(rootDir);
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteBaseDir = path.join(rootDir, "remote-base");
const remoteWorkspaceDir = path.join(remoteBaseDir, ".paperclip-runtime", "runs", "test", "workspace");
await mkdir(localWorkspaceDir, { recursive: true });
await mkdir(remoteBaseDir, { recursive: true });
await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8");
const { runner, calls } = makeSpawnRunner();
await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteBaseDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
workspaceRemoteDir: remoteWorkspaceDir,
});
expect(calls.length).toBeGreaterThan(0);
expect(calls.every((call) => call.cwd === "/")).toBe(true);
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n");
});
it("uploads a multi-MB payload in a single process and preserves exact bytes", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-write-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
// ~3 MB of every byte value so the test catches any non-binary-safe handling.
const payload = Buffer.alloc(3 * 1024 * 1024);
for (let i = 0; i < payload.length; i++) payload[i] = i % 256;
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
const progress: Array<{ done: number; total: number | null }> = [];
await withBase64StringByteLimit(4 * 1024 * 1024, async () => {
await client.writeFile(remotePath, toArrayBuffer(payload), {
onProgress: (done, total) => {
progress.push({ done, total });
},
});
});
// Exactly one upload process: O(1) round-trips regardless of payload size.
expect(calls.length).toBe(2);
expect(calls[1].args?.join(" ")).toContain("rm -rf");
expect(calls[0].stdin).toBeTypeOf("string");
const written = await readFile(remotePath);
expect(written.equals(payload)).toBe(true);
// Progress is monotonically non-decreasing and reaches the total.
expect(progress.length).toBeGreaterThan(0);
for (let i = 1; i < progress.length; i++) {
expect(progress[i].done).toBeGreaterThanOrEqual(progress[i - 1].done);
}
expect(progress.at(-1)).toEqual({ done: payload.length, total: payload.length });
});
it("stages a single-file write to <path>.paperclip-upload then atomically renames it (single-stream path)", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-atomic-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await client.writeFile(remotePath, toArrayBuffer(Buffer.from("hello atomic\n")));
// Characterization guardrail: the legacy single-file transport must keep its
// stage-then-atomic-rename shape (temp .paperclip-upload + `mv -f`).
const script = (calls[0].args ?? []).join(" ");
expect(script).toContain(`${remotePath}.paperclip-upload`);
expect(script).toContain(`trap cleanup EXIT`);
expect(script).toContain(`mv -f`);
expect(script.indexOf(".paperclip-upload")).toBeLessThan(script.indexOf("mv -f"));
expect(await readFile(remotePath, "utf8")).toBe("hello atomic\n");
});
it("cleans up a staged upload when rename fails", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-upload-cleanup-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
const payload = Buffer.alloc(3 * 1024 * 1024, 7);
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const delegatedExecute = runner.execute.bind(runner);
runner.execute = async (input) => {
const script = (input.args ?? []).join(" ");
if (script.includes("mv -f") && script.includes(".paperclip-upload.")) {
calls.push({ command: input.command, args: input.args, cwd: input.cwd, stdin: input.stdin });
return {
exitCode: 1,
signal: null,
timedOut: false,
stdout: "",
stderr: "rename failed",
pid: null,
startedAt: new Date().toISOString(),
};
}
return await delegatedExecute(input);
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await expect(client.writeFile(remotePath, toArrayBuffer(payload))).rejects.toThrow(/rename failed/);
const uploadCall = calls.find((call) => (call.args ?? []).join(" ").includes(".paperclip-upload."));
expect(uploadCall).toBeDefined();
const stagedPath = (uploadCall?.args ?? []).join(" ").match(/([/A-Za-z0-9_.-]+\.paperclip-upload\.[A-Za-z0-9-]+)/)?.[1];
expect(stagedPath).toBeDefined();
await expect(readFile(stagedPath!, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
expect(calls.some((call) => (call.args ?? []).join(" ").includes(`rm -rf '${stagedPath}'`))).toBe(true);
await expect(readFile(remotePath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
});
it("stages a single-file write to a temp then renames it on the chunked fallback path too", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-atomic-fallback-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
const payload = Buffer.alloc(10 * 1024 * 1024);
for (let i = 0; i < payload.length; i++) payload[i] = i % 256;
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: false });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await client.writeFile(remotePath, toArrayBuffer(payload));
const scripts = calls.map((call) => (call.args ?? []).join(" "));
expect(scripts.some((script) => script.includes(`${remotePath}.paperclip-upload`))).toBe(true);
expect(scripts.some((script) => script.includes(`mv -f`))).toBe(true);
expect((await readFile(remotePath)).equals(payload)).toBe(true);
});
it("test_client_syncIn_present_even_without_native_runner_syncIn", () => {
// Phase 2 (PAP-3222): `client.syncIn` is ALWAYS present so the caller can
// delegate unconditionally. `syncOut` stays native-only (no generic outbound
// fallback in this seam).
const base = makeSpawnRunner().runner;
const client = createCommandManagedRuntimeClient({ runner: base, commandCwd: "/", timeoutMs: 1 });
expect(client.syncIn).toBeTypeOf("function");
expect(client.syncOut).toBeUndefined();
// A runner advertising only one verb still gets the fallback syncIn; syncOut
// stays undefined (native delegation needs BOTH verbs).
const onlyIn: CommandManagedRuntimeRunner = { ...base, syncIn: async () => ({ operations: [] }) };
const partial = createCommandManagedRuntimeClient({ runner: onlyIn, commandCwd: "/", timeoutMs: 1 });
expect(partial.syncIn).toBeTypeOf("function");
expect(partial.syncOut).toBeUndefined();
// With both verbs, syncIn delegates natively and syncOut is exposed.
const both: CommandManagedRuntimeRunner = {
...base,
syncIn: async () => ({ operations: [] }),
syncOut: async () => ({ operations: [] }),
};
const native = createCommandManagedRuntimeClient({ runner: both, commandCwd: "/", timeoutMs: 1 });
expect(native.syncIn).toBeTypeOf("function");
expect(native.syncOut).toBeTypeOf("function");
});
it("base64 fallback client reports allowConcurrentSyncOperations true", () => {
// A runner with no native sync uses the base64 fallback, which always
// permits concurrent sync operations.
const base = makeSpawnRunner().runner;
const client = createCommandManagedRuntimeClient({ runner: base, commandCwd: "/", timeoutMs: 1 });
expect(client.allowConcurrentSyncOperations).toBe(true);
});
it("undeclared native runner reports allowConcurrentSyncOperations false", () => {
// A native runner (both sync verbs) that never opted into concurrency keeps
// the flag off.
const base = makeSpawnRunner().runner;
const undeclaredNative: CommandManagedRuntimeRunner = {
...base,
syncIn: async () => ({ operations: [] }),
syncOut: async () => ({ operations: [] }),
};
const client = createCommandManagedRuntimeClient({
runner: undeclaredNative,
commandCwd: "/",
timeoutMs: 1,
});
expect(client.allowConcurrentSyncOperations).toBe(false);
});
it("declared native runner reports allowConcurrentSyncOperations true", () => {
// A native runner that verified the opt-in carries the flag through to the
// client.
const base = makeSpawnRunner().runner;
const declaredNative: CommandManagedRuntimeRunner = {
...base,
allowConcurrentSyncOperations: true,
syncIn: async () => ({ operations: [] }),
syncOut: async () => ({ operations: [] }),
};
const client = createCommandManagedRuntimeClient({
runner: declaredNative,
commandCwd: "/",
timeoutMs: 1,
});
expect(client.allowConcurrentSyncOperations).toBe(true);
});
it("base64 fallback ignores a runner opt-in without both sync verbs", () => {
// A runner that sets the opt-in but exposes only one sync verb still uses
// the fallback, which permits concurrency independent of the runner flag.
const base = makeSpawnRunner().runner;
const onlyIn: CommandManagedRuntimeRunner = {
...base,
allowConcurrentSyncOperations: true,
syncIn: async () => ({ operations: [] }),
};
const client = createCommandManagedRuntimeClient({ runner: onlyIn, commandCwd: "/", timeoutMs: 1 });
expect(client.allowConcurrentSyncOperations).toBe(true);
});
it("test_client_syncIn_delegates_to_native_runner_with_zero_execute_calls", async () => {
// With a native runner, `client.syncIn` forwards `files` + `postUploadCommands`
// to the runner and issues ZERO `execute` round-trips (the provider owns the
// transport + command execution).
let executeCalls = 0;
const forwarded: SandboxSyncOperation[][] = [];
const runner: CommandManagedRuntimeRunner = {
execute: async () => {
executeCalls += 1;
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: "" };
},
syncIn: async (operations) => {
forwarded.push(operations);
return {
operations: operations.map((op) => ({
operationId: op.operationId,
filesTransferred: op.files.length,
bytesTransferred: 0,
})),
};
},
syncOut: async () => ({ operations: [] }),
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 1 });
const operations: SandboxSyncOperation[] = [
{
operationId: "op-1",
files: [{ sourcePath: "/host/a", targetPath: "/remote/a", kind: "directory" }],
postUploadCommands: [{ command: "echo done", cwd: "/remote/a" }],
},
];
const result = await client.syncIn!(operations);
expect(executeCalls).toBe(0);
expect(forwarded).toEqual([operations]);
expect(result.operations[0]).toMatchObject({ operationId: "op-1", filesTransferred: 1 });
});
it("fallback syncIn tarballs+uploads a directory then runs post-upload commands in order", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-syncin-fallback-"));
cleanupDirs.push(rootDir);
const sourceDir = path.join(rootDir, "source");
const targetDir = path.join(rootDir, "target");
const markerDir = path.join(rootDir, "markers");
await mkdir(path.join(sourceDir, "nested"), { recursive: true });
await mkdir(markerDir, { recursive: true });
await writeFile(path.join(sourceDir, "file.txt"), "payload\n", "utf8");
await writeFile(path.join(sourceDir, "nested", "deep.txt"), "deep\n", "utf8");
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await client.syncIn!([
{
operationId: "op-dir",
files: [{ sourcePath: sourceDir, targetPath: targetDir, kind: "directory" }],
postUploadCommands: [
{ command: "touch " + shellQuoteForTest(path.join(markerDir, "1-first")) },
{ command: "touch " + shellQuoteForTest(path.join(markerDir, "2-second")) },
],
},
]);
// Files landed via tar → untar (destroy-then-replace).
expect(await readFile(path.join(targetDir, "file.txt"), "utf8")).toBe("payload\n");
expect(await readFile(path.join(targetDir, "nested", "deep.txt"), "utf8")).toBe("deep\n");
// Both post-upload commands ran (markers exist).
await expect(readFile(path.join(markerDir, "1-first"))).resolves.toBeDefined();
await expect(readFile(path.join(markerDir, "2-second"))).resolves.toBeDefined();
// Ordering: upload → untar → command 1 → command 2. The tarball upload is the
// single stdin-backed call; the untar and the two commands follow it in order.
const scripts = calls.map((call) => (call.args ?? []).join("\n"));
const uploadIdx = scripts.findIndex((s) => s.includes(".paperclip-syncin.tar") && s.includes("base64 -d"));
const untarIdx = scripts.findIndex((s) => s.includes("tar -xf") && s.includes(targetDir));
const cmd1Idx = scripts.findIndex((s) => s.includes("1-first"));
const cmd2Idx = scripts.findIndex((s) => s.includes("2-second"));
expect(uploadIdx).toBeGreaterThanOrEqual(0);
expect(untarIdx).toBeGreaterThan(uploadIdx);
expect(cmd1Idx).toBeGreaterThan(untarIdx);
expect(cmd2Idx).toBeGreaterThan(cmd1Idx);
});
it("fallback syncIn runs a post-upload command under its own timeout, not the sync-client default", async () => {
// The run-specific timeout (`spec.timeoutMs`, stamped onto each delegated
// post-upload command) can differ from the sync client's own default. The
// fallback must honor the per-command `timeoutMs` so the delegated
// extract/cleanup/merge runs under the run limit — not the sync default.
const syncClientTimeoutMs = 30_000;
const runTimeoutMs = 7_000;
const execTimeouts: Array<number | undefined> = [];
const runner: CommandManagedRuntimeRunner = {
execute: async (input) => {
execTimeouts.push(input.timeoutMs);
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: "" };
},
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: syncClientTimeoutMs });
await client.syncIn!([
{
operationId: "op-timeout",
files: [],
postUploadCommands: [
{ command: "echo carries-run-timeout", timeoutMs: runTimeoutMs },
{ command: "echo defaults-to-sync-timeout" },
],
},
]);
// First command carries the run timeout; a command with no explicit timeout
// still falls back to the sync-client default (matched by the stamping in
// prepareSandboxManagedRuntime, which never leaves a delegated command bare).
expect(execTimeouts).toEqual([runTimeoutMs, syncClientTimeoutMs]);
});
it("fallback syncIn writes a mode-constrained file directly to its target and then applies the mode", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-syncin-mode-"));
cleanupDirs.push(rootDir);
const sourceFile = path.join(rootDir, "source.txt");
const targetFile = path.join(rootDir, "target.txt");
await writeFile(sourceFile, "payload\n", "utf8");
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await client.syncIn!([
{
operationId: "op-mode",
files: [{ sourcePath: sourceFile, targetPath: targetFile, kind: "file", mode: 0o640 }],
},
]);
expect(await readFile(targetFile, "utf8")).toBe("payload\n");
// The write goes straight to the target path. No staging name and no
// rename step exist between the write and the chmod.
const scripts = calls.map((call) => (call.args ?? []).join(" "));
expect(scripts.some((script) => script.includes(".paperclip-syncin."))).toBe(false);
expect(scripts.some((script) => script.includes("mv -f") && script.includes(".paperclip-syncin."))).toBe(
false,
);
const chmodScript = scripts.find((script) => script.includes("chmod 640"));
expect(chmodScript).toBeDefined();
expect(chmodScript).toContain(targetFile);
});
it("post-upload commands execute verbatim and are never rewritten", async () => {
// The provider/client treats each command as opaque: it is executed VERBATIM,
// never concatenated with asset keys / paths or otherwise rewritten.
const executed: string[] = [];
const runner: CommandManagedRuntimeRunner = {
execute: async (input) => {
// Only capture the post-upload command executions (single `sh -c <cmd>`).
if ((input.args?.[0] === "-c") && typeof input.args?.[1] === "string") {
executed.push(input.args[1]);
}
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: "" };
},
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 1 });
const verbatim = "my-tool --flag 'quoted value' && echo $HOME";
await client.syncIn!([
{ operationId: "op-verbatim", files: [], postUploadCommands: [{ command: verbatim }] },
]);
// The exact string appears among executed scripts, unmodified.
expect(executed).toContain(verbatim);
});
it("post-upload command cwd that escapes the target root is rejected", async () => {
// A `cwd` that escapes the operation's target root — via `..` or an absolute
// path outside the target — is rejected BEFORE any handoff (no execute).
let executeCalls = 0;
const runner: CommandManagedRuntimeRunner = {
execute: async () => {
executeCalls += 1;
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: "" };
},
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 1 });
const traversal: SandboxSyncOperation[] = [
{
operationId: "op-traversal",
files: [{ sourcePath: "/host/a", targetPath: "/remote/a", kind: "directory" }],
postUploadCommands: [{ command: "echo x", cwd: "/remote/a/../etc" }],
},
];
await expect(client.syncIn!(traversal)).rejects.toThrow(/confined absolute POSIX path|escapes/);
const absoluteEscape: SandboxSyncOperation[] = [
{
operationId: "op-escape",
files: [{ sourcePath: "/host/a", targetPath: "/remote/a", kind: "directory" }],
postUploadCommands: [{ command: "echo x", cwd: "/etc/passwd" }],
},
];
await expect(client.syncIn!(absoluteEscape)).rejects.toThrow(/escapes the operation's target root/);
// A confined cwd (equal to the target root) passes confinement — it fails
// later at tar time (the source dir does not exist), which is a DIFFERENT
// error than a confinement rejection.
const confined: SandboxSyncOperation[] = [
{
operationId: "op-confined",
files: [{ sourcePath: "/host/a", targetPath: "/remote/a", kind: "directory" }],
postUploadCommands: [{ command: "echo x", cwd: "/remote/a" }],
},
];
let confinementRejected = false;
try {
await client.syncIn!(confined);
} catch (error) {
confinementRejected = /escapes the operation's target root|confined absolute POSIX path/.test(
(error as Error).message,
);
}
expect(confinementRejected).toBe(false);
// Confinement rejected before any exec for the escape cases.
expect(executeCalls).toBe(0);
});
it("fallback syncIn aborts on the first non-zero exit", async () => {
// The first non-zero post-upload command aborts the operation: syncIn rejects,
// the remaining commands do NOT run, and there is no silent partial fallback.
const executed: string[] = [];
const runner: CommandManagedRuntimeRunner = {
execute: async (input) => {
const script = input.args?.[1] ?? "";
executed.push(script);
const isFailing = script.includes("FAIL-COMMAND");
return {
exitCode: isFailing ? 3 : 0,
signal: null,
timedOut: false,
stdout: "",
stderr: isFailing ? "boom" : "",
pid: null,
startedAt: "",
};
},
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 1 });
await expect(
client.syncIn!([
{
operationId: "op-failfast",
files: [],
postUploadCommands: [
{ command: "echo before" },
{ command: "FAIL-COMMAND" },
{ command: "echo SHOULD-NOT-RUN" },
],
},
]),
).rejects.toThrow(/exit code 3|boom/);
expect(executed.some((s) => s.includes("echo before"))).toBe(true);
expect(executed.some((s) => s.includes("FAIL-COMMAND"))).toBe(true);
expect(executed.some((s) => s.includes("SHOULD-NOT-RUN"))).toBe(false);
});
it("test_single_stream_writeFile_collapses_roundtrips_under_96MiB", async () => {
// Research A1: with single-stream enabled a ≤96 MiB write is ONE round-trip;
// without it, the chunked path is `2 + ceil(bytes / 3 MiB)`. Same payload,
// same client API — only the runner capability flag differs.
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-single-stream-collapse-"));
cleanupDirs.push(rootDir);
const payload = Buffer.alloc(9 * 1024 * 1024, 7); // 9 MiB → chunked = 2 + 3 = 5 execs
const single = makeSpawnRunner({ supportsSingleStreamStdinProgress: true });
const singleClient = createCommandManagedRuntimeClient({ runner: single.runner, commandCwd: "/", timeoutMs: 30_000 });
await singleClient.writeFile(path.join(rootDir, "single.bin"), toArrayBuffer(payload));
expect(single.calls.length).toBe(2);
const chunked = makeSpawnRunner({ supportsSingleStreamStdinProgress: false });
const chunkedClient = createCommandManagedRuntimeClient({ runner: chunked.runner, commandCwd: "/", timeoutMs: 30_000 });
await chunkedClient.writeFile(path.join(rootDir, "chunked.bin"), toArrayBuffer(payload));
// 3 (init temp + final mv + cleanup) + ceil(9MiB / 3MiB) = 6 round-trips.
expect(chunked.calls.length).toBe(3 + Math.ceil(payload.byteLength / (3 * 1024 * 1024)));
expect(chunked.calls.length).toBeGreaterThan(single.calls.length);
expect((await readFile(path.join(rootDir, "single.bin"))).equals(payload)).toBe(true);
expect((await readFile(path.join(rootDir, "chunked.bin"))).equals(payload)).toBe(true);
});
it("falls back to chunked upload progress when the runner cannot report mid-stream stdin progress", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-write-fallback-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
const payload = Buffer.alloc(12 * 1024 * 1024);
for (let i = 0; i < payload.length; i++) payload[i] = i % 256;
const { runner, calls } = makeSpawnRunner({ supportsSingleStreamStdinProgress: false });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
const progress: Array<{ done: number; total: number | null }> = [];
await client.writeFile(remotePath, toArrayBuffer(payload), {
onProgress: (done, total) => {
progress.push({ done, total });
},
});
const written = await readFile(remotePath);
expect(written.equals(payload)).toBe(true);
// Provider-backed sandbox runners cannot surface mid-flight progress for a
// single stdin RPC, so we intentionally use several large append commands.
expect(calls.length).toBeGreaterThan(2);
const stdinCalls = calls.filter((call) => call.stdin != null);
expect(stdinCalls.length).toBeGreaterThan(2);
expect(stdinCalls.every((call) => Buffer.byteLength(call.stdin ?? "", "utf8") <= 4.1 * 1024 * 1024)).toBe(true);
expect(progress.length).toBeGreaterThan(2);
for (let i = 1; i < progress.length; i++) {
expect(progress[i].done).toBeGreaterThanOrEqual(progress[i - 1].done);
}
expect(progress.at(-1)).toEqual({ done: payload.length, total: payload.length });
});
it("falls back to bounded chunks when the runner does not explicitly opt in", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-write-fallback-no-progress-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "nested", "payload.bin");
const payload = Buffer.alloc(12 * 1024 * 1024);
for (let i = 0; i < payload.length; i++) payload[i] = i % 256;
const { runner, calls } = makeSpawnRunner();
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await withBase64StringByteLimit(4 * 1024 * 1024, async () => {
await client.writeFile(remotePath, toArrayBuffer(payload));
});
const written = await readFile(remotePath);
expect(written.equals(payload)).toBe(true);
// A runner that doesn't mark single-stream stdin support must avoid passing
// the whole base64 archive as one string, so we expect multiple append calls.
const stdinCalls = calls.filter((call) => call.stdin != null);
expect(stdinCalls.length).toBeGreaterThan(1);
expect(stdinCalls.every((call) => Buffer.byteLength(call.stdin ?? "", "utf8") <= 4.1 * 1024 * 1024)).toBe(true);
});
it("downloads in bounded stdout chunks and reports monotonic byte progress to the total", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-read-"));
cleanupDirs.push(rootDir);
const remotePath = path.join(rootDir, "download.bin");
const payload = Buffer.alloc(7 * 1024 * 1024);
for (let i = 0; i < payload.length; i++) payload[i] = (i * 7) % 256;
await writeFile(remotePath, payload);
const { runner, calls } = makeSpawnRunner({ maxStdoutBytes: 5 * 1024 * 1024 });
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
const progress: Array<{ done: number; total: number | null }> = [];
const bytes = await client.readFile(remotePath, {
onProgress: (done, total) => {
progress.push({ done, total });
},
});
expect(Buffer.from(bytes as ArrayBuffer).equals(payload)).toBe(true);
// The old single `base64 < file` path would exceed the runner's stdout cap.
// The bounded path reads with several small `dd | base64` commands instead.
expect(calls.some((call) => call.args?.join(" ").includes("base64 <"))).toBe(false);
expect(calls.filter((call) => call.args?.join(" ").includes("dd if=")).length).toBeGreaterThan(1);
expect(progress.length).toBeGreaterThan(1);
for (let i = 1; i < progress.length; i++) {
expect(progress[i].done).toBeGreaterThanOrEqual(progress[i - 1].done);
}
expect(progress.every((entry) => entry.total === payload.length)).toBe(true);
expect(progress.at(-1)?.done).toBe(payload.length);
});
it("includes stdout diagnostics when a managed runtime command fails", async () => {
const startedAt = new Date().toISOString();
const runner: CommandManagedRuntimeRunner = {
execute: async () => ({
exitCode: 2,
signal: null,
timedOut: false,
stdout: "tar: workspace-download.tar: Cannot open: Permission denied\n",
stderr: "",
pid: null,
startedAt,
}),
};
const client = createCommandManagedRuntimeClient({ runner, commandCwd: "/", timeoutMs: 30_000 });
await expect(client.run("tar -cf workspace-download.tar .", { timeoutMs: 30_000 })).rejects.toThrow(
/stdout: tar: workspace-download\.tar: Cannot open: Permission denied/,
);
});
it("test_channel_round_trips_all_byte_values", () => {
const channel = createFakeEchoDuplexChannel();
const allByteValues = Uint8Array.from({ length: 256 }, (_, value) => value);
const received: Uint8Array[] = [];
channel.onData((chunk) => {
received.push(chunk);
});
channel.write(allByteValues);
expect(received).toHaveLength(1);
// A byte value of zero must survive. A UTF-8 string channel loses it: a
// JavaScript string can hold the code point U+0000, but a C-style consumer
// downstream of a string channel often treats it as a terminator.
expect(received[0]).toEqual(allByteValues);
expect(Array.from(received[0] ?? [])).toEqual(Array.from({ length: 256 }, (_, value) => value));
});
});