Files
summercms/.planning/phases/19.7-generic-mcp-module/19.7-VALIDATION.md

23 KiB

phase, slug, status, nyquist_compliant, wave_0_complete, created
phase slug status nyquist_compliant wave_0_complete created
19.7 generic-mcp-module validated true true 2026-10-21

Phase 19.7 — Validation Strategy

Per-phase validation contract for feedback sampling during execution. Source: 19.7-RESEARCH.md § Validation Architecture.


Test Infrastructure

Property Value
Framework Go testing + testcontainers-go (Postgres) + miniredis + go-sdk client; vitest for the admin SPA seam
Config file none (Go); admin/vitest.config.* for the SPA
Quick run command go test -short ./modules/pier ./modules/surf ./modules/bouncer ./modules/cabana ./modules/wristband ./modules/pact
Full suite command go vet ./... && go test ./... (framework) + go test ./... in each touched plugin + npm --prefix admin run test + scripts/check-phase19.7.sh
Estimated runtime ~120 seconds (quick ~20 s)

Sampling Rate

  • After every task commit: quick run command for the touched module(s)
  • After every plan wave: framework go vet ./... && go test ./... plus the touched plugin suites
  • Before /msd-verify-work: bash scripts/check-phase19.7.sh --all green on a Docker host (every stage PASS, no NOT RUN docker)
  • Max feedback latency: 120 seconds

Per-Task Verification Map

Filled in by the planner per task; the behaviour → test map is:

