Files
paperclip/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts
T
Devin Foley 5a64cf52a1 Add exe.dev sandbox provider plugin (#5688)
> _Stacked on top of #5685#5686#5687. Diff against master includes
commits from earlier PRs in the stack — review focuses on the two new
commits (`Add long-secret textarea variant to JsonSchemaForm
SecretField` + `Add exe.dev sandbox provider plugin`)._

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Each agent runs in a sandbox environment, and operators choose the
provider — today E2B, Daytona, and (in this stack) Cloudflare
> - exe.dev offers per-VM sandboxes via a small CLI / HTTP API — useful
for operators who want full Linux VMs (vs container/runtime-only
sandboxes)
> - The plugin shape mirrors the e2b plugin: lifecycle hooks (`new`,
`ls`, `rm`) drive exe.dev's CLI; SSH plumbing handles direct VM access
for adapters that need it
> - exe.dev VMs come up bare — `node` is not preinstalled, so the
Paperclip sandbox callback bridge (a Node script) needs Node 20
installed at VM init via `--setup-script`. The plugin defaults the setup
script to a Nodesource install
> - The auth field accepts long SSH private keys, which need a textarea
variant of the existing `SecretField` in `JsonSchemaForm` — added behind
a `maxLength > THRESHOLD` opt-in so other secret fields are unaffected
> - The benefit is that operators get exe.dev as a fully working sandbox
provider out of the box, with no manual VM provisioning required

## What Changed

**Shared UI support (`Add long-secret textarea variant to JsonSchemaForm
SecretField`):**

- `ui/src/components/JsonSchemaForm.tsx` + new
`JsonSchemaForm.test.tsx`: when a secret-formatted field declares
`maxLength` larger than the existing single-line threshold, render a
monospace textarea instead of the masked input. Short secrets (API keys,
tokens) keep the existing masked-input + show/hide toggle behavior.

**The exe.dev plugin (`Add exe.dev sandbox provider plugin`):**

- `packages/plugins/sandbox-providers/exe-dev/`: plugin entry, manifest,
plugin runtime, README, and 19-test Vitest suite.
- Manifest fields: API token (with `secret-ref` + `/exec` permission
notes — needs `new`, `ls`, `rm`), API URL override, optional SSH
username, optional SSH private key (uses the new `JsonSchemaForm`
textarea variant via `maxLength: 4096`), optional SSH identity-file
path, optional setup script.
- Default `--setup-script` is a Nodesource Node 20 install. exe.dev VMs
come up bare and the Paperclip sandbox callback bridge is a Node script,
so without Node preinstalled the bridge can't start. Operators can
override by supplying their own setup script.
- `runLifecycleCommand` redacts env values from the executed command
before surfacing it in error messages, so secrets passed via
`--env=KEY=VALUE` don't leak into operator-visible failures.
- The plugin distinguishes exe.dev's SSH onboarding failures (`Please
complete registration by running: ssh exe.dev`) from general SSH
failures and surfaces a clear remediation message.
- `scripts/release-package-manifest.json`: register the new plugin for
CI publish alongside the existing daytona / e2b providers.

## Verification

- `pnpm typecheck`
- `pnpm exec vitest run --no-coverage
ui/src/components/JsonSchemaForm.test.tsx`
- `(cd packages/plugins/sandbox-providers/exe-dev && pnpm test)` — 19
passing

For an operator-side smoke test:

1. Get an exe.dev API token with `/exec` permission for `new`, `ls`,
`rm`.
2. Register the plugin in your Paperclip instance, configure an
environment with the token.
3. Create a sandbox env whose provider is `exe-dev`, then run a Codex or
Claude job against it. The default Node 20 setup script should bring the
VM up automatically.

## Risks

- Adds a new sandbox provider plugin that follows the existing daytona /
e2b shape; behavior on existing providers is unchanged.
- The `JsonSchemaForm` textarea variant only engages for fields that opt
in via `maxLength` larger than the existing threshold. All existing
secret fields (which don't declare a `maxLength`) keep their current
rendering. Test coverage pins both paths.
- The redaction in `runLifecycleCommand` is a defense-in-depth measure;
the test suite exercises the redaction path. If the redaction misses a
future env-arg shape, the worst case is restored behavior (secrets in
error messages), which is what the existing daytona / e2b plugins also
do today.
- Default setup script downloads from `deb.nodesource.com` over HTTPS at
VM init. Operators on air-gapped networks or with a different package
strategy can override the setup script.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (1M context)
- Capabilities used: extended reasoning, tool use (Read/Edit/Bash/Grep)

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — UI change is a textarea variant of an existing secret
field; will attach screenshots before requesting merge
- [x] I have updated relevant documentation to reflect my changes
(plugin README, manifest descriptions)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-11 07:42:18 -07:00

698 lines
21 KiB
TypeScript

import { EventEmitter } from "node:events";
import { beforeEach, describe, expect, it, vi } from "vitest";
const fetchMock = vi.fn();
const spawnMock = vi.hoisted(() => vi.fn());
vi.stubGlobal("fetch", fetchMock);
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
spawn: spawnMock,
};
});
import plugin from "./plugin.js";
class MockChildProcess extends EventEmitter {
stdout = new EventEmitter();
stderr = new EventEmitter();
stdin = {
written: "" as string,
ended: false,
write: (chunk: string) => {
this.stdin.written += chunk;
return true;
},
end: () => {
this.stdin.ended = true;
},
};
kill = vi.fn();
constructor(input: { code?: number; signal?: string | null; stdout?: string; stderr?: string }) {
super();
queueMicrotask(() => {
if (input.stdout) this.stdout.emit("data", input.stdout);
if (input.stderr) this.stderr.emit("data", input.stderr);
this.emit("close", input.code ?? 0, input.signal ?? null);
});
}
}
function queueSpawnResult(input: { code?: number; signal?: string | null; stdout?: string; stderr?: string }) {
spawnMock.mockImplementationOnce(() => new MockChildProcess(input));
}
describe("exe.dev sandbox provider plugin", () => {
beforeEach(() => {
fetchMock.mockReset();
spawnMock.mockReset();
delete process.env.EXE_API_KEY;
});
it("declares environment lifecycle handlers", async () => {
expect(await plugin.definition.onHealth?.()).toEqual({
status: "ok",
message: "exe.dev sandbox provider plugin healthy",
});
expect(plugin.definition.onEnvironmentAcquireLease).toBeTypeOf("function");
expect(plugin.definition.onEnvironmentExecute).toBeTypeOf("function");
});
it("normalizes config and emits SSH guidance warnings", async () => {
process.env.EXE_API_KEY = "host-key";
const result = await plugin.definition.onEnvironmentValidateConfig?.({
driverKey: "exe-dev",
config: {
apiUrl: "https://exe.dev",
namePrefix: " Paperclip Sandbox ",
image: " ubuntu:22.04 ",
cpu: "4.8",
memory: " 8GB ",
disk: " 50GB ",
env: {
FOO: " bar ",
},
integrations: [" github "],
tags: "prod, sandbox",
timeoutMs: "450000.9",
reuseLease: true,
sshPort: "2222",
},
});
expect(result).toEqual({
ok: true,
warnings: [
"The Paperclip host must have SSH access to the created exe.dev VM, and its SSH key must be registered with exe.dev. The API token only covers provisioning.",
"reuseLease keeps the VM alive between runs; this provider does not suspend retained VMs.",
],
normalizedConfig: {
apiKey: null,
apiUrl: "https://exe.dev/exec",
namePrefix: "paperclip-sandbox",
image: "ubuntu:22.04",
command: null,
cpu: 4,
memory: "8GB",
disk: "50GB",
comment: null,
env: { FOO: "bar" },
integrations: ["github"],
tags: ["prod", "sandbox"],
setupScript: null,
prompt: null,
timeoutMs: 450000,
reuseLease: true,
sshUser: null,
sshPrivateKey: null,
sshIdentityFile: null,
sshPort: 2222,
strictHostKeyChecking: "accept-new",
},
});
});
it("normalizes trailing /exec apiUrl inputs without duplication", async () => {
process.env.EXE_API_KEY = "host-key";
const result = await plugin.definition.onEnvironmentValidateConfig?.({
driverKey: "exe-dev",
config: {
apiUrl: "https://exe.dev/exec/",
},
});
expect(result).toMatchObject({
ok: true,
normalizedConfig: {
apiUrl: "https://exe.dev/exec",
},
});
});
it("rejects invalid config", async () => {
await expect(plugin.definition.onEnvironmentValidateConfig?.({
driverKey: "exe-dev",
config: {
apiUrl: "not-a-url",
cpu: 0,
env: {
"BAD-KEY": "value",
},
sshPort: 70000,
strictHostKeyChecking: "",
timeoutMs: 0,
},
})).resolves.toEqual({
ok: false,
warnings: [
"The Paperclip host must have SSH access to the created exe.dev VM, and its SSH key must be registered with exe.dev. The API token only covers provisioning.",
],
errors: [
"apiUrl must be a valid URL.",
"timeoutMs must be between 1 and 86400000.",
"cpu must be greater than 0 when provided.",
"sshPort must be between 1 and 65535.",
"exe.dev environments require an API key in config or EXE_API_KEY.",
"env contains an invalid key: BAD-KEY",
"strictHostKeyChecking cannot be empty.",
],
});
});
it("acquires a lease by creating a VM and preparing the SSH workspace", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-env-run",
ssh_dest: "paperclip-env-run.exe.xyz",
https_url: "https://paperclip-env-run.exe.xyz",
status: "running",
}), { status: 200 }),
);
queueSpawnResult({ stdout: "/home/exe\nbash\n" });
queueSpawnResult({});
const lease = await plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
requestedCwd: "/workspace/custom",
config: {
apiKey: "api-key",
namePrefix: "paperclip",
image: "ubuntu:22.04",
timeoutMs: 300000,
},
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(String(fetchMock.mock.calls[0]?.[1]?.body ?? "")).toContain("new --json --no-email");
expect(spawnMock).toHaveBeenCalledTimes(2);
expect(lease).toMatchObject({
providerLeaseId: "paperclip-env-run",
metadata: {
provider: "exe-dev",
vmName: "paperclip-env-run",
sshDest: "paperclip-env-run.exe.xyz",
remoteCwd: "/workspace/custom",
shellCommand: "bash",
reuseLease: false,
},
});
});
it("uses a pasted sshPrivateKey when connecting to the VM", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-env-run",
ssh_dest: "paperclip-env-run.exe.xyz",
https_url: "https://paperclip-env-run.exe.xyz",
status: "running",
}), { status: 200 }),
);
queueSpawnResult({ stdout: "/home/exe\nbash\n" });
queueSpawnResult({});
await plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
sshPrivateKey: "-----BEGIN PRIVATE KEY-----\npretend\n-----END PRIVATE KEY-----",
},
});
const firstSpawnArgs = spawnMock.mock.calls[0]?.[1] as string[] | undefined;
expect(firstSpawnArgs).toContain("-i");
expect(firstSpawnArgs).toContain("-o");
expect(firstSpawnArgs).toContain("IdentitiesOnly=yes");
});
it("supplies a default Node-install setup script when none is provided", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-env-run",
ssh_dest: "paperclip-env-run.exe.xyz",
https_url: "https://paperclip-env-run.exe.xyz",
status: "running",
}), { status: 200 }),
);
queueSpawnResult({ stdout: "/home/exedev\nbash\n" });
queueSpawnResult({});
await plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
},
});
const body = String(fetchMock.mock.calls[0]?.[1]?.body ?? "");
expect(body).toContain("--setup-script=");
expect(body).toContain("nodesource.com/setup_20.x");
expect(body).toContain("sudo apt-get install -y nodejs");
});
it("preserves an operator-supplied setup script and does not append the default", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-env-run",
ssh_dest: "paperclip-env-run.exe.xyz",
https_url: "https://paperclip-env-run.exe.xyz",
status: "running",
}), { status: 200 }),
);
queueSpawnResult({ stdout: "/home/exedev\nbash\n" });
queueSpawnResult({});
await plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
setupScript: "echo custom",
},
});
const body = String(fetchMock.mock.calls[0]?.[1]?.body ?? "");
expect(body).toContain("--setup-script='echo custom'");
expect(body).not.toContain("nodesource.com");
});
it("does not redact the built-in default setup script in API errors", async () => {
fetchMock.mockResolvedValueOnce(new Response("upstream boom", { status: 500 }));
const acquirePromise = plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
},
});
await expect(acquirePromise).rejects.toMatchObject({
name: "ExeDevApiError",
status: 500,
});
await acquirePromise?.catch((error: Error) => {
// Operator did not supply a setupScript, so the visible default install
// is not a secret and stays in the error for debuggability.
expect(error.message).toContain("nodesource.com/setup_20.x");
expect(error.message).not.toContain("[REDACTED]");
});
});
it("surfaces exe.dev SSH onboarding guidance during lease acquisition", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-env-run",
ssh_dest: "paperclip-env-run.exe.xyz",
https_url: "https://paperclip-env-run.exe.xyz",
status: "running",
}), { status: 200 }),
);
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
queueSpawnResult({ code: 1, stdout: "Please complete registration by running: ssh exe.dev\n" });
await expect(plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
})).rejects.toThrow(
"the Paperclip host SSH key is not registered with exe.dev",
);
expect(String(fetchMock.mock.calls[1]?.[1]?.body ?? "")).toBe("rm --json 'paperclip-env-run'");
});
it("redacts sensitive lifecycle flags in API errors", async () => {
fetchMock.mockResolvedValueOnce(new Response("upstream boom", { status: 500 }));
const acquirePromise = plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
apiKey: "api-key",
env: {
SECRET: "super-secret",
},
prompt: "build me a secret app",
setupScript: "export TOKEN=super-secret",
},
});
await expect(acquirePromise).rejects.toMatchObject({
name: "ExeDevApiError",
status: 500,
body: "upstream boom",
});
await acquirePromise?.catch((error: Error) => {
expect(error.message).toContain("--env='SECRET=[REDACTED]'");
expect(error.message).toContain("--prompt='[REDACTED]'");
expect(error.message).toContain("--setup-script='[REDACTED]'");
expect(error.message).not.toContain("super-secret");
});
});
it("returns an expired lease when the retained VM no longer exists", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ vms: [] }), { status: 200 }),
);
const lease = await plugin.definition.onEnvironmentResumeLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "missing-vm",
config: {
apiKey: "api-key",
},
leaseMetadata: {
sshDest: "missing-vm.exe.xyz",
},
});
expect(lease).toEqual({
providerLeaseId: null,
metadata: {
expired: true,
},
});
});
it("executes commands over SSH with cwd, env, and stdin", async () => {
queueSpawnResult({ code: 0, stdout: "hello\n", stderr: "" });
const result = await plugin.definition.onEnvironmentExecute?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
lease: {
providerLeaseId: "vm-1",
metadata: {
sshDest: "vm-1.exe.xyz",
},
},
command: "node",
args: ["-e", "process.stdout.write('hello\\n')"],
cwd: "/workspace",
env: {
FOO: "bar",
},
stdin: "input-body",
timeoutMs: 1000,
});
expect(spawnMock).toHaveBeenCalledTimes(1);
expect(spawnMock.mock.calls[0]?.[0]).toBe("ssh");
expect(String(spawnMock.mock.calls[0]?.[1]?.at(-1) ?? "")).toContain("/workspace");
expect(String(spawnMock.mock.calls[0]?.[1]?.at(-1) ?? "")).toContain("FOO='");
const child = spawnMock.mock.results[0]?.value as MockChildProcess;
expect(child.stdin.written).toBe("input-body");
expect(child.stdin.ended).toBe(true);
expect(result).toMatchObject({
exitCode: 0,
timedOut: false,
stdout: "hello\n",
stderr: "",
metadata: {
provider: "exe-dev",
vmName: "vm-1",
},
});
});
it("returns exe.dev SSH onboarding guidance for command execution failures", async () => {
queueSpawnResult({ code: 1, stdout: "Please complete registration by running: ssh exe.dev\n" });
const result = await plugin.definition.onEnvironmentExecute?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
lease: {
providerLeaseId: "vm-1",
metadata: {
sshDest: "vm-1.exe.xyz",
},
},
command: "node",
args: ["-v"],
});
expect(result?.exitCode).toBe(1);
expect(String(result?.stderr ?? "")).toContain("the Paperclip host SSH key is not registered with exe.dev");
expect(String(result?.stderr ?? "")).toContain("ssh exe.dev");
});
it("probes by creating and then deleting a VM after SSH verification", async () => {
fetchMock
.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-probe",
ssh_dest: "paperclip-probe.exe.xyz",
status: "running",
}), { status: 200 }),
)
.mockResolvedValueOnce(new Response("{}", { status: 200 }));
queueSpawnResult({ stdout: "/home/exe\nbash\n" });
queueSpawnResult({});
const result = await plugin.definition.onEnvironmentProbe?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
},
});
expect(result).toMatchObject({
ok: true,
summary: "Connected to exe.dev VM paperclip-probe.",
metadata: {
provider: "exe-dev",
vmName: "paperclip-probe",
sshDest: "paperclip-probe.exe.xyz",
shellCommand: "bash",
},
});
expect(String(fetchMock.mock.calls[1]?.[1]?.body ?? "")).toBe("rm --json 'paperclip-probe'");
});
it("cleans up the probe VM when SSH verification fails", async () => {
fetchMock
.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-probe",
ssh_dest: "paperclip-probe.exe.xyz",
status: "running",
}), { status: 200 }),
)
.mockResolvedValueOnce(new Response("{}", { status: 200 }));
queueSpawnResult({ code: 1, stderr: "permission denied" });
const result = await plugin.definition.onEnvironmentProbe?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
},
});
expect(result).toMatchObject({
ok: false,
summary: "exe.dev environment probe failed.",
metadata: {
provider: "exe-dev",
},
});
expect(String(result?.metadata?.error ?? "")).toContain("permission denied");
expect(String(fetchMock.mock.calls[1]?.[1]?.body ?? "")).toBe("rm --json 'paperclip-probe'");
});
it("returns onboarding guidance when probe hits exe.dev SSH registration", async () => {
fetchMock
.mockResolvedValueOnce(
new Response(JSON.stringify({
vm_name: "paperclip-probe",
ssh_dest: "paperclip-probe.exe.xyz",
status: "running",
}), { status: 200 }),
)
.mockResolvedValueOnce(new Response("{}", { status: 200 }));
queueSpawnResult({ code: 1, stdout: "Please complete registration by running: ssh exe.dev\n" });
const result = await plugin.definition.onEnvironmentProbe?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
},
});
expect(result).toMatchObject({
ok: false,
summary: "exe.dev environment probe failed.",
});
expect(String(result?.metadata?.error ?? "")).toContain("the Paperclip host SSH key is not registered with exe.dev");
expect(String(fetchMock.mock.calls[1]?.[1]?.body ?? "")).toBe("rm --json 'paperclip-probe'");
});
it("deletes non-reusable leases on release", async () => {
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
await plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "vm-1",
config: {
apiKey: "api-key",
reuseLease: false,
},
leaseMetadata: {},
});
expect(String(fetchMock.mock.calls[0]?.[1]?.body ?? "")).toBe("rm --json 'vm-1'");
});
it("destroys leases on demand", async () => {
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
await plugin.definition.onEnvironmentDestroyLease?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "vm-2",
config: {
apiKey: "api-key",
},
leaseMetadata: {},
});
expect(String(fetchMock.mock.calls[0]?.[1]?.body ?? "")).toBe("rm --json 'vm-2'");
});
it("realizes a workspace by mkdir-ing the remote cwd over SSH when VM metadata is present", async () => {
queueSpawnResult({ code: 0, stdout: "", stderr: "" });
const result = await plugin.definition.onEnvironmentRealizeWorkspace?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
lease: {
providerLeaseId: "vm-1",
metadata: {
sshDest: "vm-1.exe.xyz",
remoteCwd: "/srv/paperclip/run-1",
},
},
workspace: {
localPath: "/local/paperclip",
remotePath: undefined,
},
});
expect(spawnMock).toHaveBeenCalledTimes(1);
expect(spawnMock.mock.calls[0]?.[0]).toBe("ssh");
const sshCommand = String(spawnMock.mock.calls[0]?.[1]?.at(-1) ?? "");
expect(sshCommand).toContain("mkdir -p");
expect(sshCommand).toContain("/srv/paperclip/run-1");
expect(result).toMatchObject({
cwd: "/srv/paperclip/run-1",
metadata: {
provider: "exe-dev",
remoteCwd: "/srv/paperclip/run-1",
},
});
});
it("falls back through workspace.remotePath then workspace.localPath when lease.metadata.remoteCwd is missing", async () => {
queueSpawnResult({ code: 0, stdout: "", stderr: "" });
const result = await plugin.definition.onEnvironmentRealizeWorkspace?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
lease: {
providerLeaseId: "vm-1",
metadata: {
sshDest: "vm-1.exe.xyz",
},
},
workspace: {
localPath: "/local/paperclip",
remotePath: "/srv/paperclip/remote-fallback",
},
});
expect(result?.cwd).toBe("/srv/paperclip/remote-fallback");
});
it("skips ensureRemoteWorkspace and returns the resolved cwd when no VM metadata is available", async () => {
const result = await plugin.definition.onEnvironmentRealizeWorkspace?.({
driverKey: "exe-dev",
companyId: "company-1",
environmentId: "env-1",
config: {
apiKey: "api-key",
timeoutMs: 300000,
},
lease: {
providerLeaseId: null,
metadata: {
remoteCwd: "/srv/paperclip/no-vm",
},
},
workspace: {
localPath: "/local/paperclip",
},
});
expect(spawnMock).not.toHaveBeenCalled();
expect(result?.cwd).toBe("/srv/paperclip/no-vm");
});
});