import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { execute } from "@penclipai/adapter-codebuddy-local/server"; async function writeFakeCodeBuddyCommand(root: string, scriptBody: string): Promise { if (process.platform === "win32") { const scriptPath = path.join(root, "codebuddy.js"); const commandPath = path.join(root, "codebuddy.cmd"); await fs.writeFile(scriptPath, scriptBody, "utf8"); await fs.writeFile(commandPath, `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`, "utf8"); return commandPath; } const commandPath = path.join(root, "codebuddy"); await fs.writeFile(commandPath, `#!/usr/bin/env node\n${scriptBody}`, "utf8"); await fs.chmod(commandPath, 0o755); return commandPath; } 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; } type CapturePayload = { argv: string[]; prompt: string; paperclipEnvKeys: string[]; agentHome: string | null; }; describe("codebuddy execute", () => { const cleanupDirs = new Set(); const previousEnv = new Map(); afterEach(async () => { for (const [key, value] of previousEnv.entries()) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } previousEnv.clear(); await Promise.all(Array.from(cleanupDirs).map((dir) => fs.rm(dir, { recursive: true, force: true }))); cleanupDirs.clear(); }); function swapEnv(key: string, value: string) { if (!previousEnv.has(key)) previousEnv.set(key, process.env[key]); process.env[key] = value; } it("injects Paperclip env vars, instructions, and runtime skills before execution", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codebuddy-execute-")); cleanupDirs.add(root); const workspace = path.join(root, "workspace"); const runtimeSkillsRoot = path.join(root, "runtime-skills"); const capturePath = path.join(root, "capture.json"); const instructionsPath = path.join(root, "instructions", "AGENTS.md"); const agentHome = path.join(root, "agent-home"); await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(path.dirname(instructionsPath), { recursive: true }); await fs.mkdir(agentHome, { recursive: true }); await fs.writeFile(instructionsPath, "# CEO\nRead HEARTBEAT.md from this directory.", "utf8"); const paperclipDir = await createSkillDir(runtimeSkillsRoot, "paperclip"); const asciiHeartDir = await createSkillDir(runtimeSkillsRoot, "ascii-heart"); const commandPath = await writeFakeCodeBuddyCommand( root, ` const fs = require("node:fs"); const args = process.argv.slice(2); if (args.includes("--help")) { console.log("Usage: codebuddy [options]\\n --model Select model. Currently supported: (glm-5.0, glm-4.7, kimi-k2.5)"); process.exit(0); } const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH; const payload = { argv: args, prompt: fs.readFileSync(0, "utf8"), paperclipEnvKeys: Object.keys(process.env).filter((key) => key.startsWith("PAPERCLIP_")).sort(), agentHome: process.env.AGENT_HOME ?? null, }; if (capturePath) { fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8"); } console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "codebuddy-session-1", model: "glm-5.0", })); console.log(JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "hello" }] }, })); console.log(JSON.stringify({ type: "result", subtype: "success", session_id: "codebuddy-session-1", usage: { input_tokens: 12, output_tokens: 4, cached_input_tokens: 2 }, total_cost_usd: 0.001, result: "done", })); `, ); swapEnv("HOME", root); let invocationPrompt = ""; const result = await execute({ runId: "run-1", agent: { id: "agent-1", companyId: "company-1", name: "CodeBuddy Agent", adapterType: "codebuddy_local", adapterConfig: {}, }, runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null, }, config: { command: commandPath, cwd: workspace, model: "glm-5.0", effort: "high", maxTurnsPerRun: 7, instructionsFilePath: instructionsPath, promptTemplate: "Continue issue {{context.taskId}} for {{agent.name}}.", bootstrapPromptTemplate: "Bootstrap {{agent.id}}.", env: { HOME: root, PAPERCLIP_TEST_CAPTURE_PATH: capturePath, }, paperclipRuntimeSkills: [ { key: "paperclip", runtimeName: "paperclip", source: paperclipDir, required: true, requiredReason: "Bundled Paperclip skills are always available for local adapters.", }, { key: "ascii-heart", runtimeName: "ascii-heart", source: asciiHeartDir, }, ], paperclipSkillSync: { desiredSkills: ["ascii-heart"], }, }, context: { taskId: "issue-123", paperclipSessionHandoffMarkdown: "Resume the active Paperclip task.", paperclipLocalizationPromptMarkdown: "Reply in the user's language.", paperclipWorkspace: { cwd: workspace, source: "workspace", workspaceId: "workspace-1", agentHome, }, }, authToken: "run-jwt-token", onLog: async () => {}, onMeta: async (meta) => { invocationPrompt = meta.prompt ?? ""; }, }); expect(result.exitCode).toBe(0); expect(result.errorMessage).toBeNull(); expect(result.sessionId).toBe("codebuddy-session-1"); expect(result.summary).toBe("hello"); expect(result.usage).toEqual({ inputTokens: 12, outputTokens: 4, cachedInputTokens: 2, }); const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; expect(capture.argv).toEqual( expect.arrayContaining(["-p", "--output-format", "stream-json", "--model", "glm-5.0", "--effort", "high", "--max-turns", "7", "-y"]), ); expect(capture.paperclipEnvKeys).toEqual( expect.arrayContaining([ "PAPERCLIP_AGENT_ID", "PAPERCLIP_API_KEY", "PAPERCLIP_API_URL", "PAPERCLIP_COMPANY_ID", "PAPERCLIP_RUN_ID", "PAPERCLIP_TASK_ID", ]), ); expect(capture.agentHome).toBe(agentHome); expect(capture.prompt).toContain("The above agent instructions were loaded from"); expect(capture.prompt).toContain(`Resolve any relative file references from ${path.dirname(instructionsPath)}/`); expect(capture.prompt).toContain("Bootstrap agent-1."); expect(capture.prompt).toContain("Resume the active Paperclip task."); expect(capture.prompt).toContain("Paperclip runtime note:"); expect(capture.prompt).toContain("Reply in the user's language."); expect(capture.prompt).toContain("Continue issue issue-123 for CodeBuddy Agent."); expect(capture.prompt.indexOf("Continue issue issue-123 for CodeBuddy Agent.")).toBeLessThan( capture.prompt.indexOf("Reply in the user's language."), ); expect(capture.prompt.trimEnd().endsWith("Reply in the user's language.")).toBe(true); expect(invocationPrompt).toContain("Bootstrap agent-1."); expect(invocationPrompt.trimEnd().endsWith("Reply in the user's language.")).toBe(true); expect(await fs.realpath(path.join(root, ".codebuddy", "skills", "ascii-heart"))).toBe( await fs.realpath(asciiHeartDir), ); }); it("retries without --resume when CodeBuddy reports a missing session", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codebuddy-execute-resume-")); cleanupDirs.add(root); const workspace = path.join(root, "workspace"); const attemptsPath = path.join(root, "attempts.jsonl"); await fs.mkdir(workspace, { recursive: true }); const commandPath = await writeFakeCodeBuddyCommand( root, ` const fs = require("node:fs"); const args = process.argv.slice(2); if (args.includes("--help")) { console.log("Usage: codebuddy [options]\\n --model Select model. Currently supported: (glm-5.0, glm-4.7)"); process.exit(0); } const attemptsPath = process.env.PAPERCLIP_TEST_ATTEMPTS_PATH; if (attemptsPath) { fs.appendFileSync(attemptsPath, JSON.stringify(args) + "\\n", "utf8"); } if (args.includes("--resume")) { console.log(JSON.stringify({ type: "error", message: "No conversation found with session ID: stale-session", })); process.exit(0); } console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "fresh-session", model: "glm-5.0", })); console.log(JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "recovered" }] }, })); console.log(JSON.stringify({ type: "result", subtype: "success", session_id: "fresh-session", result: "ok", })); `, ); swapEnv("HOME", root); const result = await execute({ runId: "run-2", agent: { id: "agent-2", companyId: "company-1", name: "CodeBuddy Agent", adapterType: "codebuddy_local", adapterConfig: {}, }, runtime: { sessionId: "stale-session", sessionParams: { sessionId: "stale-session", cwd: workspace, }, sessionDisplayId: "stale-session", taskKey: null, }, config: { command: commandPath, cwd: workspace, model: "glm-5.0", env: { HOME: root, PAPERCLIP_TEST_ATTEMPTS_PATH: attemptsPath, }, }, context: {}, onLog: async () => {}, }); expect(result.exitCode).toBe(0); expect(result.errorMessage).toBeNull(); expect(result.clearSession).toBe(true); expect(result.sessionId).toBe("fresh-session"); const attempts = (await fs.readFile(attemptsPath, "utf8")) .trim() .split(/\r?\n/) .map((line) => JSON.parse(line) as string[]); expect(attempts).toHaveLength(2); expect(attempts[0]).toEqual(expect.arrayContaining(["--resume", "stale-session"])); expect(attempts[1]).not.toContain("--resume"); }); });