paperclip/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts

698 lines
21 KiB
TypeScript
Raw Normal View History

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
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");
});
});