mirror of
https://github.com/paperclipai/paperclip
synced 2026-03-25 11:21:48 +00:00
263 lines
8.0 KiB
TypeScript
263 lines
8.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { execute } from "@paperclipai/adapter-cursor-local/server";
|
|
|
|
async function writeFakeCursorCommand(commandPath: string): Promise<void> {
|
|
const script = `#!/usr/bin/env node
|
|
const fs = require("node:fs");
|
|
|
|
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
|
const payload = {
|
|
argv: process.argv.slice(2),
|
|
prompt: fs.readFileSync(0, "utf8"),
|
|
paperclipEnvKeys: Object.keys(process.env)
|
|
.filter((key) => key.startsWith("PAPERCLIP_"))
|
|
.sort(),
|
|
};
|
|
if (capturePath) {
|
|
fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8");
|
|
}
|
|
console.log(JSON.stringify({
|
|
type: "system",
|
|
subtype: "init",
|
|
session_id: "cursor-session-1",
|
|
model: "auto",
|
|
}));
|
|
console.log(JSON.stringify({
|
|
type: "assistant",
|
|
message: { content: [{ type: "output_text", text: "hello" }] },
|
|
}));
|
|
console.log(JSON.stringify({
|
|
type: "result",
|
|
subtype: "success",
|
|
session_id: "cursor-session-1",
|
|
result: "ok",
|
|
}));
|
|
`;
|
|
await fs.writeFile(commandPath, script, "utf8");
|
|
await fs.chmod(commandPath, 0o755);
|
|
}
|
|
|
|
type CapturePayload = {
|
|
argv: string[];
|
|
prompt: string;
|
|
paperclipEnvKeys: string[];
|
|
};
|
|
|
|
async function createSkillDir(root: string, name: string) {
|
|
const skillDir = path.join(root, name);
|
|
await fs.mkdir(skillDir, { recursive: true });
|
|
await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n`, "utf8");
|
|
return skillDir;
|
|
}
|
|
|
|
describe("cursor execute", () => {
|
|
it("injects paperclip env vars and prompt note by default", async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-execute-"));
|
|
const workspace = path.join(root, "workspace");
|
|
const commandPath = path.join(root, "agent");
|
|
const capturePath = path.join(root, "capture.json");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await writeFakeCursorCommand(commandPath);
|
|
|
|
const previousHome = process.env.HOME;
|
|
process.env.HOME = root;
|
|
|
|
let invocationPrompt = "";
|
|
try {
|
|
const result = await execute({
|
|
runId: "run-1",
|
|
agent: {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Cursor Coder",
|
|
adapterType: "cursor",
|
|
adapterConfig: {},
|
|
},
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionParams: null,
|
|
sessionDisplayId: null,
|
|
taskKey: null,
|
|
},
|
|
config: {
|
|
command: commandPath,
|
|
cwd: workspace,
|
|
model: "auto",
|
|
env: {
|
|
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
|
},
|
|
promptTemplate: "Follow the paperclip heartbeat.",
|
|
},
|
|
context: {},
|
|
authToken: "run-jwt-token",
|
|
onLog: async () => {},
|
|
onMeta: async (meta) => {
|
|
invocationPrompt = meta.prompt ?? "";
|
|
},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.errorMessage).toBeNull();
|
|
|
|
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
|
expect(capture.argv).not.toContain("Follow the paperclip heartbeat.");
|
|
expect(capture.argv).not.toContain("--mode");
|
|
expect(capture.argv).not.toContain("ask");
|
|
expect(capture.paperclipEnvKeys).toEqual(
|
|
expect.arrayContaining([
|
|
"PAPERCLIP_AGENT_ID",
|
|
"PAPERCLIP_API_KEY",
|
|
"PAPERCLIP_API_URL",
|
|
"PAPERCLIP_COMPANY_ID",
|
|
"PAPERCLIP_RUN_ID",
|
|
]),
|
|
);
|
|
expect(capture.prompt).toContain("Paperclip runtime note:");
|
|
expect(capture.prompt).toContain("PAPERCLIP_API_KEY");
|
|
expect(invocationPrompt).toContain("Paperclip runtime note:");
|
|
expect(invocationPrompt).toContain("PAPERCLIP_API_URL");
|
|
} finally {
|
|
if (previousHome === undefined) {
|
|
delete process.env.HOME;
|
|
} else {
|
|
process.env.HOME = previousHome;
|
|
}
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("passes --mode when explicitly configured", async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-execute-mode-"));
|
|
const workspace = path.join(root, "workspace");
|
|
const commandPath = path.join(root, "agent");
|
|
const capturePath = path.join(root, "capture.json");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await writeFakeCursorCommand(commandPath);
|
|
|
|
const previousHome = process.env.HOME;
|
|
process.env.HOME = root;
|
|
|
|
try {
|
|
const result = await execute({
|
|
runId: "run-2",
|
|
agent: {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Cursor Coder",
|
|
adapterType: "cursor",
|
|
adapterConfig: {},
|
|
},
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionParams: null,
|
|
sessionDisplayId: null,
|
|
taskKey: null,
|
|
},
|
|
config: {
|
|
command: commandPath,
|
|
cwd: workspace,
|
|
model: "auto",
|
|
mode: "ask",
|
|
env: {
|
|
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
|
},
|
|
promptTemplate: "Follow the paperclip heartbeat.",
|
|
},
|
|
context: {},
|
|
authToken: "run-jwt-token",
|
|
onLog: async () => {},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.errorMessage).toBeNull();
|
|
|
|
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
|
expect(capture.argv).toContain("--mode");
|
|
expect(capture.argv).toContain("ask");
|
|
} finally {
|
|
if (previousHome === undefined) {
|
|
delete process.env.HOME;
|
|
} else {
|
|
process.env.HOME = previousHome;
|
|
}
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("injects company-library runtime skills into the Cursor skills home before execution", async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-execute-runtime-skill-"));
|
|
const workspace = path.join(root, "workspace");
|
|
const commandPath = path.join(root, "agent");
|
|
const runtimeSkillsRoot = path.join(root, "runtime-skills");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await writeFakeCursorCommand(commandPath);
|
|
|
|
const paperclipDir = await createSkillDir(runtimeSkillsRoot, "paperclip");
|
|
const asciiHeartDir = await createSkillDir(runtimeSkillsRoot, "ascii-heart");
|
|
|
|
const previousHome = process.env.HOME;
|
|
process.env.HOME = root;
|
|
|
|
try {
|
|
const result = await execute({
|
|
runId: "run-3",
|
|
agent: {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Cursor Coder",
|
|
adapterType: "cursor",
|
|
adapterConfig: {},
|
|
},
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionParams: null,
|
|
sessionDisplayId: null,
|
|
taskKey: null,
|
|
},
|
|
config: {
|
|
command: commandPath,
|
|
cwd: workspace,
|
|
model: "auto",
|
|
paperclipRuntimeSkills: [
|
|
{
|
|
name: "paperclip",
|
|
source: paperclipDir,
|
|
required: true,
|
|
requiredReason: "Bundled Paperclip skills are always available for local adapters.",
|
|
},
|
|
{
|
|
name: "ascii-heart",
|
|
source: asciiHeartDir,
|
|
},
|
|
],
|
|
paperclipSkillSync: {
|
|
desiredSkills: ["ascii-heart"],
|
|
},
|
|
promptTemplate: "Follow the paperclip heartbeat.",
|
|
},
|
|
context: {},
|
|
authToken: "run-jwt-token",
|
|
onLog: async () => {},
|
|
onMeta: async () => {},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.errorMessage).toBeNull();
|
|
expect((await fs.lstat(path.join(root, ".cursor", "skills", "ascii-heart"))).isSymbolicLink()).toBe(true);
|
|
expect(await fs.realpath(path.join(root, ".cursor", "skills", "ascii-heart"))).toBe(
|
|
await fs.realpath(asciiHeartDir),
|
|
);
|
|
} finally {
|
|
if (previousHome === undefined) {
|
|
delete process.env.HOME;
|
|
} else {
|
|
process.env.HOME = previousHome;
|
|
}
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|