Skip to content

test(operations): route read_notes through the registration gate - #117

Merged
AlexMost merged 7 commits into
mainfrom
worktree-operations-tests-through-gate
Aug 30, 2026
Merged

AlexMost merged 7 commits into
mainfrom
worktree-operations-tests-through-gate

Conversation

@AlexMost

Copy link
Copy Markdown
Owner

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.

registerTool wraps each tool's schema with coercion and z.object(...).strict(), then safeParses 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:

Test's old pin What the gate actually returns
paths: '' → INVALID_ARGUMENT INVALID_PARAMS — paths: Too small: expected string to have >=1 characters
paths: [] → INVALID_ARGUMENT INVALID_PARAMS — paths: Too small: expected array to have >=1 items
51 paths → INVALID_ARGUMENT INVALID_PARAMS — paths: Too big: expected array to have <=50 items
content: 'none' → INVALID_ARGUMENT INVALID_PARAMS — content: Invalid option
legacy fields key silently stripped INVALID_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 the string → string[] widening survives, inlined into its one caller. No client-observable behaviour changes: production already returned INVALID_PARAMS for every case above.

New helpers (test/_gate.ts), adopted by the next three PRs across ~220 call sites:

  • callTool<T>(reg, args) — calls through reg.handler, unwraps structuredContent, and re-throws errors so existing rejects.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 populate structuredContent.
  • expectToolError(reg, args) — for tests whose subject is the CallToolResult envelope itself.

New coverage read_notes never had: a stringified paths array coercing to two paths, and a vault argument rejected as an unrecognized key in single-vault mode (vaultParamShape contributes no vault param when one vault is registered).

Review found and fixed one real hazard before it multiplied: callTool initially minted an UNKNOWN_ERROR code 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 plain Error when the envelope carries no code.

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-gate

Spec deltas ride along in the change directory and sync on archive in PR 4: read-notes-content-modes (the invalid-content scenario said INVALID_ARGUMENT; the legacy-fields scenario said the key "is ignored" — the latter also contradicted tolerant-arguments §"Unknown keys remain rejected") and tolerant-arguments (the strict/coercing boundary applies uniformly; vault is an unknown key in single-vault mode).

Checks

  • npm test — 1302 passed (105 files)
  • npm run lint — eslint . clean
  • npm run typecheck — tsc --noEmit clean
  • openspec validate --all — 19/19

🤖 Generated with Claude Code

AlexMost and others added 7 commits August 28, 2026 20:25
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>
@AlexMost
AlexMost merged commit c6dc524 into main Aug 30, 2026
2 checks passed
@AlexMost
AlexMost deleted the worktree-operations-tests-through-gate branch August 30, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant