test(cli): cover board prompt handoff (#9137)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The CLI is one external control-plane entry point for scripts,
operators, and agent handoff flows.
> - Prompt handoff intentionally maps prompts back to Paperclip work
objects: issues, comments, and optional wakeups.
> - The existing prompt tests covered the agent-authenticated path, but
the board-authenticated path had less direct coverage.
> - That left regressions in persona validation, board issue creation,
comment append, and no-wake behavior harder to catch.
> - This pull request adds focused tests for those board prompt handoff
paths.
> - The benefit is safer CLI parity work without changing runtime
behavior.

## Linked Issues or Issue Description

No public issue found. This is a test coverage improvement for the CLI
prompt handoff workflow described in
`doc/plans/2026-05-23-cli-api-parity.md`.

## What Changed

- Added `runBoardPrompt` coverage for rejecting agent persona profiles.
- Added board-authenticated issue creation coverage, including target
agent resolution and wakeup.
- Added board-authenticated comment append coverage with `wake: false`
to verify no wakeup request is sent.

## Verification

- `./node_modules/.bin/vitest run cli/src/__tests__/prompt.test.ts
--config cli/vitest.config.ts` passes: 1 file, 6 tests.
- `pnpm --filter paperclipai typecheck` was attempted, but this local
checkout fails before reaching this change because
`@paperclipai/plugin-sdk` dist artifacts are missing for server imports;
representative errors include `Cannot find module
@paperclipai/plugin-sdk` from `server/src/app.ts` and
`server/src/routes/plugins.ts`.

## Risks

Low risk. This is test-only coverage for existing CLI behavior.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI GPT-5 Codex in Codex desktop, with repository inspection, shell
command execution, and code editing tools.

## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: 馨冉 <xinxincui239@gmail.com>
This commit is contained in:
馨冉 2026-07-21 23:00:28 +08:00 committed by GitHub
parent 876ac7596c
commit 99a1b5c83b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 96 additions and 1 deletions

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeContext } from "../client/context.js";
import { runAgentPrompt } from "../commands/client/prompt.js";
import { runAgentPrompt, runBoardPrompt } from "../commands/client/prompt.js";
const ORIGINAL_ENV = { ...process.env };
@ -99,4 +99,99 @@ describe("prompt handoff", () => {
});
expect(fetchMock.mock.calls[2]?.[0]).toBe("http://localhost:3100/api/agents/11111111-1111-4111-8111-111111111111/wakeup");
});
it("fails when a board prompt uses an agent persona profile", async () => {
const contextPath = createTempContextPath();
writeContext(
{
version: 2,
currentProfile: "agent",
profiles: {
agent: {
apiBase: "http://localhost:3100",
companyId: "22222222-2222-4222-8222-222222222222",
persona: "agent",
agentId: "11111111-1111-4111-8111-111111111111",
},
},
},
contextPath,
);
await expect(runBoardPrompt("worker", "Do the work", {
context: contextPath,
apiKey: "board-token",
})).rejects.toThrow(/persona=agent/);
});
it("creates an assigned issue and wakes the target agent with board auth", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(agent()), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
id: "issue-1",
companyId: "22222222-2222-4222-8222-222222222222",
title: "Investigate queue lag",
status: "todo",
priority: "medium",
assigneeAgentId: "11111111-1111-4111-8111-111111111111",
}), { status: 201 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ id: "run-1", status: "queued" }), { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
const result = await runBoardPrompt("worker", "Investigate queue lag", {
apiBase: "http://localhost:3100",
apiKey: "board-token",
companyId: "22222222-2222-4222-8222-222222222222",
});
expect(result.actor).toBe("board");
expect(result.mode).toBe("issue");
expect(result.agent.id).toBe("11111111-1111-4111-8111-111111111111");
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(fetchMock.mock.calls[0]?.[0]).toBe(
"http://localhost:3100/api/agents/worker?companyId=22222222-2222-4222-8222-222222222222",
);
expect(fetchMock.mock.calls[1]?.[0]).toBe(
"http://localhost:3100/api/companies/22222222-2222-4222-8222-222222222222/issues",
);
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toMatchObject({
assigneeAgentId: "11111111-1111-4111-8111-111111111111",
description: "Investigate queue lag",
title: "Investigate queue lag",
});
expect(fetchMock.mock.calls[2]?.[0]).toBe(
"http://localhost:3100/api/agents/11111111-1111-4111-8111-111111111111/wakeup",
);
});
it("adds a board-authored prompt comment without waking when disabled", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(agent()), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
id: "comment-1",
issueId: "issue-1",
body: "Follow up on queue lag",
}), { status: 201 }));
vi.stubGlobal("fetch", fetchMock);
const result = await runBoardPrompt("worker", "Follow up on queue lag", {
apiBase: "http://localhost:3100",
apiKey: "board-token",
companyId: "22222222-2222-4222-8222-222222222222",
issue: "issue-1",
wake: false,
});
expect(result.actor).toBe("board");
expect(result.mode).toBe("comment");
expect(result.wakeup).toBeNull();
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[1]?.[0]).toBe("http://localhost:3100/api/issues/issue-1/comments");
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toMatchObject({
body: "Follow up on queue lag",
resume: false,
});
});
});