Files
PaperClipAI/cli/src/__tests__/agent-lifecycle.test.ts
T
DottaandPaperclip aa879604ae feat: default new Codex agents to the qualified Paperclip Runner
Resolve creation intent across onboarding, hires, imports, and the UI. Preserve existing execution and qualify automatic defaults for shipped Linux x64 artifacts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-09 10:12:15 -05:00

138 lines
6.9 KiB
TypeScript

import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { registerAgentCommands } from "../commands/client/agent.js";
const COMPANY_ID = "22222222-2222-4222-8222-222222222222";
const AGENT_ID = "11111111-1111-4111-8111-111111111111";
const REVISION_ID = "33333333-3333-4333-8333-333333333333";
function createProgram(): Command {
const program = new Command();
program.exitOverride();
program.configureOutput({
writeOut: () => {},
writeErr: () => {},
});
registerAgentCommands(program);
return program;
}
async function run(args: string[]): Promise<void> {
await createProgram().parseAsync([
...args,
"--api-base", "http://localhost:3100",
"--api-key", "board-token",
], { from: "user" });
}
describe("agent lifecycle commands", () => {
beforeEach(() => {
vi.restoreAllMocks();
delete process.env.PAPERCLIP_API_KEY;
delete process.env.PAPERCLIP_API_URL;
vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("wraps agent lifecycle and state endpoints", async () => {
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
vi.stubGlobal("fetch", fetchMock);
await run([
"agent", "create",
"--company-id", COMPANY_ID,
"--payload-json", JSON.stringify({ name: "Builder", adapterType: "codex_local" }),
]);
await run(["agent", "hire", "--company-id", COMPANY_ID, "--payload-json", "{}"]);
await run(["agent", "update", AGENT_ID, "--payload-json", JSON.stringify({ title: "Senior Builder" })]);
await run(["agent", "pause", AGENT_ID]);
await run(["agent", "resume", AGENT_ID]);
await run(["agent", "approve", AGENT_ID]);
await run(["agent", "terminate", AGENT_ID]);
await run(["agent", "heartbeat:invoke", AGENT_ID]);
await run(["agent", "claude-login", AGENT_ID]);
await run(["agent", "delete", AGENT_ID, "--yes"]);
expect(fetchMock.mock.calls.map((call) => [call[1]?.method ?? "GET", call[0]])).toEqual([
["POST", `http://localhost:3100/api/companies/${COMPANY_ID}/agents`],
["POST", `http://localhost:3100/api/companies/${COMPANY_ID}/agent-hires`],
["PATCH", `http://localhost:3100/api/agents/${AGENT_ID}`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/pause`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/resume`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/approve`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/terminate`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/heartbeat/invoke`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/claude-login`],
["DELETE", `http://localhost:3100/api/agents/${AGENT_ID}`],
]);
});
it("sends automatic selection when omitted and supports explicit runner overrides", async () => {
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
vi.stubGlobal("fetch", fetchMock);
const payload = { name: "Builder", adapterType: "codex_local" };
await run(["agent", "create", "--company-id", COMPANY_ID, "--payload-json", JSON.stringify(payload)]);
await run(["agent", "create", "--company-id", COMPANY_ID, "--payload-json", JSON.stringify({ ...payload, runner: "auto" }), "--runner", "legacy"]);
await run(["agent", "hire", "--company-id", COMPANY_ID, "--payload-json", JSON.stringify(payload), "--runner", "paperclip"]);
expect(JSON.parse(fetchMock.mock.calls[0]![1].body).runner).toBeUndefined();
expect(JSON.parse(fetchMock.mock.calls[1]![1].body).runner).toBe("legacy");
expect(JSON.parse(fetchMock.mock.calls[2]![1].body).runner).toBe("paperclip");
});
it("wraps configuration, runtime, skills, and instructions endpoints", async () => {
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
vi.stubGlobal("fetch", fetchMock);
await run(["agent", "permissions:update", AGENT_ID, "--payload-json", JSON.stringify({ canCreateAgents: true, canAssignTasks: true })]);
await run(["agent", "configuration", AGENT_ID]);
await run(["agent", "config-revisions", AGENT_ID]);
await run(["agent", "config-revision:get", AGENT_ID, REVISION_ID]);
await run(["agent", "config-revision:rollback", AGENT_ID, REVISION_ID]);
await run(["agent", "runtime-state", AGENT_ID]);
await run(["agent", "runtime-state:reset-session", AGENT_ID, "--task-key", "task-1"]);
await run(["agent", "task-sessions", AGENT_ID]);
await run(["agent", "skills", AGENT_ID]);
await run([
"agent",
"skills:sync",
AGENT_ID,
"--desired-skills",
"paperclip,github",
"--mode",
"replace",
]);
await run(["agent", "instructions-path:update", AGENT_ID, "--payload-json", JSON.stringify({ path: "/tmp/AGENTS.md" })]);
await run(["agent", "instructions-bundle", AGENT_ID]);
await run(["agent", "instructions-bundle:update", AGENT_ID, "--payload-json", JSON.stringify({ mode: "managed" })]);
await run(["agent", "instructions-file:get", AGENT_ID, "--path", "AGENTS.md"]);
await run(["agent", "instructions-file:put", AGENT_ID, "--path", "AGENTS.md", "--content", "hello"]);
await run(["agent", "instructions-file:delete", AGENT_ID, "--path", "AGENTS.md"]);
expect(fetchMock.mock.calls.map((call) => [call[1]?.method ?? "GET", call[0]])).toEqual([
["PATCH", `http://localhost:3100/api/agents/${AGENT_ID}/permissions`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/configuration`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/config-revisions`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/config-revisions/${REVISION_ID}`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/config-revisions/${REVISION_ID}/rollback`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/runtime-state`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/runtime-state/reset-session`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/task-sessions`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/skills`],
["POST", `http://localhost:3100/api/agents/${AGENT_ID}/skills/sync`],
["PATCH", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-path`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-bundle`],
["PATCH", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-bundle`],
["GET", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-bundle/file?path=AGENTS.md`],
["PUT", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-bundle/file`],
["DELETE", `http://localhost:3100/api/agents/${AGENT_ID}/instructions-bundle/file?path=AGENTS.md`],
]);
});
});
function jsonResponse(body: unknown = { ok: true }, init: ResponseInit = { status: 200 }): Response {
return new Response(JSON.stringify(body), init);
}