From 27c81a4a52c8b4df4e19fd392d0d1d5c7832a149 Mon Sep 17 00:00:00 2001 From: Anton Arapov Date: Fri, 29 May 2026 11:51:47 +0200 Subject: [PATCH] test: cover all chunked multi-get callers --- CHANGELOG.md | 4 ++-- HOWTO.md | 2 +- src/tools/tasks.ts | 5 +++-- tests/opportunities.test.ts | 18 ++++++++++++++++++ tests/projects.test.ts | 18 ++++++++++++++++++ tests/tasks.test.ts | 15 +++++++++++++++ 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 453f27b..b48c082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,8 +57,8 @@ versions adhere to [Semantic Versioning](https://semver.org). "≤10 ids → single GET, else split into 10-id chunks, fan out in parallel, concatenate" block; they're now one-line delegations. No behavioural or API change — the "Capsule caps multi-id GET at - 10" rule now lives in one place. Covered by the existing - per-handler batch tests (incl. the >10-id chunking case). + 10" rule now lives in one place. Covered by per-handler batch tests, + including the >10-id chunking path for all four callers. ### Fixed diff --git a/HOWTO.md b/HOWTO.md index 727963d..488c081 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -11,7 +11,7 @@ npm install npm test ``` -540 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers: +543 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers: - **Per-tool unit tests** (e.g. `tests/parties.test.ts`): import the tool function, mock `undici.fetch`, assert on the URL, method, body, and response handling. Most tests live here. - **MCP-protocol integration tests** (`tests/mcp-integration.test.ts`): drive a real `McpServer` through the wire protocol via the SDK's in-memory transport pair, with `undici.fetch` still mocked. Catches the layer between "tool function works" and "MCP correctly registers and dispatches the tool". Includes the `get_attachment` content-type routing logic (which lives in `server.ts`, not the tool function). diff --git a/src/tools/tasks.ts b/src/tools/tasks.ts index 8550984..8d32a62 100644 --- a/src/tools/tasks.ts +++ b/src/tools/tasks.ts @@ -52,8 +52,9 @@ export async function getTask(input: z.infer) { // ─────────────────────────────────────────────────────────────────────────── // -// Batch fetch up to 10 tasks by id in a single call. Capsule's path -// syntax: GET /tasks/,,... — the server caps at 10 per call. +// Batch fetch up to 50 tasks by id. Capsule's path syntax: +// GET /tasks/,,... caps at 10 ids per request, so larger +// caller batches are split into 10-id chunks and merged. export const getTasksSchema = z.object({ ids: z diff --git a/tests/opportunities.test.ts b/tests/opportunities.test.ts index 2bdecda..8055434 100644 --- a/tests/opportunities.test.ts +++ b/tests/opportunities.test.ts @@ -409,4 +409,22 @@ describe("getOpportunities (batch)", () => { const [url] = vi.mocked(fetch).mock.calls[0]!; expect(url).toMatch(/\/opportunities\/1,2,3($|\?)/); }); + + it("splits >10 ids into chunks, propagates embed, and merges results", async () => { + mockFetch(200, { opportunities: Array.from({ length: 10 }, (_v, i) => ({ id: i + 1 })) }); + mockFetch(200, { opportunities: [{ id: 11 }] }); + + const { getOpportunities } = await import("../src/tools/opportunities.js"); + const ids = Array.from({ length: 11 }, (_v, i) => i + 1); + const result = (await getOpportunities({ ids, embed: "tags,fields" })) as { + opportunities: Array<{ id: number }>; + }; + + expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2); + const urls = vi.mocked(fetch).mock.calls.map((c) => String(c[0])); + expect(urls[0]).toContain("/opportunities/1,2,3,4,5,6,7,8,9,10"); + expect(urls[1]).toContain("/opportunities/11"); + expect(urls.every((u) => u.includes("embed=tags%2Cfields"))).toBe(true); + expect(result.opportunities.map((o) => o.id)).toEqual(ids); + }); }); diff --git a/tests/projects.test.ts b/tests/projects.test.ts index 5d1a126..2e4ee8e 100644 --- a/tests/projects.test.ts +++ b/tests/projects.test.ts @@ -467,4 +467,22 @@ describe("getProjects (batch)", () => { expect(url).toMatch(/\/kases\/1,2($|\?)/); expect(url).not.toContain("/projects/"); }); + + it("splits >10 ids into chunks, propagates embed, and merges kases", async () => { + mockFetch(200, { kases: Array.from({ length: 10 }, (_v, i) => ({ id: i + 1 })) }); + mockFetch(200, { kases: [{ id: 11 }] }); + + const { getProjects } = await import("../src/tools/projects.js"); + const ids = Array.from({ length: 11 }, (_v, i) => i + 1); + const result = (await getProjects({ ids, embed: "tags,fields" })) as { + kases: Array<{ id: number }>; + }; + + expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2); + const urls = vi.mocked(fetch).mock.calls.map((c) => String(c[0])); + expect(urls[0]).toContain("/kases/1,2,3,4,5,6,7,8,9,10"); + expect(urls[1]).toContain("/kases/11"); + expect(urls.every((u) => u.includes("embed=tags%2Cfields"))).toBe(true); + expect(result.kases.map((kase) => kase.id)).toEqual(ids); + }); }); diff --git a/tests/tasks.test.ts b/tests/tasks.test.ts index a5c0902..56976b2 100644 --- a/tests/tasks.test.ts +++ b/tests/tasks.test.ts @@ -232,4 +232,19 @@ describe("getTasks (batch)", () => { const [url] = vi.mocked(fetch).mock.calls[0]!; expect(url).toMatch(/\/tasks\/1,2($|\?)/); }); + + it("splits >10 ids into chunks and merges tasks", async () => { + mockFetch(200, { tasks: Array.from({ length: 10 }, (_v, i) => ({ id: i + 1 })) }); + mockFetch(200, { tasks: [{ id: 11 }] }); + + const { getTasks } = await import("../src/tools/tasks.js"); + const ids = Array.from({ length: 11 }, (_v, i) => i + 1); + const result = (await getTasks({ ids })) as { tasks: Array<{ id: number }> }; + + expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2); + const urls = vi.mocked(fetch).mock.calls.map((c) => String(c[0])); + expect(urls[0]).toContain("/tasks/1,2,3,4,5,6,7,8,9,10"); + expect(urls[1]).toContain("/tasks/11"); + expect(result.tasks.map((task) => task.id)).toEqual(ids); + }); });