From aa5e436d91f8ada879727684d9e299014e8d27ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Sat, 13 Jun 2026 10:24:16 +0100 Subject: [PATCH] fix(mcp): rename query/cypher params so Claude Code can call them (#2186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): advertise search_query/statement params for query/cypher tools (#2175) Claude Code drops a tool-call argument named exactly 'query', making the query and cypher tools unusable from it. Rename the advertised required parameters to search_query and statement so the client transmits them. Handler-side backward-compat for the legacy 'query' key follows in the next commit. * fix(mcp): accept search_query/statement with legacy query fallback (#2175) Resolve the new advertised param names in the backend while still accepting the legacy 'query' key, so curl/HTTP, other MCP clients, the CLI, the group path, and the internal executeCypher() all keep working. Alias is normalized once at the callTool chokepoint (covers group-forward + search alias); query() and cypher() dual-read defensively. New name wins when both are supplied. Updates the required-error message and adds dual-accept unit + integration coverage. * fix(cli): pass canonical search_query/statement params to query/cypher tools (#2175) Stop the CLI from depending on the deprecated 'query' alias. No user-facing change — the positional args are unchanged and the backend accepts both keys. * fix(mcp): generators advertise search_query in query() examples (#2175) Update the three doc/example generators (ai-context AGENTS/CLAUDE block, skill-gen community skills, resources repo hint) so future analyze runs emit query({search_query: ...}) — the param name Claude Code actually transmits. Tests assert the new form is present and the legacy query({query: form is absent (the #2059 generator-test pattern). * docs(mcp): advertise search_query/statement in skill & guidance examples (#2175) Sync the committed agent-facing docs to the renamed params so a Claude Code agent following them emits the transmittable key: AGENTS.md/CLAUDE.md gitnexus block, the canonical gitnexus/skills/* source and its installed/plugin/cursor mirrors, and the README examples. Scoped rewrite of the two call prefixes only (query({query: -> search_query, cypher({query: -> statement). * style(mcp): prettier line-wrap for #2175 alias-resolution edits * fix(review): uniform search_query precedence + cypher empty guard (#2175) Code-review findings (correctness/adversarial/api-contract/maintainability consensus): - Group-mode query inverted the 'new name wins' rule: the callTool chokepoint backfilled params.query only when empty and the @group-forward read params.query directly, so a both-keys (or whitespace-legacy) group call let the legacy value win — unlike the local path. Replace the hidden param mutation with a self-contained 'search_query ?? query' resolve at the group-forward; precedence is now uniformly new-wins at every consumer site. - cypher() now returns the same friendly required-param error as query() when neither statement nor query is supplied, instead of a raw DB prepare error. - Document the legacy alias as permanent (third-party clients may send query=). Adds group-forward alias tests (both-keys + legacy-only), empty/whitespace search_query, the search-alias path, and the cypher empty-statement guard. * fix(review): non-string alias safety + drop stale chokepoint comment (#2175) Tri-review findings (correctness/adversarial/security + maintainability): - Non-string statement/search_query/query (the MCP envelope is not schema-validated) hit .trim() and threw TypeError to the server boundary instead of a friendly required-param error. Introduce resolveAliasString() (new name wins; non-string -> undefined) used by query(), cypher(), and the group-forward, so all three return the structured error. Empirically verified (123 ?? '' -> 123, (123).trim() throws) — this overrides a critic refutation that mis-read ?? as a string coercion. - Remove the stale query() comment claiming alias resolution happens at a callTool chokepoint; that mutation was removed earlier in this PR — each site resolves the alias itself. - Document GroupToolPort.query's intentionally-narrower required type vs the wider LocalBackend impl. Adds non-string and empty-new-key precedence tests. * fix(mcp): alias falls back to legacy value when new key is blank (#2175) PR #2186 review finding: resolveAliasString used `canonical ?? legacy` (nullish), so an explicitly empty/whitespace new-name value (e.g. {search_query:'', query:'real'}) won and was rejected — discarding a valid legacy value, contradicting the 'new name wins when both supplied' intent. Resolve to the first NON-BLANK string instead (new preferred when it carries a real value, else legacy). Covers query(), cypher(), and the group-forward (all route through the helper); non-string still resolves to a friendly error. Flips the presence-based test and adds whitespace/cypher/group fallback cases. * fix(mcp): drop legacy "query" mention from query/cypher schema descriptions (#2175) PR #2186 review finding: the search_query/statement inputSchema descriptions named the legacy "query" key — the exact arg Claude Code drops — and description text is read by an LLM choosing arguments, weakly nudging it to send "query". Trim the descriptions to their clean form and move the legacy-alias note to a code comment next to the schema (preserved for maintainers / non-CC clients). properties/required unchanged (no `query`). --- .../gitnexus/gitnexus-debugging/SKILL.md | 8 +- .../gitnexus/gitnexus-exploring/SKILL.md | 6 +- .../gitnexus/gitnexus-refactoring/SKILL.md | 2 +- AGENTS.md | 2 +- CLAUDE.md | 2 +- README.md | 4 +- .../skills/gitnexus-debugging/SKILL.md | 8 +- .../skills/gitnexus-exploring/SKILL.md | 6 +- .../skills/gitnexus-refactoring/SKILL.md | 2 +- .../skills/gitnexus-debugging/SKILL.md | 8 +- .../skills/gitnexus-exploring/SKILL.md | 6 +- .../skills/gitnexus-refactoring/SKILL.md | 2 +- gitnexus/README.md | 2 +- gitnexus/skills/gitnexus-debugging.md | 8 +- gitnexus/skills/gitnexus-exploring.md | 6 +- gitnexus/skills/gitnexus-refactoring.md | 2 +- gitnexus/src/cli/ai-context.ts | 2 +- gitnexus/src/cli/skill-gen.ts | 2 +- gitnexus/src/cli/tool.ts | 6 +- gitnexus/src/core/group/service.ts | 4 + gitnexus/src/mcp/local/local-backend.ts | 58 +++++- gitnexus/src/mcp/resources.ts | 2 +- gitnexus/src/mcp/tools.ts | 22 +- .../local-backend-calltool.test.ts | 20 ++ gitnexus/test/unit/ai-context.test.ts | 5 +- gitnexus/test/unit/calltool-dispatch.test.ts | 193 +++++++++++++++++- gitnexus/test/unit/resources.test.ts | 4 +- gitnexus/test/unit/skill-gen.test.ts | 4 +- gitnexus/test/unit/tools.test.ts | 19 +- 29 files changed, 351 insertions(+), 64 deletions(-) diff --git a/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md index 9834f94b79..80f9c0ec51 100644 --- a/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. query({query: ""}) → Find related execution flows +1. query({search_query: ""}) → Find related execution flows 2. context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow -4. cypher({query: "MATCH path..."}) → Custom traces if needed +4. cypher({statement: "MATCH path..."}) → Custom traces if needed ``` > If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal. @@ -51,7 +51,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking **query** — find code related to error: ``` -query({query: "payment validation error"}) +query({search_query: "payment validation error"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError, PaymentException ``` @@ -75,7 +75,7 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. query({query: "payment error handling"}) +1. query({search_query: "payment error handling"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError diff --git a/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md index ccf684c285..f483c2fd63 100644 --- a/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md @@ -18,7 +18,7 @@ description: "Use when the user asks how code works, wants to understand archite ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. query({query: ""}) → Find related execution flows +3. query({search_query: ""}) → Find related execution flows 4. context({name: ""}) → Deep dive on specific symbol 5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` @@ -50,7 +50,7 @@ description: "Use when the user asks how code works, wants to understand archite **query** — find execution flows related to a concept: ``` -query({query: "payment processing"}) +query({search_query: "payment processing"}) → Processes: CheckoutFlow, RefundFlow, WebhookHandler → Symbols grouped by flow with file locations ``` @@ -68,7 +68,7 @@ context({name: "validateUser"}) ``` 1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes -2. query({query: "payment processing"}) +2. query({search_query: "payment processing"}) → CheckoutFlow: processPayment → validateCard → chargeStripe → RefundFlow: initiateRefund → calculateRefund → processRefund 3. context({name: "processPayment"}) diff --git a/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md index e13c04e140..90c8c324d8 100644 --- a/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md @@ -17,7 +17,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru ``` 1. impact({target: "X", direction: "upstream"}) → Map all dependents -2. query({query: "X"}) → Find execution flows involving X +2. query({search_query: "X"}) → Find execution flows involving X 3. context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` diff --git a/AGENTS.md b/AGENTS.md index 64d2641262..285a121860 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. - **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`. ## Never Do diff --git a/CLAUDE.md b/CLAUDE.md index f2bf1e4876..7350cfb092 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,7 +65,7 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. - **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`. ## Never Do diff --git a/README.md b/README.md index 876ff81e23..ab01db810e 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,7 @@ It is opt-in and a no-op without `UNDERSTAND_QUICKLY_TOKEN` — a fine-grained G | `group_query` | Search execution flows across all repos in a group | — | | `group_status` | Check staleness of repos in a group | — | -> When only one repo is indexed, the `repo` parameter is optional. With multiple repos, specify which one: `query({query: "auth", repo: "my-app"})`. +> When only one repo is indexed, the `repo` parameter is optional. With multiple repos, specify which one: `query({search_query: "auth", repo: "my-app"})`. **Resources** for instant context: @@ -738,7 +738,7 @@ gitnexus impact get_embeddings --uid "Function:src/embed.py:get_embeddings" # e ### Process-Grouped Search ``` -query({query: "authentication middleware"}) +query({search_query: "authentication middleware"}) processes: - summary: "LoginFlow" diff --git a/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md index 9834f94b79..80f9c0ec51 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. query({query: ""}) → Find related execution flows +1. query({search_query: ""}) → Find related execution flows 2. context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow -4. cypher({query: "MATCH path..."}) → Custom traces if needed +4. cypher({statement: "MATCH path..."}) → Custom traces if needed ``` > If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal. @@ -51,7 +51,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking **query** — find code related to error: ``` -query({query: "payment validation error"}) +query({search_query: "payment validation error"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError, PaymentException ``` @@ -75,7 +75,7 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. query({query: "payment error handling"}) +1. query({search_query: "payment error handling"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError diff --git a/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md index ccf684c285..f483c2fd63 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md @@ -18,7 +18,7 @@ description: "Use when the user asks how code works, wants to understand archite ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. query({query: ""}) → Find related execution flows +3. query({search_query: ""}) → Find related execution flows 4. context({name: ""}) → Deep dive on specific symbol 5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` @@ -50,7 +50,7 @@ description: "Use when the user asks how code works, wants to understand archite **query** — find execution flows related to a concept: ``` -query({query: "payment processing"}) +query({search_query: "payment processing"}) → Processes: CheckoutFlow, RefundFlow, WebhookHandler → Symbols grouped by flow with file locations ``` @@ -68,7 +68,7 @@ context({name: "validateUser"}) ``` 1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes -2. query({query: "payment processing"}) +2. query({search_query: "payment processing"}) → CheckoutFlow: processPayment → validateCard → chargeStripe → RefundFlow: initiateRefund → calculateRefund → processRefund 3. context({name: "processPayment"}) diff --git a/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md index e13c04e140..90c8c324d8 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md @@ -17,7 +17,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru ``` 1. impact({target: "X", direction: "upstream"}) → Map all dependents -2. query({query: "X"}) → Find execution flows involving X +2. query({search_query: "X"}) → Find execution flows involving X 3. context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` diff --git a/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md index a88b764307..cde6a6a3aa 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md @@ -15,10 +15,10 @@ description: Trace bugs through call chains using knowledge graph ## Workflow ``` -1. query({query: ""}) → Find related execution flows +1. query({search_query: ""}) → Find related execution flows 2. context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow -4. cypher({query: "MATCH path..."}) → Custom traces if needed +4. cypher({statement: "MATCH path..."}) → Custom traces if needed ``` > If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal. @@ -49,7 +49,7 @@ description: Trace bugs through call chains using knowledge graph **query** — find code related to error: ``` -query({query: "payment validation error"}) +query({search_query: "payment validation error"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError, PaymentException ``` @@ -71,7 +71,7 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. query({query: "payment error handling"}) +1. query({search_query: "payment error handling"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError diff --git a/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md index 73df1353a3..993a38481a 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md @@ -17,7 +17,7 @@ description: Navigate unfamiliar code using GitNexus knowledge graph ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. query({query: ""}) → Find related execution flows +3. query({search_query: ""}) → Find related execution flows 4. context({name: ""}) → Deep dive on specific symbol 5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` @@ -48,7 +48,7 @@ description: Navigate unfamiliar code using GitNexus knowledge graph **query** — find execution flows related to a concept: ``` -query({query: "payment processing"}) +query({search_query: "payment processing"}) → Processes: CheckoutFlow, RefundFlow, WebhookHandler → Symbols grouped by flow with file locations ``` @@ -65,7 +65,7 @@ context({name: "validateUser"}) ``` 1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes -2. query({query: "payment processing"}) +2. query({search_query: "payment processing"}) → CheckoutFlow: processPayment → validateCard → chargeStripe → RefundFlow: initiateRefund → calculateRefund → processRefund 3. context({name: "processPayment"}) diff --git a/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md index 76c9d3351b..fbf1931824 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md @@ -16,7 +16,7 @@ description: Plan safe refactors using blast radius and dependency mapping ``` 1. impact({target: "X", direction: "upstream"}) → Map all dependents -2. query({query: "X"}) → Find execution flows involving X +2. query({search_query: "X"}) → Find execution flows involving X 3. context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` diff --git a/gitnexus/README.md b/gitnexus/README.md index a306f08524..92cf4593c0 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -134,7 +134,7 @@ Your AI agent gets these tools automatically: | `rename` | Multi-file coordinated rename with graph + text search | Optional | | `cypher` | Raw Cypher graph queries | Optional | -> With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({query: "auth", repo: "my-app"})`. +> With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({search_query: "auth", repo: "my-app"})`. ## MCP Resources diff --git a/gitnexus/skills/gitnexus-debugging.md b/gitnexus/skills/gitnexus-debugging.md index 9834f94b79..80f9c0ec51 100644 --- a/gitnexus/skills/gitnexus-debugging.md +++ b/gitnexus/skills/gitnexus-debugging.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. query({query: ""}) → Find related execution flows +1. query({search_query: ""}) → Find related execution flows 2. context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow -4. cypher({query: "MATCH path..."}) → Custom traces if needed +4. cypher({statement: "MATCH path..."}) → Custom traces if needed ``` > If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal. @@ -51,7 +51,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking **query** — find code related to error: ``` -query({query: "payment validation error"}) +query({search_query: "payment validation error"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError, PaymentException ``` @@ -75,7 +75,7 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. query({query: "payment error handling"}) +1. query({search_query: "payment error handling"}) → Processes: CheckoutFlow, ErrorHandling → Symbols: validatePayment, handlePaymentError diff --git a/gitnexus/skills/gitnexus-exploring.md b/gitnexus/skills/gitnexus-exploring.md index ccf684c285..f483c2fd63 100644 --- a/gitnexus/skills/gitnexus-exploring.md +++ b/gitnexus/skills/gitnexus-exploring.md @@ -18,7 +18,7 @@ description: "Use when the user asks how code works, wants to understand archite ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. query({query: ""}) → Find related execution flows +3. query({search_query: ""}) → Find related execution flows 4. context({name: ""}) → Deep dive on specific symbol 5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` @@ -50,7 +50,7 @@ description: "Use when the user asks how code works, wants to understand archite **query** — find execution flows related to a concept: ``` -query({query: "payment processing"}) +query({search_query: "payment processing"}) → Processes: CheckoutFlow, RefundFlow, WebhookHandler → Symbols grouped by flow with file locations ``` @@ -68,7 +68,7 @@ context({name: "validateUser"}) ``` 1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes -2. query({query: "payment processing"}) +2. query({search_query: "payment processing"}) → CheckoutFlow: processPayment → validateCard → chargeStripe → RefundFlow: initiateRefund → calculateRefund → processRefund 3. context({name: "processPayment"}) diff --git a/gitnexus/skills/gitnexus-refactoring.md b/gitnexus/skills/gitnexus-refactoring.md index e13c04e140..90c8c324d8 100644 --- a/gitnexus/skills/gitnexus-refactoring.md +++ b/gitnexus/skills/gitnexus-refactoring.md @@ -17,7 +17,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru ``` 1. impact({target: "X", direction: "upstream"}) → Map all dependents -2. query({query: "X"}) → Find execution flows involving X +2. query({search_query: "X"}) → Find execution flows involving X 3. context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 4bf1179748..6700c020bb 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -177,7 +177,7 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user. - **MUST run \`detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`. - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use \`query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When exploring unfamiliar code, use \`query({search_query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`context({name: "symbolName"})\`. - For security review, \`explain({target: "fileOrSymbol"})\` lists taint findings (source→sink flows; needs \`analyze --pdg\`). diff --git a/gitnexus/src/cli/skill-gen.ts b/gitnexus/src/cli/skill-gen.ts index 61569e6a4a..d718a91dd6 100644 --- a/gitnexus/src/cli/skill-gen.ts +++ b/gitnexus/src/cli/skill-gen.ts @@ -651,7 +651,7 @@ const renderSkillMarkdown = ( lines.push(''); lines.push(`1. \`context({name: "${firstEntry}"})\` \u2014 see callers and callees`); lines.push( - `2. \`query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`, + `2. \`query({search_query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`, ); lines.push('3. Read key files listed above for implementation details'); lines.push( diff --git a/gitnexus/src/cli/tool.ts b/gitnexus/src/cli/tool.ts index b892f6e2c7..76850eb9f3 100644 --- a/gitnexus/src/cli/tool.ts +++ b/gitnexus/src/cli/tool.ts @@ -76,7 +76,8 @@ export async function queryCommand( const backend = await getBackend(); const result = await backend.callTool('query', { - query: queryText, + // #2175: canonical param is search_query; the backend still accepts legacy "query". + search_query: queryText, task_context: options?.context, goal: options?.goal, limit: options?.limit ? parseInt(options.limit) : undefined, @@ -204,7 +205,8 @@ export async function cypherCommand( const backend = await getBackend(); const result = await backend.callTool('cypher', { - query, + // #2175: canonical param is statement; the backend still accepts legacy "query". + statement: query, repo: options?.repo, branch: options?.branch, }); diff --git a/gitnexus/src/core/group/service.ts b/gitnexus/src/core/group/service.ts index b957db15b2..a46118bd53 100644 --- a/gitnexus/src/core/group/service.ts +++ b/gitnexus/src/core/group/service.ts @@ -49,6 +49,10 @@ export interface GroupToolPort { query( repo: GroupRepoHandle, params: { + // GroupService always supplies `query` as a string (it resolves the #2175 + // search_query alias before calling the port), so the port contract keeps it + // required here even though the LocalBackend implementation accepts the wider + // `{ query?, search_query? }` shape for the direct MCP callTool path. query: string; task_context?: string; goal?: string; diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 99bec18527..d50bd67acf 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -77,6 +77,23 @@ function looksLikeFilePath(target: string): boolean { const lower = target.toLowerCase(); return SOURCE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext)); } + +/** + * Resolve a string tool param from its canonical name or legacy alias (#2175). + * Returns the first NON-BLANK string of [canonical, legacy] — the canonical (new) + * name is preferred when it carries a real value, otherwise the legacy value is used. + * A blank/whitespace new value therefore does NOT clobber a valid legacy value (e.g. a + * gradually-migrating client that always emits the new key, blank when unset). A + * non-string value (the MCP envelope is not schema-validated, so clients can send any + * JSON type) and an all-blank input resolve to `undefined`, so the caller returns a + * friendly required-param error instead of throwing `TypeError` on `.trim()`. + */ +function resolveAliasString(canonical: unknown, legacy: unknown): string | undefined { + for (const value of [canonical, legacy]) { + if (typeof value === 'string' && value.trim()) return value; + } + return undefined; +} // AI context generation is CLI-only (gitnexus analyze) // import { generateAIContextFiles } from '../../cli/ai-context.js'; @@ -1242,6 +1259,16 @@ export class LocalBackend { } const p = params && typeof params === 'object' ? (params as Record) : {}; + + // #2175: Claude Code drops a tool-call argument named exactly "query", so the + // query/cypher tools advertise "search_query"/"statement" while still accepting the + // legacy "query" key for backward compat. The alias is resolved with `?? ` (new name + // wins) at every consumer site rather than by mutating params here, so precedence is + // uniform and there is no hidden mutation: query()/cypher() read it directly, the + // legacy "search" alias routes through query(), and the cross-repo group-forward + // resolves it self-contained in callToolAtGroupRepo. This is permanent compatibility + // — third-party MCP clients may legitimately send "query", so the alias is not slated + // for removal even if Claude Code's argument handling later changes. if ( (method === 'impact' || method === 'query' || method === 'context') && typeof p.repo === 'string' && @@ -1345,7 +1372,8 @@ export class LocalBackend { private async query( repo: RepoHandle, params: { - query: string; + query?: string; + search_query?: string; task_context?: string; goal?: string; limit?: number; @@ -1353,8 +1381,12 @@ export class LocalBackend { include_content?: boolean; }, ): Promise { - if (!params.query?.trim()) { - return { error: 'query parameter is required and cannot be empty.' }; + // #2175: each consumer resolves the search_query/query alias itself (there is no + // chokepoint mutation in callTool). This also serves the GroupService port, which + // reaches query() carrying only the legacy `query` key. + const rawQuery = resolveAliasString(params.search_query, params.query); + if (!rawQuery?.trim()) { + return { error: 'search_query (or legacy query) parameter is required and cannot be empty.' }; } await this.ensureInitialized(repo); @@ -1362,7 +1394,7 @@ export class LocalBackend { const processLimit = params.limit || 5; const maxSymbolsPerProcess = params.max_symbols || 10; const includeContent = params.include_content ?? false; - const searchQuery = params.query.trim(); + const searchQuery = rawQuery.trim(); // Per-phase timing instrumentation (#553). Records wall time for each // observable sub-step of the search pipeline so production latency can @@ -1932,7 +1964,9 @@ export class LocalBackend { private async cypher( repo: RepoHandle, - request: { query: string; params?: Record }, + // #2175: "statement" is the advertised param; "query" is the legacy alias, + // still accepted (and the field the internal executeCypher() passes). New wins. + request: { query?: string; statement?: string; params?: Record }, ): Promise { await this.ensureInitialized(repo); @@ -1945,8 +1979,15 @@ export class LocalBackend { }; } + const cypherText = resolveAliasString(request.statement, request.query) ?? ''; + if (!cypherText.trim()) { + // Mirror query()'s friendly required-param error instead of letting an empty + // string fall through to a raw LadybugDB prepare error (#2175 review). + return { error: 'statement (or legacy query) parameter is required and cannot be empty.' }; + } + try { - const result = await executeParameterized(repo.lbugPath, request.query, request.params ?? {}); + const result = await executeParameterized(repo.lbugPath, cypherText, request.params ?? {}); return result; } catch (err: any) { const msg = err.message || 'Query failed'; @@ -4820,7 +4861,10 @@ export class LocalBackend { if (method === 'query') { const queryArgs: Record = { name: groupName, - query: params.query, + // #2175: resolve the search_query alias here (new name wins, same rule as the + // local query() handler) so the group path is self-contained and does not depend + // on params being normalized upstream. groupQuery() reads `query`. + query: resolveAliasString(params.search_query, params.query), }; if (typeof params.task_context === 'string') queryArgs.task_context = params.task_context; if (typeof params.goal === 'string') queryArgs.goal = params.goal; diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts index bb900d924d..ec37c196c0 100644 --- a/gitnexus/src/mcp/resources.ts +++ b/gitnexus/src/mcp/resources.ts @@ -292,7 +292,7 @@ async function getReposResource(backend: LocalBackend): Promise { if (repos.length > 1) { lines.push(''); lines.push('# Multiple repos indexed. Use repo parameter in tool calls:'); - lines.push(`# query({query: "auth", repo: "${repos[0].name}"})`); + lines.push(`# query({search_query: "auth", repo: "${repos[0].name}"})`); } return lines.join('\n'); diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index 4df17eb98f..ec3d76e046 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -130,7 +130,14 @@ SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). W inputSchema: { type: 'object', properties: { - query: { type: 'string', description: 'Natural language or keyword search query' }, + // #2175: the legacy `query` key is still accepted by the handler + // (resolveAliasString in local-backend.ts), but is deliberately NOT named in the + // advertised property or its description — surfacing "query" in the schema an LLM + // reads would nudge it to send `query`, the exact argument Claude Code drops. + search_query: { + type: 'string', + description: 'Natural language or keyword search query.', + }, task_context: { type: 'string', description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.', @@ -171,7 +178,7 @@ SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). W 'Optional monorepo service root (relative path, "/" separators). In group mode (@repo), prefix-matches symbol file paths; ignored for a normal repo name. Empty string is rejected server-side.', }, }, - required: ['query'], + required: ['search_query'], }, }, { @@ -224,7 +231,14 @@ TIPS: inputSchema: { type: 'object', properties: { - query: { type: 'string', description: 'Cypher query to execute' }, + // #2175: the legacy `query` key is still accepted by the handler + // (resolveAliasString in local-backend.ts), but is deliberately NOT named in the + // advertised property or its description — surfacing "query" in the schema an LLM + // reads would nudge it to send `query`, the exact argument Claude Code drops. + statement: { + type: 'string', + description: 'Cypher statement to execute.', + }, params: { type: 'object', description: @@ -235,7 +249,7 @@ TIPS: description: 'Repository name or path. Omit if only one repo is indexed.', }, }, - required: ['query'], + required: ['statement'], }, }, { diff --git a/gitnexus/test/integration/local-backend-calltool.test.ts b/gitnexus/test/integration/local-backend-calltool.test.ts index eed46b18d0..00d08a09a9 100644 --- a/gitnexus/test/integration/local-backend-calltool.test.ts +++ b/gitnexus/test/integration/local-backend-calltool.test.ts @@ -119,6 +119,26 @@ withTestLbugDB( expect(result).not.toHaveProperty('partial'); }); + // #2175: end-to-end proof that the renamed parameters work against a real + // index (Claude Code drops a tool arg named exactly "query"). + it('query tool returns results via the new search_query param (#2175)', async () => { + const result = await backend.callTool('query', { search_query: 'login' }); + expect(result).not.toHaveProperty('error'); + expect(result).toHaveProperty('processes'); + expect(result.processes.map((p: any) => p.id)).toContain('proc:login-flow'); + expect(result.process_symbols.map((s: any) => s.id)).toContain('func:login'); + }); + + it('cypher tool executes via the new statement param (#2175)', async () => { + const result = await backend.callTool('cypher', { + statement: 'MATCH (n:Function) RETURN n.name AS name ORDER BY n.name', + }); + expect(result).toHaveProperty('markdown'); + expect(result).toHaveProperty('row_count'); + expect(result.row_count).toBeGreaterThanOrEqual(3); + expect(result.markdown).toContain('login'); + }); + // PR #222 port: the query tool batches per-symbol process/cohesion/content // lookups (N+1 → 2-3 `WHERE n.id IN $nodeIds` queries). These assertions // guard the batch-adaptation hazards that a naive cherry-pick would break: diff --git a/gitnexus/test/unit/ai-context.test.ts b/gitnexus/test/unit/ai-context.test.ts index dc732f1fcd..747bcec1a2 100644 --- a/gitnexus/test/unit/ai-context.test.ts +++ b/gitnexus/test/unit/ai-context.test.ts @@ -920,7 +920,10 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust expect(content).not.toMatch(/gitnexus_(impact|query|context|detect_changes|rename|cypher)/); expect(content).toContain('impact({target: "symbolName", direction: "upstream"})'); expect(content).toContain('detect_changes()'); - expect(content).toContain('query({query: "concept"})'); + // #2175: the generated guidance must advertise the renamed param, never the + // legacy "query" key (Claude Code drops a tool arg named exactly "query"). + expect(content).toContain('query({search_query: "concept"})'); + expect(content).not.toContain('query({query:'); expect(content).toContain('context({name: "symbolName"})'); }); diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index f95b1e4b89..84bdc4e69a 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -85,6 +85,13 @@ vi.mock('../../src/mcp/core/embedder.js', () => ({ getEmbeddingDims: vi.fn().mockReturnValue(384), })); +// #2175: lets the @group-forward path be exercised without real group.yaml infra. +// No existing test in this file uses an @repo, so this mock is inert for them. +const { resolveAtMemberMock } = vi.hoisted(() => ({ resolveAtMemberMock: vi.fn() })); +vi.mock('../../src/core/group/resolve-at-member.js', () => ({ + resolveAtGroupMemberRepoPath: resolveAtMemberMock, +})); + import { LocalBackend, REPO_ID_HASH_LENGTH, @@ -433,12 +440,14 @@ describe('LocalBackend.callTool', () => { it('query tool returns error for empty query', async () => { const result = await backend.callTool('query', { query: '' }); - expect(result.error).toContain('query parameter is required'); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); }); it('query tool returns error for whitespace-only query', async () => { const result = await backend.callTool('query', { query: ' ' }); - expect(result.error).toContain('query parameter is required'); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); }); it('dispatches cypher tool and blocks write queries', async () => { @@ -459,6 +468,186 @@ describe('LocalBackend.callTool', () => { expect(result.row_count).toBe(1); }); + // ── #2175: backward-compatible parameter-alias dispatch ────────────────── + // Claude Code drops a tool-call argument named exactly "query", so the query + // and cypher tools advertise search_query / statement. The handlers must accept + // the new names AND keep accepting the legacy "query" key (verified by the + // existing tests above, which still pass { query: ... }). + + it('query tool accepts the new search_query parameter (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([]); + const result = await backend.callTool('query', { search_query: 'auth' }); + expect(result).toHaveProperty('processes'); + expect(result).toHaveProperty('definitions'); + expect(result).not.toHaveProperty('error'); + }); + + it('query tool prefers search_query over the legacy query when both are given (#2175)', async () => { + const { searchFTSFromLbug } = await import('../../src/core/search/bm25-index.js'); + (executeParameterized as any).mockResolvedValue([]); + + await backend.callTool('query', { search_query: 'newName', query: 'oldName' }); + + // bm25Search passes the resolved search text as arg 0 to searchFTSFromLbug. + const lastTerm = String(vi.mocked(searchFTSFromLbug).mock.calls.at(-1)?.[0] ?? ''); + expect(lastTerm).toBe('newName'); + }); + + it('query tool returns error when neither search_query nor query is provided (#2175)', async () => { + const result = await backend.callTool('query', {}); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); + }); + + it('cypher tool accepts the new statement parameter (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([{ name: 'test', filePath: 'src/test.ts' }]); + const result = await backend.callTool('cypher', { + statement: 'MATCH (n:Function) RETURN n.name AS name, n.filePath AS filePath LIMIT 5', + }); + expect(result).toHaveProperty('markdown'); + expect(result).toHaveProperty('row_count'); + expect(result.row_count).toBe(1); + }); + + it('cypher tool prefers statement over the legacy query when both are given (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([]); + await backend.callTool('cypher', { + statement: 'MATCH (a) RETURN a', + query: 'MATCH (b) RETURN b', + }); + const passedCypher = (executeParameterized as any).mock.calls.at(-1)[1] as string; + expect(passedCypher).toBe('MATCH (a) RETURN a'); + }); + + it('executeCypher (internal API) still works via the legacy query field (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([{ name: 'x' }]); + const result = await backend.executeCypher('test-project', 'MATCH (n) RETURN n LIMIT 1'); + expect(result).not.toHaveProperty('error'); + const passedCypher = (executeParameterized as any).mock.calls.at(-1)[1] as string; + expect(passedCypher).toBe('MATCH (n) RETURN n LIMIT 1'); + }); + + it('query tool returns error for empty search_query (new key) (#2175)', async () => { + const result = await backend.callTool('query', { search_query: '' }); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); + }); + + it('query tool returns error for whitespace-only search_query (new key) (#2175)', async () => { + const result = await backend.callTool('query', { search_query: ' ' }); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); + }); + + it('search legacy alias accepts the new search_query parameter (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([]); + const result = await backend.callTool('search', { search_query: 'auth' }); + expect(result).toHaveProperty('processes'); + expect(result).not.toHaveProperty('error'); + }); + + it('cypher tool returns a friendly required error when neither statement nor query is given (#2175)', async () => { + const result = await backend.callTool('cypher', {}); + expect(result.error).toContain('statement'); + expect(result.error).toContain('parameter is required'); + }); + + // #2175 review: the @group-forward path reads `query` from the forwarded args, so it + // must resolve the search_query alias itself (new name wins, mirroring query()). + it('group-mode query forwards the resolved search_query alias (#2175)', async () => { + resolveAtMemberMock.mockResolvedValue({ ok: true, repoPath: '/tmp/test-project' }); + const groupQuerySpy = vi + .spyOn(backend.getGroupService(), 'groupQuery') + .mockResolvedValue({ ok: true } as any); + + await backend.callTool('query', { + search_query: 'alias-wins', + query: 'legacy-loses', + repo: '@grp', + }); + + expect(groupQuerySpy).toHaveBeenCalledTimes(1); + expect((groupQuerySpy.mock.calls[0][0] as any).query).toBe('alias-wins'); + groupQuerySpy.mockRestore(); + }); + + it('group-mode query still forwards a legacy-only query (#2175)', async () => { + resolveAtMemberMock.mockResolvedValue({ ok: true, repoPath: '/tmp/test-project' }); + const groupQuerySpy = vi + .spyOn(backend.getGroupService(), 'groupQuery') + .mockResolvedValue({ ok: true } as any); + + await backend.callTool('query', { query: 'legacy', repo: '@grp' }); + + expect((groupQuerySpy.mock.calls[0][0] as any).query).toBe('legacy'); + groupQuerySpy.mockRestore(); + }); + + // #2175 review: the MCP envelope is not schema-validated, so a client can send a + // non-string value for a string param. Resolve it to a friendly required-param error + // rather than throwing TypeError on `.trim()` (query() and cypher() both). + it('query tool returns a friendly error (no throw) for a non-string search_query (#2175)', async () => { + const result = await backend.callTool('query', { search_query: 123 as any }); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); + }); + + it('cypher tool returns a friendly error (no throw) for a non-string statement (#2175)', async () => { + const result = await backend.callTool('cypher', { statement: 123 as any }); + expect(result.error).toContain('statement'); + expect(result.error).toContain('parameter is required'); + }); + + // #2175 review (PR #2186): resolution prefers the first NON-BLANK string, so a blank + // new-name value falls back to a valid legacy value instead of clobbering it. + it('query tool: a blank new search_query falls back to a valid legacy query (#2175)', async () => { + const { searchFTSFromLbug } = await import('../../src/core/search/bm25-index.js'); + (executeParameterized as any).mockResolvedValue([]); + + const result = await backend.callTool('query', { search_query: '', query: 'real' }); + + expect(result).not.toHaveProperty('error'); + expect(result).toHaveProperty('processes'); + const lastTerm = String(vi.mocked(searchFTSFromLbug).mock.calls.at(-1)?.[0] ?? ''); + expect(lastTerm).toBe('real'); + }); + + it('query tool: a whitespace-only new search_query falls back to a valid legacy query (#2175)', async () => { + const { searchFTSFromLbug } = await import('../../src/core/search/bm25-index.js'); + (executeParameterized as any).mockResolvedValue([]); + + const result = await backend.callTool('query', { search_query: ' ', query: 'real' }); + + expect(result).not.toHaveProperty('error'); + const lastTerm = String(vi.mocked(searchFTSFromLbug).mock.calls.at(-1)?.[0] ?? ''); + expect(lastTerm).toBe('real'); + }); + + it('query tool: both keys blank still returns the required error (#2175)', async () => { + const result = await backend.callTool('query', { search_query: '', query: ' ' }); + expect(result.error).toContain('search_query'); + expect(result.error).toContain('parameter is required'); + }); + + it('cypher tool: a blank statement falls back to a valid legacy query (#2175)', async () => { + (executeParameterized as any).mockResolvedValue([]); + await backend.callTool('cypher', { statement: '', query: 'MATCH (n) RETURN n LIMIT 1' }); + const passedCypher = (executeParameterized as any).mock.calls.at(-1)[1] as string; + expect(passedCypher).toBe('MATCH (n) RETURN n LIMIT 1'); + }); + + it('group-mode query: a blank new search_query falls back to the legacy query (#2175)', async () => { + resolveAtMemberMock.mockResolvedValue({ ok: true, repoPath: '/tmp/test-project' }); + const groupQuerySpy = vi + .spyOn(backend.getGroupService(), 'groupQuery') + .mockResolvedValue({ ok: true } as any); + + await backend.callTool('query', { search_query: '', query: 'real', repo: '@grp' }); + + expect((groupQuerySpy.mock.calls[0][0] as any).query).toBe('real'); + groupQuerySpy.mockRestore(); + }); + it('dispatches context tool', async () => { (executeParameterized as any).mockResolvedValue([ { diff --git a/gitnexus/test/unit/resources.test.ts b/gitnexus/test/unit/resources.test.ts index 598698b8c7..a0ea2348f1 100644 --- a/gitnexus/test/unit/resources.test.ts +++ b/gitnexus/test/unit/resources.test.ts @@ -388,7 +388,9 @@ describe('readResource', () => { expect(result).toContain('repo parameter'); // The example must use a registered tool name, not the unregistered // `gitnexus_search` / `gitnexus_*` prefix (#2059). - expect(result).toContain('query({query: "auth"'); + // #2175: advertise the renamed param, not the legacy "query" key. + expect(result).toContain('query({search_query: "auth"'); + expect(result).not.toContain('query({query:'); expect(result).not.toMatch(/gitnexus_/); }); }); diff --git a/gitnexus/test/unit/skill-gen.test.ts b/gitnexus/test/unit/skill-gen.test.ts index 8e8800be66..21f993ff30 100644 --- a/gitnexus/test/unit/skill-gen.test.ts +++ b/gitnexus/test/unit/skill-gen.test.ts @@ -650,7 +650,9 @@ describe('generateSkillFiles — file output', () => { ); expect(content).not.toMatch(/gitnexus_(context|query|impact|detect_changes|rename|cypher)/); expect(content).toContain('context({name:'); - expect(content).toContain('query({query:'); + // #2175: advertise the renamed param, not the legacy "query" key. + expect(content).toContain('query({search_query:'); + expect(content).not.toContain('query({query:'); }); /** diff --git a/gitnexus/test/unit/tools.test.ts b/gitnexus/test/unit/tools.test.ts index 298a2ee5cd..156cc7a203 100644 --- a/gitnexus/test/unit/tools.test.ts +++ b/gitnexus/test/unit/tools.test.ts @@ -99,16 +99,23 @@ describe('GITNEXUS_TOOLS', () => { } }); - it('query tool requires "query" parameter', () => { + it('query tool requires "search_query" parameter (renamed from "query" for #2175)', () => { const queryTool = GITNEXUS_TOOLS.find((t) => t.name === 'query')!; - expect(queryTool.inputSchema.required).toContain('query'); - expect(queryTool.inputSchema.properties.query).toBeDefined(); - expect(queryTool.inputSchema.properties.query.type).toBe('string'); + expect(queryTool.inputSchema.required).toContain('search_query'); + // The legacy "query" key must NOT be advertised — Claude Code drops it (#2175). + expect(queryTool.inputSchema.required).not.toContain('query'); + expect(queryTool.inputSchema.properties.query).toBeUndefined(); + expect(queryTool.inputSchema.properties.search_query).toBeDefined(); + expect(queryTool.inputSchema.properties.search_query.type).toBe('string'); }); - it('cypher tool requires "query" parameter', () => { + it('cypher tool requires "statement" parameter (renamed from "query" for #2175)', () => { const cypherTool = GITNEXUS_TOOLS.find((t) => t.name === 'cypher')!; - expect(cypherTool.inputSchema.required).toContain('query'); + expect(cypherTool.inputSchema.required).toContain('statement'); + expect(cypherTool.inputSchema.required).not.toContain('query'); + expect(cypherTool.inputSchema.properties.query).toBeUndefined(); + expect(cypherTool.inputSchema.properties.statement).toBeDefined(); + expect(cypherTool.inputSchema.properties.statement.type).toBe('string'); expect(cypherTool.inputSchema.properties.params).toBeDefined(); expect(cypherTool.inputSchema.properties.params.type).toBe('object'); expect(cypherTool.inputSchema.properties.params.description).toContain('prepared statement');