From cc226e16b58d0a1dab785ccf24c83301c75aef6f Mon Sep 17 00:00:00 2001 From: barry <91018388+barry166@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:38:02 +0800 Subject: [PATCH] Align CLI search limits with the shared retrieval contract The CLI advertised and accepted more results than the MCP surface and SPEC\nallow. Reusing the shared cap removes the silent divergence and adds an\nintegration test on the 15-result boundary so the two interfaces stay in lockstep.\n\nConstraint: CLI and MCP retrieval rules need one consistent documented cap\nRejected: Keep a larger CLI-only limit and document an exception | unnecessary contract split for a simple query surface\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Keep CLI search limits sourced from the shared MCP constants unless the SPEC changes first\nTested: npm run typecheck --workspace @agentctxhq/agentctx\nTested: npm run build --workspace @agentctxhq/agentctx\nTested: npx biome check packages/agentctx/src/cli/search.ts packages/agentctx/test/cli/commands.test.ts\nTested: npx vitest run test/cli/commands.test.ts\nNot-tested: Full workspace test suite --- packages/agentctx/src/cli/search.ts | 12 ++++----- packages/agentctx/test/cli/commands.test.ts | 29 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/agentctx/src/cli/search.ts b/packages/agentctx/src/cli/search.ts index 61b05d8..63bff74 100644 --- a/packages/agentctx/src/cli/search.ts +++ b/packages/agentctx/src/cli/search.ts @@ -8,6 +8,7 @@ */ import { existsSync } from "node:fs"; import { parseArgs } from "node:util"; +import { SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX } from "../mcp/tools.js"; import { openDatabase } from "../storage/db.js"; import { resolveProjectId } from "../storage/namespace.js"; import { searchRecords } from "../storage/search.js"; @@ -18,10 +19,7 @@ export const SEARCH_USAGE = `Usage: agentctx search [options] Options: --type restrict to one record type (${RECORD_TYPES.join(", ")}) - --limit maximum results (default 10, max 50)`; - -const LIMIT_DEFAULT = 10; -const LIMIT_MAX = 50; + --limit maximum results (default ${SEARCH_LIMIT_DEFAULT}, max ${SEARCH_LIMIT_MAX})`; export async function runSearch(env: CliEnv, args: string[]): Promise { if (args.includes("--help")) { @@ -53,11 +51,11 @@ export async function runSearch(env: CliEnv, args: string[]): Promise { } type = values.type as RecordType; } - let limit = LIMIT_DEFAULT; + let limit = SEARCH_LIMIT_DEFAULT; if (values.limit !== undefined) { const parsed = Number(values.limit); - if (!Number.isInteger(parsed) || parsed < 1 || parsed > LIMIT_MAX) { - env.io.err(`agentctx search: --limit must be an integer between 1 and ${LIMIT_MAX}`); + if (!Number.isInteger(parsed) || parsed < 1 || parsed > SEARCH_LIMIT_MAX) { + env.io.err(`agentctx search: --limit must be an integer between 1 and ${SEARCH_LIMIT_MAX}`); return 1; } limit = parsed; diff --git a/packages/agentctx/test/cli/commands.test.ts b/packages/agentctx/test/cli/commands.test.ts index 81f58e4..cb5a9b4 100644 --- a/packages/agentctx/test/cli/commands.test.ts +++ b/packages/agentctx/test/cli/commands.test.ts @@ -139,6 +139,35 @@ describe("agentctx config", () => { }); }); +describe("agentctx search", () => { + it("rejects limits above the shared MCP/SPEC cap", async () => { + await main(["init", "--no-profile", "--no-mcp"], t.env); + + expect(await main(["search", "caching", "--limit", "16"], t.env)).toBe(1); + expect(t.stderr.join("\n")).toContain("between 1 and 15"); + }); + + it("accepts the documented max limit", async () => { + await main(["init", "--no-profile", "--no-mcp"], t.env); + const projectId = resolveProjectId(t.env.cwd); + + const db = openDatabase(t.env.dbPath); + for (let i = 0; i < 20; i++) { + insertRecord(db, { + projectId, + type: "decision", + title: `Decision ${i}`, + body: `caching detail ${i}`, + source: "cli", + }); + } + db.close(); + + expect(await main(["search", "caching", "--limit", "15"], t.env)).toBe(0); + expect(t.stdout.filter((line) => line.includes("Decision "))).toHaveLength(15); + }); +}); + describe("agentctx reset", () => { it("deletes only the current project's records, after confirmation", async () => { await main(["init", "--no-profile", "--no-mcp"], t.env);