Ref Behavior Test Type Automated Command File Exists Status
SC-1/D-30/D-33 disabled → nothing mounted; enabled without the tag → boot error; tagged → /mcp stream routes and PRM documents; collision names the owner unit go test -short ./modules/surf -run '^TestPierMount$' and go test -short -tags summercms_mcp ./modules/surf -run '^TestPierMount$' ✅ modules/surf/pier_mount_test.go ✅ green
SC-1 SSE stream flushes through the streaming route kind; throttle, CORS profile and access-log pattern kept; buffered Flush a no-op unit go test -short ./modules/surf -run '^TestStreamingRoute$' ✅ modules/surf/streaming_test.go ✅ green
SC-2/D-10..D-12/D-14 binding/duplicate/name boot errors; prefixing; annotations by method unit go test -short ./modules/pier -run '^TestCatalog$' ✅ modules/pier/catalog_test.go ✅ green
D-11/D-13 arg split, omitted fields absent, no outer credentials, isError with the route body, {"data":…}, size cap, call timeout unit go test -short ./modules/pier -run '^TestDispatch$' ✅ modules/pier/dispatch_test.go ✅ green
D-15/D-16/D-22 non-token routes refused; requirement union; method-aware provider; admin token scope per method unit go test -short ./modules/pier -run '^TestGuardRules$'; go test -short ./modules/cabana -run '^TestScopeLimitForMethods$' ✅ modules/pier/guard_rules_test.go, modules/cabana/scope_limit_for_test.go ✅ green
D-17 marker honoured in-process only; not forgeable from HTTP; frontend marker refused by the backend guard and ResolveBackend unit + Postgres go test -short ./modules/bouncer ./modules/cabana -run '^TestDispatchMarker$'; Postgres part go test ./modules/cabana -run '^TestDispatchMarker$'; gate stage marker ✅ modules/bouncer/dispatch_test.go, modules/cabana/dispatch_marker_test.go ✅ green
SC-3/D-20/D-23 per-principal tools/list; read grant hides write tools; no subject, no tool; unlisted call never reaches the route; union of guards integration go test -short ./modules/pier -run '^TestToolsList$' ✅ modules/pier/tools_list_test.go ✅ green
D-25 session binding, DELETE, cross-instance rehydration, TTL, per-binding cap (Redis and memory) integration go test -short ./modules/pier -run '^TestSessions$' ✅ modules/pier/sessions_test.go ✅ green
D-26/D-27 pub/sub list_changed; link change and cache bypass; revocation and deletion end sessions and evict stateless servers integration go test -short ./modules/pier -run '^TestFanOut$' ✅ modules/pier/fanout_test.go ✅ green
D-09 plugin frontend permission codes in editors; duplicates dropped; saved codes granted Postgres user plugin GOWORK=off go test . -run '^TestFrontendPermissionCatalog$' ✅ plugins/golem15/user/frontend_permission_catalog_test.go ✅ green
D-03..D-08 link proof, one-shot, fresh sign-in at the second, 1:1, admin set, events, per-request lookup Postgres user plugin GOWORK=off go test . -run '^TestBackendLink$' ✅ plugins/golem15/user/backend_link_test.go ✅ green
D-15/D-35 user guard: parser refusals, deleted user, expired token, dispatch credential against user.scope Postgres user plugin GOWORK=off go test . -run '^TestUserMCPGuard$' ✅ plugins/golem15/user/mcp_guard_test.go ✅ green
D-04 account contribution seam; Applies and permission filtering; 409; interactive only; transaction; existing payloads unchanged unit + Postgres + vitest go test -short ./modules/cabana -run '^TestAccountContributions$' (Postgres part without -short); npm --prefix admin run test; gate check account.go unchanged since 51d088a ✅ modules/cabana/account_contributions_test.go, admin/tests/account/AccountContributions.test.ts ✅ green
D-34 prompted record actions: localized prompt, trim and cap, refusals, empty Value for a plain action unit + Postgres + vitest go test -short ./modules/cabana -run '^TestRecordActionPrompt$'; go test ./modules/cabana -run '^TestRecordActionPromptRun$'; npm --prefix admin run test ✅ modules/cabana/record_action_prompt_test.go, admin/tests/form/RecordActionsPrompt.test.ts ✅ green
D-21 backend scope and consent line unit + Postgres go test -short ./modules/wristband -run '^TestMCPScopes$'; oauthserver plugin go test . -run '^TestMCPConsent$' ✅ modules/wristband/mcp_scopes_test.go, sm-oauthserver-plugin/mcp_consent_test.go ✅ green
SC-4 oas_at_ verifier refuses OIDC/revoked/expired/foreign/inactive-client/deleted-user tokens; conditional stamp Postgres oauthserver plugin go test . -run '^TestMCPTokenVerifier$' ✅ sm-oauthserver-plugin/mcp_verifier_test.go ✅ green
D-15/D-16 Journal tools compile through pier; input keys are the routes' keys; prompt unit blog plugin go test -short . -run '^TestJournalMCP$' ✅ sm-journal-plugin/mcp_test.go ✅ green
SC-4/D-08/D-18/D-20/D-26 end-to-end in sm-demo-app incl. unlink list_changed, read grant, revoked grant, personal tokens off e2e cd ../sm-demo-app && go test -tags summercms_mcp . -run '^TestMCPEndToEnd$' ✅ sm-demo-app/mcp_e2e_test.go ✅ green
D-31 plugins additive against their pre-phase commits; consumers build and pass at bumped pins build bash scripts/check-phase19.7.sh --plugins and --consumers ✅ scripts/check-phase19.7.sh ✅ green
A-CR-01/A-IN-01/A-IN-06 reaper closes expired copies (memory, Redis per instance), store error keeps the copy, GET stream ends when its record is gone, ephemeral label integration go test -short ./modules/pier -run '^TestSessionReaper$' (also -tags summercms_mcp) ✅ modules/pier/reaper_test.go ✅ green
A-WR-03 50 concurrent initializes open exactly max_per_binding (memory, two instances on miniredis); a failed initialize releases its slot; Count/Put fallback integration go test -short ./modules/pier -run '^TestSessionReserve$' ✅ modules/pier/reserve_test.go ✅ green
A-WR-07 a call, a DELETE and a stateless request finish while an initialize blocks in a guard check integration go test -short -race ./modules/pier -run '^TestLockScope$' ✅ modules/pier/lock_scope_test.go ✅ green
A-WR-04 a returning binding with read write, or with a backend subject, lists the new tools on both paths; narrowing hides them integration go test -short ./modules/pier -run '^TestScopeWidening$' ✅ modules/pier/widening_test.go ✅ green
A-WR-05/A-WR-06 a stateless-only Redis instance applies a revocation; a listen stream ends with its bearer; LRU eviction closes the stream integration go test -short ./modules/pier -run '^TestStatelessFanOut$' ✅ modules/pier/stateless_fanout_test.go ✅ green
A-WR-09 a subject without TokenID is 401; two users' personal tokens get distinct bindings integration go test -short ./modules/pier -run '^TestPersonalTokenBinding$' ✅ modules/pier/personal_binding_test.go ✅ green
A-WR-01/A-WR-02/A-WR-08/A-IN-04/A-IN-05/A-IN-08 traversal and empty segments refused before the route; missing path value refused; fixed provider error text; large integers keep their digits; outer Host and forwarding headers; contradicting dispatch refused unit go test -short ./modules/pier -run '^TestDispatchHardening$' ✅ modules/pier/dispatch_hardening_test.go ✅ green
A-WR-02/A-IN-07 an omitempty path field and an embedded struct on a GET tool fail boot unit go test -short ./modules/pier -run '^TestCatalogPathFields$' ✅ modules/pier/catalog_path_fields_test.go ✅ green
C-IN-01 a Writes prompt is listed only for read and write grants integration go test -short ./modules/pier -run '^TestPromptWriteCeiling$'; blog plugin go test -short . -run '^TestJournalPromptWrites$' ✅ modules/pier/prompt_ceiling_test.go, sm-journal-plugin/mcp_prompt_writes_test.go ✅ green
B-WR-04/C-IN-02 a TokenID event ends only that token's sessions, a ClientID event only that client's, neither all; through Redis between instances integration go test -short ./modules/pier -run '^TestTokenScopedRevoke$' ✅ modules/pier/token_events_test.go ✅ green
B-IN-04 a resolver sees towel.ClientIP (RemoteAddr host, or the host's trusted-proxy answer); towel round trip; ResolveBackendToken logs remote unit + Postgres go test -short ./modules/pier -run '^TestResolveClientAddress$'; go test -short ./modules/towel -run '^TestClientIP$'; go test ./modules/cabana -run '^TestResolveBackendTokenRemote$' ✅ modules/pier/token_events_test.go, modules/towel/client_ip_test.go, modules/cabana/token_resolver_remote_test.go ✅ green
C-CR-02/D-05 auth_time at mint (both audiences), unchanged through every refresh path, absent for legacy tokens; VerifyAuthTime refusals unit go test -short ./modules/bouncer -run '^TestAuthTime$' ✅ modules/bouncer/auth_time_test.go ✅ green
E-2 (framework) ValidTokenScopeName table; HasFrontendTokenScopes discovered; cabana's scope validation agrees with pact unit go test -short ./modules/pact -run '^TestFrontendTokenScopes$'; go test -short ./modules/cabana -run '^TestTokenScopeNameParity$' ✅ modules/pact/frontend_token_scopes_test.go, modules/cabana/token_scope_name_test.go ✅ green
B-WR-03 javascript:, data:, vbscript: and // action URLs dropped and logged without the URL; https and same-origin kept unit go test -short ./modules/cabana -run '^TestAccountContributionURL$' ✅ modules/cabana/account_contributions_hardening_test.go ✅ green
B-WR-06/B-IN-03 a failing Status marks only its section, a failing Applies drops only its action, 200; dynamic status text not looked up unit go test -short ./modules/cabana -run '^TestAccountContributionsIsolation$' ✅ modules/cabana/account_contributions_hardening_test.go ✅ green
B-IN-02 a non-string prompt payload answers the localized action_prompt_invalid text (en, pl) unit go test -short ./modules/cabana -run '^TestRecordActionPromptLocalized$' ✅ modules/cabana/record_action_prompt_localized_test.go ✅ green
B-WR-04 revoking one g15_ token fires one revoked event with its TokenID Postgres go test ./modules/cabana -run '^TestAccountTokenRevokeEvent$' ✅ modules/cabana/account_token_events_test.go ✅ green
B-WR-01/B-WR-02 plain keys ignored from an open shadow root and a custom field element in a form; Ctrl+S and Ctrl+Enter save the taglist draft vitest npm --prefix admin run test (tests/app/hotkeysEditing.test.ts required by the gate) ✅ admin/tests/app/hotkeysEditing.test.ts ✅ green
B-WR-03/B-WR-06/B-IN-01 no link and no window.open for a javascript: URL; section error text; toast and kept sections on a failed reload; My account confirm and stale texts vitest npm --prefix admin run test ✅ admin/tests/account/AccountContributions.test.ts ✅ green
E-2 E-2's acceptance list item by item (mint and middleware.Scope, the 403 message, 422, listing, discovery in en and pl), per-application catalog, legacy fallback Postgres user plugin GOWORK=off go test . -run '^TestMemberTokenScopes$' ✅ plugins/golem15/user/member_token_scopes_test.go ✅ green
E-2 duplicate (both plugins named), redeclared built-ins, bad names, empty label and a missing phrase key fail boot unit user plugin GOWORK=off go test -short . -run '^TestMemberTokenScopesBoot$' ✅ plugins/golem15/user/member_token_scopes_test.go ✅ green
C-CR-02 the link proof against login, request, refresh and re-login orders, no auth_time, another user's session token Postgres user plugin GOWORK=off go test . -run '^TestBackendLinkAuthTime$' ✅ plugins/golem15/user/backend_link_auth_time_test.go ✅ green
C-WR-01/C-IN-06 POST show answers like GET with no-store; fragment URL; backend_links.page_path Postgres user plugin GOWORK=off go test . -run '^TestBackendLinkShowPost$' ✅ plugins/golem15/user/backend_link_show_test.go ✅ green
C-CR-01 AccessRevokedEvent from ban, reset, change and admin-set password (ids, Before, reason, transaction, subject event after commit, rollback); banned user loses the link Postgres user plugin GOWORK=off go test . -run '^TestAccessRevocation$' ✅ plugins/golem15/user/access_revocation_test.go ✅ green
C-IN-04 MCP personal-token resolve stamps last_used_ip Postgres user plugin GOWORK=off go test . -run '^TestUserMCPGuardClientIP$' ✅ plugins/golem15/user/mcp_guard_client_ip_test.go ✅ green
C-IN-02 DELETE /tokens/{id} fires one revoked event with that TokenID Postgres user plugin GOWORK=off go test . -run '^TestAPITokenRevokeEvent$' ✅ plugins/golem15/user/api_token_events_test.go ✅ green
E-1 logo fileupload after the avatar; upload stored with field logo; OrganisationLogoFields with and without a logo; delete removes both attachments Postgres user plugin GOWORK=off go test . -run '^TestOrganisationLogo$' ✅ plugins/golem15/user/organisation_logo_test.go ✅ green
C-CR-01 the verifier and refresh refuse pre-cutoff (same-second), banned and deleted users without a revoke listener; untruncated Before Postgres oauthserver plugin go test . -run '^TestMCPVerifierRevocation$' ✅ sm-oauthserver-plugin/mcp_revocation_test.go ✅ green
C-CR-01/C-WR-02/C-IN-02 MCP and OIDC grants before Before revoked, later kept; consents and unused codes deleted; link changes revoke only backend tokens with client events after commit; authorized-apps revoke event Postgres oauthserver plugin go test . -run '^TestMCPGrantRevocation$' ✅ sm-oauthserver-plugin/mcp_revocation_test.go ✅ green
SC-4/C-CR-01/C-WR-02/C-IN-01/C-IN-02 demo revocation matrix through the SDK client: ban via the bulk route, unlink and a new consent, read-only prompts, one usr_ token revoked e2e cd ../sm-demo-app && go test -tags summercms_mcp . -run '^TestMCPRevocationEndToEnd$' ✅ sm-demo-app/mcp_revocation_e2e_test.go ✅ green
C-WR-01 readLinkCode and isLinkCode vitest pnpm --dir ../sm-demo-app/vue-demo-app run test (app/lib/linkCode.test.ts required by the gate) ✅ sm-demo-app/vue-demo-app/app/lib/linkCode.test.ts ✅ green
T-19.7-64 (P-40) workspace check, consumers built against the unreleased user plugin, unreleased-pin policy and PHASE197_RELEASED strict mode gate bash scripts/check-phase19.7.sh --self-test, --plugins, --consumers; PHASE197_RELEASED=1 bash scripts/check-phase19.7.sh --consumers exits 1 before the release ✅ scripts/check-phase19.7.sh ✅ green
A-IN-02 cooler's KeyPrefix and keyPrefix carry their own doc comments docs bash scripts/check-phase19.7.sh --docs ✅ scripts/check-phase19.7.sh ✅ green
RA-WR-01 fan-out never waits on a session lock under the router lock; re-filter guard checks bounded by mcp.call_timeout integration go test -short -race ./modules/pier -run '^TestLockScopeUnderWidening$' ✅ modules/pier/lock_scope_test.go ✅ green
RA-WR-02 the stateless fingerprint ignores the OAuth token id; a narrow token does not flip a shared server's tool set mid-call unit + integration go test -short ./modules/pier -run '^(TestFingerprintIgnoresTokenID|TestStatelessSharedBinding)$' ✅ modules/pier/fingerprint_test.go, modules/pier/stateless_fanout_test.go ✅ green
RA-WR-03 a guard error while widening or recomputing (stateless, stateful, on an event) does not narrow the session integration go test -short ./modules/pier -run '^TestWideningGuardError$' ✅ modules/pier/widening_test.go ✅ green
RA-WR-04 LRU eviction spares a server with a listen stream; a revocation still ends the stream integration go test -short ./modules/pier -run '^TestStatelessFanOut$' ✅ modules/pier/stateless_fanout_test.go ✅ green
RA-WR-05 prompt Render errors stay out of the client's answer (stateful and stateless) integration go test -short ./modules/pier -run '^TestPromptRenderError$' ✅ modules/pier/prompt_ceiling_test.go ✅ green
RA-IN-02 stream bearer re-checks carry the client address integration go test -short ./modules/pier -run '^TestStreamRecheckClientIP$' ✅ modules/pier/token_events_test.go ✅ green
RA-IN-04 the non-reserving fallback writes the record before building the server; a failed initialize deletes it (Count-then-Put race deferred) integration go test -short ./modules/pier -run '^TestSessionReserve$' ✅ modules/pier/reserve_test.go ✅ green
RA-IN-05 an admin personal token whose scopes all resolve to nothing gets "the token's scopes no longer grant this route", not "account unknown"; the listing check is a refusal; known scope, missing admin and OAuth subject unchanged Postgres go test ./modules/pier -run '^TestBackendScopeUnavailable$' ✅ modules/pier/backend_scope_unavailable_test.go ✅ green
RA-WR-06/RA-IN-06 the verdict is qualified while demo pins are unreleased; a silent failing pin check is refused gate bash scripts/check-phase19.7.sh --self-test (pins-test-only-unrecorded, pins-silent-failure) ✅ scripts/check-phase19.7.sh ✅ green
RB-WR-01/RB-IN-01 Ctrl+S and Ctrl+Enter commit a draft inside an open or closed field-type shadow root; control-character and bare https:// URLs render no link and open no window vitest npm --prefix admin run test ✅ admin/tests/app/hotkeysEditing.test.ts, admin/tests/account/AccountContributions.test.ts ✅ green
RB-WR-02 a padded or whitespace-carrying token scope name fails boot unit go test -short ./modules/cabana -run '^TestTokenScopeNameParity$' ✅ modules/cabana/token_scope_name_test.go ✅ green
RB-IN-02 a failing Applies marks its My account section unit go test -short ./modules/cabana -run '^TestAccountContributionsAppliesFailureFlagsSection$' ✅ modules/cabana/account_contributions_hardening_test.go ✅ green
RC-WR-01 a backend grant acts only as the administrator it was consented for: decide 409 matrix, binding through code, exchange, rotation and verifier, link change, revoke racing a rotation Postgres + integration + e2e oauthserver plugin go test . -run '^TestMCPBackendBinding$'; go test -short ./modules/pier -run '^(TestToolsList|TestFanOut)$'; demo TestMCPEndToEnd ✅ sm-oauthserver-plugin, modules/pier/tools_list_test.go, modules/pier/fanout_test.go, sm-demo-app/mcp_e2e_test.go ✅ green
RC-CR-01/RC-WR-02 a self-service password change revokes grants rotated after the session's iat and keeps the session; a ban alone refuses the verifier and rotation Postgres user plugin GOWORK=off go test . -run '^TestAccessRevocation$'; oauthserver plugin go test . -run '^(TestMCPGrantRevocation|TestMCPVerifierRevocation)$' ✅ plugins/golem15/user/access_revocation_test.go, sm-oauthserver-plugin/mcp_revocation_test.go ✅ green
RC-IN-01/RC-IN-03 a link code in the query string is ignored on POST; avatar and logo fallbacks use the public path Postgres user plugin GOWORK=off go test . -run '^(TestBackendLinkShowPost|TestAvatarURL|TestOrganisationLogo)$' ✅ plugins/golem15/user/backend_link_show_test.go, plugins/golem15/user/organisation_logo_test.go ✅ green

Status legend: ✅ green · ❌ red · ⚠️ flaky. Every row is green as of the plan 09 gate run (bash scripts/check-phase19.7.sh --all, every stage PASS). The rows after D-31 are the gap closure (plans 06-09); the RA-/RB-/RC- rows are the re-review fixes (19.7-REREVIEW-FIX.md), added by the 2026-10-24 validation audit.


Wave 0 Requirements

  • scripts/check-phase19.7.sh — phase gate (pattern: scripts/check-phase19.4.sh)
  • pier test helpers: fake guard provider, fake verifier, two-instance harness on one miniredis, SDK client with a bearer-injecting http.Client (modules/pier/helpers_test.go)
  • user-plugin Postgres fixtures for links and frontend permission rows (the admin harness, startLink/confirmLink, the seeded permission rows)
  • additive-API checks for sm-user-plugin, sm-journal-plugin, sm-oauthserver-plugin (gate stage plugins)

Manual-Only Verifications

Behavior Requirement Why Manual Test Instructions
Real MCP client (Claude) connects once via OAuth and sees both tool sets SC-4 external client and browser consent Add the demo's /mcp URL to a client, sign in, link an admin, request backend, call a Journal tool
go-sdk legitimacy check before go get D-24 supply-chain human check Confirm module path, version and maintainers in the decision note

Validation Sign-Off

  • All tasks have <automated> verify or Wave 0 dependencies
  • Sampling continuity: no 3 consecutive tasks without automated verify
  • Wave 0 covers all MISSING references
  • No watch-mode flags
  • Feedback latency < 120s for the quick run (the full gate with Docker takes several minutes)
  • Nyquist compliance recorded in the frontmatter

Approval: plan 19.7-05 (the gate passes in full); extended by plan 19.7-09 for the gap closure (the gate passes in full)


Validation Audit 2026-10-24

The audit compared the map with the re-review fixes (19.7-REREVIEW-FIX.md). Fifteen behaviours had tests but no row, and they are now in the map. RA-IN-01, RA-IN-03, RB-IN-04 and RC-IN-02 changed only documentation, so they get no row. RA-IN-05 had no test, and TestBackendScopeUnavailable (49a3df6) now covers it.

Metric Count
Gaps found 1
Resolved 1
Escalated 0