Spaces:
Paused
Paused
| import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { PaperclipApiClient } from "./client.js"; | |
| import { createToolDefinitions } from "./tools.js"; | |
| function makeClient() { | |
| return new PaperclipApiClient({ | |
| apiUrl: "http://localhost:3100/api", | |
| apiKey: "token-123", | |
| companyId: "11111111-1111-1111-1111-111111111111", | |
| agentId: "22222222-2222-2222-2222-222222222222", | |
| runId: "33333333-3333-3333-3333-333333333333", | |
| hfToken: null, | |
| }); | |
| } | |
| function getTool(name: string) { | |
| const tool = createToolDefinitions(makeClient()).find((candidate) => candidate.name === name); | |
| if (!tool) throw new Error(`Missing tool ${name}`); | |
| return tool; | |
| } | |
| function mockJsonResponse(body: unknown, status = 200) { | |
| return new Response(JSON.stringify(body), { | |
| status, | |
| headers: { "Content-Type": "application/json" }, | |
| }); | |
| } | |
| describe("paperclip MCP tools", () => { | |
| beforeEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| it("adds auth headers and run id to mutating requests", async () => { | |
| const fetchMock = vi.fn().mockResolvedValue( | |
| mockJsonResponse({ ok: true }), | |
| ); | |
| vi.stubGlobal("fetch", fetchMock); | |
| const tool = getTool("paperclipUpdateIssue"); | |
| await tool.execute({ | |
| issueId: "PAP-1135", | |
| status: "done", | |
| }); | |
| expect(fetchMock).toHaveBeenCalledTimes(1); | |
| const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; | |
| expect(String(url)).toBe("http://localhost:3100/api/issues/PAP-1135"); | |
| expect(init.method).toBe("PATCH"); | |
| expect((init.headers as Record<string, string>)["Authorization"]).toBe("Bearer token-123"); | |
| expect((init.headers as Record<string, string>)["X-Paperclip-Run-Id"]).toBe( | |
| "33333333-3333-3333-3333-333333333333", | |
| ); | |
| }); | |
| it("uses default company id for company-scoped list tools", async () => { | |
| const fetchMock = vi.fn().mockResolvedValue( | |
| mockJsonResponse([{ id: "issue-1" }]), | |
| ); | |
| vi.stubGlobal("fetch", fetchMock); | |
| const tool = getTool("paperclipListIssues"); | |
| const response = await tool.execute({}); | |
| expect(fetchMock).toHaveBeenCalledTimes(1); | |
| const [url] = fetchMock.mock.calls[0] as [string]; | |
| expect(String(url)).toBe( | |
| "http://localhost:3100/api/companies/11111111-1111-1111-1111-111111111111/issues", | |
| ); | |
| expect(response.content[0]?.text).toContain("issue-1"); | |
| }); | |
| it("uses default agent id for checkout requests", async () => { | |
| const fetchMock = vi.fn().mockResolvedValue( | |
| mockJsonResponse({ id: "PAP-1135", status: "in_progress" }), | |
| ); | |
| vi.stubGlobal("fetch", fetchMock); | |
| const tool = getTool("paperclipCheckoutIssue"); | |
| await tool.execute({ | |
| issueId: "PAP-1135", | |
| }); | |
| const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; | |
| expect(JSON.parse(String(init.body))).toEqual({ | |
| agentId: "22222222-2222-2222-2222-222222222222", | |
| expectedStatuses: ["todo", "backlog", "blocked"], | |
| }); | |
| }); | |
| it("defaults issue document format to markdown", async () => { | |
| const fetchMock = vi.fn().mockResolvedValue( | |
| mockJsonResponse({ key: "plan", latestRevisionNumber: 2 }), | |
| ); | |
| vi.stubGlobal("fetch", fetchMock); | |
| const tool = getTool("paperclipUpsertIssueDocument"); | |
| await tool.execute({ | |
| issueId: "PAP-1135", | |
| key: "plan", | |
| body: "# Updated", | |
| }); | |
| const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; | |
| expect(JSON.parse(String(init.body))).toEqual({ | |
| format: "markdown", | |
| body: "# Updated", | |
| }); | |
| }); | |
| it("creates approvals with the expected company-scoped payload", async () => { | |
| const fetchMock = vi.fn().mockResolvedValue( | |
| mockJsonResponse({ id: "approval-1" }), | |
| ); | |
| vi.stubGlobal("fetch", fetchMock); | |
| const tool = getTool("paperclipCreateApproval"); | |
| await tool.execute({ | |
| type: "hire_agent", | |
| payload: { branch: "pap-1167" }, | |
| issueIds: ["44444444-4444-4444-4444-444444444444"], | |
| }); | |
| expect(fetchMock).toHaveBeenCalledTimes(1); | |
| const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; | |
| expect(String(url)).toBe( | |
| "http://localhost:3100/api/companies/11111111-1111-1111-1111-111111111111/approvals", | |
| ); | |
| expect(init.method).toBe("POST"); | |
| expect(JSON.parse(String(init.body))).toEqual({ | |
| type: "hire_agent", | |
| payload: { branch: "pap-1167" }, | |
| issueIds: ["44444444-4444-4444-4444-444444444444"], | |
| }); | |
| }); | |
| it("rejects invalid generic request paths", async () => { | |
| vi.stubGlobal("fetch", vi.fn()); | |
| const tool = getTool("paperclipApiRequest"); | |
| const response = await tool.execute({ | |
| method: "GET", | |
| path: "issues", | |
| }); | |
| expect(response.content[0]?.text).toContain("path must start with /"); | |
| }); | |
| it("rejects generic request paths that escape /api", async () => { | |
| vi.stubGlobal("fetch", vi.fn()); | |
| const tool = getTool("paperclipApiRequest"); | |
| const response = await tool.execute({ | |
| method: "GET", | |
| path: "/../../secret", | |
| }); | |
| expect(response.content[0]?.text).toContain("must not contain '..'"); | |
| }); | |
| }); | |