paperclip/packages/adapters/cursor-local/src/server/remote-command.test.ts
Devin Foley 0fe39a2d5c
fix(cursor-local): resolve sandbox agent installs from cursor bin (#5686)
> _Stacked on top of #5685 (Harden remote sandbox runtime). Diff against
master includes commits from earlier PRs in the stack — review focuses
on the new commit only._

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The cursor-local adapter wraps the Cursor Agent CLI so a Paperclip
workflow can drive it inside a sandbox
> - When the adapter runs in a remote sandbox, the Cursor Agent CLI
installs under `$HOME/.local/bin/cursor-agent` (or wherever
`$XDG_BIN_HOME` points), not on the global PATH
> - The existing post-install resolution assumed `cursor-agent` would
resolve via the sandbox's login shell PATH after `npm install -g`, which
fails on sandboxes where the install lands in a user-prefixed directory
that isn't on PATH at probe time
> - This pull request resolves the agent CLI from the cursor binary's
own directory (`dirname "$(command -v cursor)"`) so the install probe
and execute path agree on a real binary location
> - The benefit is that cursor-local works correctly on any sandbox
provider where `npm install` lands in a user-prefixed directory

## What Changed

- `packages/adapters/cursor-local/src/server/remote-command.ts`: resolve
the cursor-agent binary from the cursor bin directory after install,
instead of relying on PATH.
- `packages/adapters/cursor-local/src/server/test.ts`: corresponding
probe tweak.
- `packages/adapters/cursor-local/src/server/test.test.ts` (new) +
`remote-command.test.ts`: focused coverage that exercises the install +
resolve path against a sandbox runner that places the binary in a
user-prefixed directory.

## Verification

- `pnpm exec vitest run --no-coverage
packages/adapters/cursor-local/src/server/test.test.ts
packages/adapters/cursor-local/src/server/remote-command.test.ts
packages/adapters/cursor-local/src/server/execute.test.ts`

All passing locally.

## Risks

- Local cursor-local runs are unaffected — the resolution change only
kicks in for the sandbox install path.
- Low risk; isolated to one adapter.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (1M context)
- Capabilities used: tool use (Read/Edit/Bash), no code execution beyond
local repo commands

## 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 — N/A, no UI change
- [x] I have updated relevant documentation to reflect my changes
- [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 00:41:20 -07:00

137 lines
5.1 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { runChildProcess } from "@paperclipai/adapter-utils/server-utils";
import { prepareCursorSandboxCommand } from "./remote-command.js";
function createLocalSandboxRunner() {
let counter = 0;
return {
execute: async (input: {
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
stdin?: string;
timeoutMs?: number;
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
}) => {
counter += 1;
return await runChildProcess(`cursor-remote-command-${counter}`, input.command, input.args ?? [], {
cwd: input.cwd ?? process.cwd(),
env: input.env ?? {},
stdin: input.stdin,
timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)),
graceSec: 5,
onLog: input.onLog ?? (async () => {}),
onSpawn: input.onSpawn
? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt })
: undefined,
});
},
};
}
async function writeFakeAgent(commandPath: string): Promise<void> {
const script = `#!/bin/sh
printf '%s\\n' ok
`;
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, script, "utf8");
await fs.chmod(commandPath, 0o755);
}
describe("prepareCursorSandboxCommand", () => {
it("prefers the Cursor installer bin directory when the default agent entrypoint is installed there", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-remote-command-cursor-bin-"));
const systemHomeDir = path.join(root, "system-home");
const managedHomeDir = path.join(root, "managed-home");
const remoteWorkspace = path.join(root, "workspace");
const cursorAgentPath = path.join(systemHomeDir, ".cursor", "bin", "agent");
await fs.mkdir(remoteWorkspace, { recursive: true });
await writeFakeAgent(cursorAgentPath);
try {
const result = await prepareCursorSandboxCommand({
runId: "run-remote-command-cursor-bin",
target: {
kind: "remote",
transport: "sandbox",
shellCommand: "bash",
remoteCwd: remoteWorkspace,
runner: createLocalSandboxRunner(),
timeoutMs: 30_000,
},
command: "agent",
cwd: remoteWorkspace,
env: {
HOME: managedHomeDir,
PATH: "/usr/bin:/bin",
},
remoteSystemHomeDirHint: systemHomeDir,
timeoutSec: 30,
graceSec: 5,
});
expect(result.command).toBe(cursorAgentPath);
expect(result.preferredCommandPath).toBe(cursorAgentPath);
expect(result.remoteSystemHomeDir).toBe(systemHomeDir);
expect(result.addedPathEntry).toBe(path.join(systemHomeDir, ".local", "bin"));
expect(result.env.PATH?.split(":").slice(0, 2)).toEqual([
path.join(systemHomeDir, ".local", "bin"),
path.join(systemHomeDir, ".cursor", "bin"),
]);
expect(result.env.PATH).not.toContain(path.join(managedHomeDir, ".cursor", "bin"));
expect(result.env.PATH).not.toContain(path.join(managedHomeDir, ".local", "bin"));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("keeps probing the original sandbox home after managed HOME overrides", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-remote-command-"));
const systemHomeDir = path.join(root, "system-home");
const managedHomeDir = path.join(root, "managed-home");
const remoteWorkspace = path.join(root, "workspace");
const systemAgentPath = path.join(systemHomeDir, ".local", "bin", "agent");
await fs.mkdir(remoteWorkspace, { recursive: true });
await writeFakeAgent(systemAgentPath);
try {
const result = await prepareCursorSandboxCommand({
runId: "run-remote-command-1",
target: {
kind: "remote",
transport: "sandbox",
shellCommand: "bash",
remoteCwd: remoteWorkspace,
runner: createLocalSandboxRunner(),
timeoutMs: 30_000,
},
command: "agent",
cwd: remoteWorkspace,
env: {
HOME: managedHomeDir,
PATH: "/usr/bin:/bin",
},
remoteSystemHomeDirHint: systemHomeDir,
timeoutSec: 30,
graceSec: 5,
});
expect(result.command).toBe(systemAgentPath);
expect(result.preferredCommandPath).toBe(systemAgentPath);
expect(result.remoteSystemHomeDir).toBe(systemHomeDir);
expect(result.addedPathEntry).toBe(path.join(systemHomeDir, ".local", "bin"));
expect(result.env.PATH?.split(":").slice(0, 2)).toEqual([
path.join(systemHomeDir, ".local", "bin"),
path.join(systemHomeDir, ".cursor", "bin"),
]);
expect(result.env.PATH).not.toContain(path.join(managedHomeDir, ".local", "bin"));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});