Files
pad/internal
xarmian fe05c170ea feat(mcp): v0.2 catalog read-only tools (workspace, collection, project, role, search) (TASK-980) (#353)
* feat(mcp): v0.2 catalog read-only tools (workspace, collection, project, role, search) (TASK-980)

Second commit of TASK-970's 3-stage rollout (PLAN-969). Adds 5 read-only
tools to the v0.2 catalog. v0.1 cmdhelp walker stays live alongside;
TASK-981 retires it.

Tools added:
- pad_workspace: list, members, invite, storage, audit-log
- pad_collection: list, create
- pad_project: dashboard, next, standup, changelog
- pad_role: list, create, delete
- pad_search: query (dispatches to "item search"; cross-workspace FTS)

Catalog adjustments from DOC-978's original list:
- Dropped pad_workspace.action: get — no equivalent CLI command exists.
- Dropped pad_collection.action: schema — schema is in `collection list`
  output; can be added later if dogfooding shows demand.

Latent bug fix in fan-out handler:
- makeFanOutHandler now strips the catalog's `action` routing key from
  the input map before invoking the action handler. Required because
  some CLI commands (workspace audit-log) declare their own `--action`
  flag — without stripping, BuildCLIArgs would silently emit
  `--action audit-log` instead of the user's filter value. The bug was
  latent in TASK-979 (pad_meta's actions don't dispatch) and surfaces
  in TASK-980 with workspace audit-log.

Custom handler for workspace.audit-log:
- The CLI's `--action <filter>` flag would still collide if exposed
  directly. Schema exposes it as `action_filter`; actionWorkspaceAuditLog
  renames to `action` before dispatch. All other audit-log flags flow
  through unchanged.

Tests:
- TestReadOnlyCatalog_AllToolsRegistered locks the new catalog entries.
- TestReadOnlyCatalog_ActionsMatchCmdhelp verifies every passThrough
  cmdPath resolves in cmdhelp (catches drift at test time).
- TestPadWorkspaceAuditLog_RenamesActionFilter / _ForwardsWithoutFilter
  pin the audit-log rename behavior in both directions.
- TestMakeFanOutHandler_StripsActionFromInput pins the strip behavior
  so no future passThrough can leak the routing key.

Parent: TASK-980 → TASK-970 → PLAN-969.

* fix(mcp): tighten v0.2 catalog tests per Codex review (round 1)

Two P3 findings on test gaps in TASK-980's catalog_readonly_test.go:

1. TestReadOnlyCatalog_ActionsMatchCmdhelp checked a hardcoded `expected`
   table against a hardcoded `liveCmdhelpDoc`. Catalog action drift
   (rename, removal, addition) and CLI command renames could pass
   silently because both halves were under test control. Now the test
   does three-way validation:
   - Every expected cmdPath resolves in liveCmdhelpDoc (catches typos
     in our own table — the original check).
   - Every catalog action (modulo inline-handling tools like pad_meta)
     has an expected entry (catches new actions added without coverage).
   - Every expected entry has a real catalog action (catches stale test
     entries that outlive the action).

2. TestReadOnlyCatalog_AllToolsRegistered only verified expected names
   were present; an accidental 7th tool would pass silently. Now fails
   on unexpected entries AND on Catalog length mismatch.

A `skipTools` set lets us exclude pad_meta (whose actions are inline,
not dispatched) and document the exclusion. TASK-981 will extend the
expected{} map for pad_item.

Parent: TASK-980 → TASK-970 → PLAN-969.

* fix(mcp): exercise catalog actions through fake dispatcher per Codex review (round 2)

Codex P2: TestReadOnlyCatalog_ActionsMatchCmdhelp + AllToolsRegistered
were tightened in round 1, but they still didn't catch a class of drift —
e.g. flipping pad_search.query from passThrough([]string{"item","search"})
to passThrough([]string{"some","other"}) would pass the bijection check
because the action name still matches. The expected{} table was never
exercised against the actual handler.

Add TestReadOnlyCatalog_ActionsDispatchExpectedCmdPath: invokes every
catalog action through a fake dispatcher with a maximal input fixture
(satisfies all required positionals across the read-only surface) and
asserts the captured cmdPath matches the expected table.

Closes the catalog → dispatch drift hole. Now if anyone changes a
passThrough cmdPath without updating expected{}, this test fails
loudly with the actual dispatched path printed in the error.

Parent: TASK-980 → TASK-970 → PLAN-969.
2026-05-01 17:45:18 -04:00
..
2026-03-26 01:52:36 +00:00