Repository navigation
test(operations): route read_notes through the registration gate - #117
Merged
Merged
Conversation
Proposal, design, delta specs, tasks and plan for routing tool-contract tests through the registration gate. Refs #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite called buildReadNotesTool(...).handler directly, entering past the coercing, strict, INVALID_PARAMS-throwing gate every MCP client crosses. Four tests pinned INVALID_ARGUMENT for inputs zod rejects first, and one asserted an unknown 'fields' key is stripped when production rejects it. Refs #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
validateReadNotesInput re-checked the paths type, the empty-string case, the 1-50 bound and the content enum after the registration gate had already rejected each one as INVALID_PARAMS. Three branches were unreachable in production; the fourth duplicated a zod type check. Only the string-to-array widening survives, inlined into its one caller. Refs #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… never emits callTool reconstructed every isError result as a ToolHandlerError, inventing the code UNKNOWN_ERROR for the no-code envelope shape toToolErrorResponse actually emits for non-ToolHandlerError failures. Reconstruct faithfully instead: re-throw a ToolHandlerError only when the payload carries a code, otherwise a plain Error with no invented code. Loosen ToolErrorPayload.code to string | undefined to match. Add tests for both envelope shapes and for the non-JSON-text diagnostic branch that tasks.md already claimed was covered. Also stop pinning zod's exact "Unrecognized key" wording in read-notes.test.ts in favor of asserting the code, our own <root> path, and that the message names the offending key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mark 1.1-1.7 done and correct 1.7's wording to reflect that it was delivered as its own commit rather than folded into 1.3-1.6, matching what actually shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review amended `callTool` (no invented `UNKNOWN_ERROR` code) and the unrecognized-key assertion idiom (`expect.stringContaining(key)` instead of zod's literal wording), but plan.md still carried the pre-review snippets in 18 places. Tasks 4-19 copy those snippets verbatim, so the next PR would have reintroduced both findings across ten files. Refs #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Routes
read_notes's tests through the registration gate every MCP client crosses, and deletes the post-gate validation that gate makes unreachable. First of four PRs for #112.registerToolwraps each tool's schema with coercion andz.object(...).strict(), thensafeParses before the handler runs. The operations suite never crossed that seam — 100 handler-direct call sites, zero through a registration — so five tests documented behaviour production does not have:paths: ''→INVALID_ARGUMENTINVALID_PARAMS—paths: Too small: expected string to have >=1 characterspaths: []→INVALID_ARGUMENTINVALID_PARAMS—paths: Too small: expected array to have >=1 itemsINVALID_ARGUMENTINVALID_PARAMS—paths: Too big: expected array to have <=50 itemscontent: 'none'→INVALID_ARGUMENTINVALID_PARAMS—content: Invalid optionfieldskey silently strippedINVALID_PARAMS—Unrecognized key: "fields"Three of
validateReadNotesInput's four branches were therefore unreachable in production and green only because the tests entered past the gate; the fourth duplicated a zod type check. All four are deleted — only thestring → string[]widening survives, inlined into its one caller. No client-observable behaviour changes: production already returnedINVALID_PARAMSfor every case above.New helpers (
test/_gate.ts), adopted by the next three PRs across ~220 call sites:callTool<T>(reg, args)— calls throughreg.handler, unwrapsstructuredContent, and re-throws errors so existingrejects.toMatchObject({ code })assertions keep working. Falls back to the text channel for the two tools that resolve with arrays (find_duplicates,get_similar_notes), which never populatestructuredContent.expectToolError(reg, args)— for tests whose subject is theCallToolResultenvelope itself.New coverage
read_notesnever had: a stringifiedpathsarray coercing to two paths, and avaultargument rejected as an unrecognized key in single-vault mode (vaultParamShapecontributes novaultparam when one vault is registered).Review found and fixed one real hazard before it multiplied:
callToolinitially minted anUNKNOWN_ERRORcode for the code-less error envelope — a string production never emits. A future author reading it off a failure diff would have pinned it, re-seeding the exact bug this PR deletes. It now re-throws a plainErrorwhen the envelope carries nocode.No test broke during the migration — every existing fixture already returned one reader item per requested path, and no input carried an undeclared key.
Tracking
Refs #112
OpenSpec change slug:
operations-tests-through-gateSpec deltas ride along in the change directory and sync on archive in PR 4:
read-notes-content-modes(the invalid-contentscenario saidINVALID_ARGUMENT; the legacy-fieldsscenario said the key "is ignored" — the latter also contradictedtolerant-arguments§"Unknown keys remain rejected") andtolerant-arguments(the strict/coercing boundary applies uniformly;vaultis an unknown key in single-vault mode).Checks
npm test— 1302 passed (105 files)npm run lint—eslint .cleannpm run typecheck—tsc --noEmitcleanopenspec validate --all— 19/19🤖 Generated with Claude Code