docs(19.7-09): complete the gap-closure tests, gate and security review plan

This commit is contained in:
2026-10-23 19:03:06 +02:00
parent e1f489058c
commit 883ac5133c

View File

@@ -0,0 +1,373 @@
---
phase: 19.7-generic-mcp-module
plan: 09
subsystem: testing
status: complete
gap_closure: true
tags: [pier, bouncer, cabana, pact, towel, admin-spa, vitest, sm-user-plugin, sm-oauthserver-plugin, sm-journal-plugin, sm-demo-app, vue-demo-app, gate, security-review, revert-check, gap-closure]
requires:
- phase: 19.7-06
provides: pier lane A fixes (reaper, reservation, widening, stateless lifecycle, dispatch hardening, prompt ceiling) and their seams
- phase: 19.7-07
provides: auth_time, pact.HasFrontendTokenScopes, narrowed revocation, towel.ClientIP, cabana and SPA lane B fixes
- phase: 19.7-08
provides: plugin lane C fixes, E-1 (organisation logo) and E-2 (member token scopes)
provides:
- a named test for every fix of plans 06-08 and both escalations, E-2's acceptance list item by item
- check-phase19.7.sh with the workspace check, consumers built against the unreleased user plugin, the unreleased-pin policy and PHASE197_RELEASED strict mode, the demo site's vitest run and the cooler doc check
- the corrected and extended 19.7-SECURITY-REVIEW.md (Corrections, Revert-checks, T-19.7-42..T-19.7-65) and 19.7-SECURITY.md (threats_open 0)
- 19.7-VALIDATION.md rows for the gap closure and the updated deferred-items.md (release order)
affects: [phase 19.7 re-review of the gap closure, msd-verifier, /msd-validate-phase 19.7, /msd-verify-work 19.7, framework v0.5.0 and the plugin tags, sm-servers-app (E-1, E-2)]
actuals:
tokens: 61800
tasks: 4
commits: 9
commits_by_repo:
summercms.go: 9
sm-user-plugin: 3
sm-oauthserver-plugin: 1
sm-journal-plugin: 1
vue-demo-app: 1
sm-demo-app: 2
plan_head_before: 05d7e9351a45eb96cf97eaab8fc3de41d81536ae
plan_head_after: e1f489058c0efa68e60886317d4310201f8df384
tech-stack:
added: []
patterns:
- "Revert-check: a critical fix is reverted in a scratch worktree with the tests kept at HEAD; the pinning test must fail there and pass at HEAD"
- "Gate trusts no local workspace: go list -m answers where each repository resolves the framework and the user plugin"
- "A plugin submodule ahead of its pin is accepted only by commits its canonical checkout holds, and reported as unreleased"
- "Consumers are built against an unreleased plugin through a scratch copy of their go.work, never by editing it"
key-files:
created:
- modules/pier/reaper_test.go
- modules/pier/reserve_test.go
- modules/pier/lock_scope_test.go
- modules/pier/widening_test.go
- modules/pier/stateless_fanout_test.go
- modules/pier/personal_binding_test.go
- modules/pier/dispatch_hardening_test.go
- modules/pier/catalog_path_fields_test.go
- modules/pier/prompt_ceiling_test.go
- modules/pier/token_events_test.go
- modules/bouncer/auth_time_test.go
- modules/pact/frontend_token_scopes_test.go
- modules/towel/client_ip_test.go
- modules/cabana/account_contributions_hardening_test.go
- modules/cabana/record_action_prompt_localized_test.go
- modules/cabana/token_scope_name_test.go
- modules/cabana/account_token_events_test.go
- modules/cabana/token_resolver_remote_test.go
- admin/tests/app/hotkeysEditing.test.ts
- ../sm-demo-app/plugins/golem15/user/member_token_scopes_test.go
- ../sm-demo-app/plugins/golem15/user/backend_link_auth_time_test.go
- ../sm-demo-app/plugins/golem15/user/backend_link_show_test.go
- ../sm-demo-app/plugins/golem15/user/access_revocation_test.go
- ../sm-demo-app/plugins/golem15/user/organisation_logo_test.go
- ../sm-demo-app/plugins/golem15/user/mcp_guard_client_ip_test.go
- ../sm-demo-app/plugins/golem15/user/api_token_events_test.go
- ../sm-oauthserver-plugin/mcp_revocation_test.go
- ../sm-journal-plugin/mcp_prompt_writes_test.go
- ../sm-demo-app/mcp_revocation_e2e_test.go
- ../sm-demo-app/vue-demo-app/app/lib/linkCode.test.ts
modified:
- scripts/check-phase19.7.sh
- modules/pier/export_test.go
- admin/tests/account/AccountContributions.test.ts
- ../sm-demo-app/plugins/golem15/user/controllers/organisations_admin_controller.go
- ../sm-demo-app/plugins/golem15/user/README.md
- ../sm-oauthserver-plugin/README.md
- ../sm-journal-plugin/README.md
- ../sm-demo-app/mcp_e2e_test.go
- .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md
- .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md
- .planning/phases/19.7-generic-mcp-module/19.7-VALIDATION.md
- .planning/phases/19.7-generic-mcp-module/deferred-items.md
key-decisions:
- "P-40 as built: workspace_check resolves with go list -m ({{.Replace.Dir}} else {{.Dir}}) through realpath, in the plugins, consumers (all three applications) and docker stages; demo_pins accepts unreleased commits only when the canonical checkout holds the submodule HEAD; PHASE197_RELEASED=1 refuses any drift before the policy runs"
- "P-41 as built: the tests stay at HEAD in every revert worktree; where a revert conflicts or removes a seam, later commits' code is kept and the seam answers the no-fix value (ReapNow 0); C-CR-02 reverts only the confirm handler of 32f9259"
- "Organisation attachments are deleted by a new FormAfterDelete hook (not in FormBeforeDelete), so the plugin's exported method declarations stay unchanged for the additive check"
- "TestSessionReaper requires the reaper for the copy of the instance that did not serve the last request; the other copy may also be closed by a late persist that finds the record gone"
patterns-established:
- "A Postgres-backed test returns before creating subtests under -short (no t.Skip), so the gate's skip refusal stays meaningful"
requirements-completed: [SC-1, SC-2, SC-3, SC-4, D-05, D-08, D-17, D-19, D-20, D-21, D-25, D-26, D-27, D-31, E-1, E-2]
coverage:
- id: D1
description: "Named framework tests for every pier fix of plan 06 (reaper, reservation, lock scope, widening, stateless fan-out, personal binding, dispatch hardening, path fields, prompt ceiling)"
requirement: "D-25"
verification:
- kind: unit
ref: "modules/pier/*_test.go#TestSessionReaper TestSessionReserve TestLockScope TestScopeWidening TestStatelessFanOut TestPersonalTokenBinding TestDispatchHardening TestCatalogPathFields TestPromptWriteCeiling"
status: pass
- kind: other
ref: "bash scripts/check-phase19.7.sh --go"
status: pass
human_judgment: false
- id: D2
description: "Named tests for every framework and admin SPA fix of plan 07 (auth_time, token-scoped revocation, client address, scope name rule, account action URL and section isolation, localized prompt refusal, hotkeys while editing)"
requirement: "D-05"
verification:
- kind: unit
ref: "modules/bouncer/auth_time_test.go#TestAuthTime; modules/pier/token_events_test.go#TestTokenScopedRevoke TestResolveClientAddress; modules/cabana (TestAccountContributionURL, TestAccountContributionsIsolation, TestRecordActionPromptLocalized, TestTokenScopeNameParity, TestAccountTokenRevokeEvent, TestResolveBackendTokenRemote); modules/pact#TestFrontendTokenScopes; modules/towel#TestClientIP"
status: pass
- kind: automated_ui
ref: "admin/tests/app/hotkeysEditing.test.ts; admin/tests/account/AccountContributions.test.ts"
status: pass
human_judgment: false
- id: D3
description: "E-2 acceptance list as tests, item by item, plus the boot refusals"
requirement: "E-2"
verification:
- kind: integration
ref: "sm-user-plugin/member_token_scopes_test.go#TestMemberTokenScopes TestMemberTokenScopesBoot"
status: pass
human_judgment: false
- id: D4
description: "Plugin, site and demo tests for plan 08 (link proof, link code transport, access revocation, client IP, token revoke event, organisation logo, verifier and grant revocation, Journal prompt ceiling, demo revocation matrix, link code reader)"
requirement: "SC-4"
verification:
- kind: integration
ref: "sm-user-plugin (TestBackendLinkAuthTime, TestBackendLinkShowPost, TestAccessRevocation, TestUserMCPGuardClientIP, TestAPITokenRevokeEvent, TestOrganisationLogo); sm-oauthserver-plugin (TestMCPVerifierRevocation, TestMCPGrantRevocation); sm-journal-plugin#TestJournalPromptWrites"
status: pass
- kind: e2e
ref: "sm-demo-app/mcp_revocation_e2e_test.go#TestMCPRevocationEndToEnd"
status: pass
- kind: unit
ref: "vue-demo-app/app/lib/linkCode.test.ts"
status: pass
human_judgment: false
- id: D5
description: "Extended fail-closed gate (workspace check, consumers on the unreleased user plugin, unreleased-pin policy, PHASE197_RELEASED, vue vitest, cooler docs) green with every stage PASS"
requirement: "D-31"
verification:
- kind: other
ref: "bash scripts/check-phase19.7.sh --all (Phase 19.7 gate passed); PHASE197_RELEASED=1 bash scripts/check-phase19.7.sh --consumers exits 1"
status: pass
human_judgment: false
- id: D6
description: "Corrected and extended security review and contract (Corrections, revert-checks for the three critical fixes, T-19.7-42..65), validation rows and deferred items"
verification:
- kind: other
ref: "Task 4 verify chain (threats_open 0, every T-19.7-42..65 in both documents, Revert-check section, B-IN-05, TestMemberTokenScopes) and the revert-check runs"
status: pass
human_judgment: true
rationale: "The corrections and threat verdicts are a reviewer's judgment; the re-review of the gap closure and msd-verifier must confirm them"
duration: 88min
completed: 2026-10-23
---
# Phase 19.7 Plan 09: Gap-closure tests, the extended gate and the corrected security review Summary
Every fix of the gap-closure plans 06-08 and both sm-servers-app escalations is now pinned by a named test that the phase gate requires: 21 framework and SPA tests, 8 user-plugin tests (E-2's acceptance list item by item), 2 OAuth server tests, a Journal test, a demo end-to-end revocation matrix and a vue-demo-app unit test. The gate no longer trusts local workspace files: it checks where each repository resolves the framework and the user plugin, builds fonoteka.go and sm-bm-app against the unreleased user plugin through scratch go.work copies, reports the demo's plugin submodules as "ahead of its pin by unreleased commits", and refuses any drift under `PHASE197_RELEASED=1`. The security review now corrects the eight verdicts the code review disproved (T-19.7-22 first) and records T-19.7-42..T-19.7-65. The revert-checks show that each of the three critical fixes' tests fails with the fix reverted.
## Performance
- **Duration:** about 88 min
- **Started:** 2026-10-23T15:32:53Z
- **Completed:** 2026-10-23T17:01:31Z
- **Tasks:** 4 of 4
- **Files:** 43 across six repositories (30 created)
## Commits
Nothing was tagged, pushed or deployed.
**summercms.go** (master, plan base 05d7e93; 9 commits, measured)
| Commit | Task | What |
|--------|------|------|
| 0aa48ae | 1 (tracer) | gate: workspace check, scratch go.work consumers, unreleased-pin policy, `PHASE197_RELEASED`, self-test cases; `TestSessionReaper`, `TestAuthTime` |
| fda4646 | 1 (follow-up) | `TestSessionReaper` tolerates a transient store read on a loaded host |
| ccf77b9 | 2 | the plan 06/07 framework, cabana and SPA tests; the `WrapStore` seam; gate arrays |
| c2d486b | 2 (follow-up) | `TestSessionReaper` requires the reaper only where nothing else can close the copy |
| 7f43e0e | 3 | gate: `USER_SHORT_TESTS`, plugin and demo names, `VUE_FILES` vitest run, cooler doc check |
| 946fb2d | 4 | docs: 19.7-SECURITY-REVIEW.md corrected and extended |
| 33faa44 | 4 | docs: 19.7-SECURITY.md register, corrected rows, audit trail |
| a636933 | 4 | docs: 19.7-VALIDATION.md rows |
| e1f4890 | 4 | docs: deferred-items.md |
**sm-user-plugin** (`../sm-demo-app/plugins/golem15/user`, master, base f20a8d3)
| Commit | Task | What |
|--------|------|------|
| 25b9f2e | 3 | `fix:` deleting an organisation removes its avatar and logo attachments (production defect found by `TestOrganisationLogo`) |
| e3c164f | 3 | tests: `TestMemberTokenScopes`, `TestMemberTokenScopesBoot`, `TestBackendLinkAuthTime`, `TestBackendLinkShowPost`, `TestAccessRevocation`, `TestUserMCPGuardClientIP`, `TestAPITokenRevokeEvent`, `TestOrganisationLogo`; README Testing |
| 38551a4 | 3 | `fix:` the attachment cleanup runs in a new `FormAfterDelete` (keeps the plugin additive) |
**sm-oauthserver-plugin** (master, base 671513d): 65f71dc `test:` `TestMCPVerifierRevocation`, `TestMCPGrantRevocation` (README Testing).
**sm-journal-plugin** (main, base b129902): 2aeeccc `test:` `TestJournalPromptWrites` (README Testing).
**vue-demo-app** (master, base 0125e29): b973a76 `test:` `app/lib/linkCode.test.ts`.
**sm-demo-app** (master, base 7731641): faeece2 `test:` `TestMCPRevocationEndToEnd`, `bootMCPDemoWith`, unused `connectWatching` removed; f53d30a `chore:` vue-demo-app pointer bump (staged alone).
## Accomplishments
- **Task 1 (tracer).**
- Gate (P-40):
- `workspace_check` runs at the top of the plugins, consumers and docker stages and refuses a mismatch with both paths named.
- `consumer_with_user_plugin` builds fonoteka.go and sm-bm-app against `$USER_PLUGIN`. Both applications' go.work files are untouched (status count 0).
- `demo_pins` gained the unreleased-commit policy and released mode.
- The header documents `PHASE197_RELEASED`.
- 31 self-test cases print one PASS line each.
- `TestSessionReaper` and `TestAuthTime` are required in the go stage, together with the plan 06 and 07 smokes.
- Tracer gate: the run is interactive and `end-of-phase`, and the verify chain is automated only. Re-running self-test, go, plugins and consumers end to end passed before the expansion tasks started. `PHASE197_RELEASED=1 --consumers` exits 1 while the pins drift.
- **Task 2.**
- Every behaviour of the plan's behavior block is a test under its exact name, with the cases as subtests.
- Every test passed at once, so no framework fix was needed. Because none could start red, the RED evidence for each was checked by hand where a fix could be undone cheaply. With `originOf` and the blur-before-click reverted in hotkeys.ts, 3 of the 4 hotkeysEditing cases fail. With the reaper reverted, `TestSessionReaper` fails (see the revert-checks).
- `go test -race ./modules/pier` is clean over 3 to 8 repeated runs.
- **Task 3.**
- The plugin, site and demo tests are written under the plan's names, one commit per repository, with the fix commits first.
- The gate requires all of them, and the demo site's vitest run requires `app/lib/linkCode.test.ts`.
- The docs stage checks the two cooler doc comments.
- **Task 4.**
- The security review gained:
- Method step 6;
- the Corrections table: T-19.7-22, cross-cutting findings 1 and 6, T-19.7-23, -03, -17, -18, -08, -31 and -35;
- cross-cutting findings 1 and 6 rewritten in place;
- the Revert-checks table;
- register rows T-19.7-42..T-19.7-65 plus the gap-closure T-19.7-SC.
- 19.7-SECURITY.md has 66 register rows, `threats_open: 0` and an audit-trail entry.
- Validation and deferred items are updated.
## Revert-checks (P-41)
Each worktree was created under the session scratch directory and removed afterwards with `git worktree remove --force` and `git worktree prune`. Nothing was committed from any of them.
| Repo | Fix reverted | Test | With the fix reverted | At HEAD |
|------|--------------|------|-----------------------|---------|
| summercms.go | 2bf9dca (A-CR-01) | `TestSessionReaper` | FAIL `reaper_test.go:79: reaped = 0, want 1` (all 5 subtests fail) | PASS |
| sm-oauthserver-plugin | 671513d (C-CR-01) | `TestMCPVerifierRevocation` | FAIL `mcp_revocation_test.go:98: created two seconds before the cutoff: verify = <nil>, want pact.ErrMCPTokenInvalid` (all 5 subtests fail) | PASS |
| sm-user-plugin | 32f9259 (C-CR-02, the confirm handler) | `TestBackendLinkAuthTime` | FAIL `backend_link_auth_time_test.go:64: confirm = 200 …, want 409 with "stale_session"` (and `:86`) | PASS |
## Gate output (final `bash scripts/check-phase19.7.sh --all`, after the documentation commits)
```
PASS self-test
PASS go
PASS plugins
PASS consumers
PASS admin
PASS docs
PASS marker
PASS docker
Phase 19.7 gate passed
```
The consumers stage printed:
- `pins: plugins/golem15/journal is ahead of its pin by unreleased commits (release after verification)`, and the same line for `oauthserver` and `user`;
- the vue-demo-app vitest summary: 182 tests passed.
The admin vitest run: 1552 tests passed.
The docker stage ran the framework's cabana suites, then 16 user-plugin, 7 OAuth server and 2 demo top-level tests, all by name.
Also run:
- the framework's `go vet ./...`, `go vet -tags summercms_mcp ./...` and `go test -count=1 ./...` with Docker, plus examples/hello: green;
- `bash scripts/check-phase19.6.1.sh --all`: "Phase 19.6.1 gate passed";
- `GOWORK=off go test -count=1 ./...` in the user plugin with Docker: green.
go.mod, go.sum and the npm locks are unchanged in every repository.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Deleting an organisation left its attachments behind (E-1)**
- **Found during:** Task 3 (`TestOrganisationLogo`, "deleting the organisation removes the logo attachment as it does the avatar")
- **Issue:** The plan assumed the avatar is removed with the organisation. Neither the avatar nor the new logo was removed: the `system_files` rows and their blobs outlived the organisation row.
- **Fix:**
- 25b9f2e deletes the attachments in the delete's transaction (`attach.DeleteForOwner`) and the blobs after the commit, as the user delete does. The README's Organisations line says so.
- The first version changed `FormBeforeDelete`'s receiver, which the gate's additive check reads as a changed exported method declaration. 38551a4 therefore moves the cleanup into a new `FormAfterDelete` and restores `FormBeforeDelete` exactly.
- **Files:** `../sm-demo-app/plugins/golem15/user/controllers/organisations_admin_controller.go`, `README.md`
- **Commits:** 25b9f2e, 38551a4 (sm-user-plugin)
**2. [Rule 1 - Test race] `TestSessionReaper`'s Redis case was flaky under load**
- **Found during:** Task 2's verify (one run in roughly ten of the full suite)
- **Issue:** A request's state is persisted after its answer has been streamed. When the instance that served the last request was still persisting after the clock moved past the expiry, persist found the record gone and closed the copy itself. The reaper then counted 0 for that instance.
- **Fix:**
- fda4646 reaps until the copies are gone and asserts both copies before the expiry.
- c2d486b requires the reaper for the copy of the instance that did not serve the last request and allows the other copy to be closed by the late persist. That behaviour is correct (an ended session is never written back).
- After both commits: 30 consecutive passes, and a clean full gate.
- **Commits:** fda4646, c2d486b
**3. [Rule 1 - Test race] `TestSessionReserve` checked the local copies before the release had run**
- **Found during:** a background stress run of Task 2
- **Fix:** The test waits for each failed initialize's slot release before the next attempt. It was folded into ccf77b9 before that commit.
**4. [Plan detail] Placement and harness choices**
- `TestResolveClientAddress` is in `token_events_test.go`. The plan's acceptance grep counts it among the nine pier files, and the plan lists no file of its own for it.
- The `-short` part of `TestResolveBackendTokenRemote` (a failed resolve without cabana logs `remote`) is also required in `GO_TESTS`.
- `TestDispatchHardening` drives its contradicting and failing dispatches through one frontend guard keyed by the route method, because pier allows one frontend guard per application.
- `TestJournalPromptWrites` serves pier with a stand-in backend guard. The built-in backend guard needs the admin database, and the test runs under `-short`.
- `consumer_with_user_plugin` uses its own go.work rewriter rather than plan 08's fixed template.
- The workspace check also covers fonoteka.go and sm-bm-app in the consumers stage, because the plan says every repository must resolve the framework to this checkout.
- The gate's `--self-test` now prints one PASS line per planted case (31 cases).
**5. [Plan wording] `readLinkCode` with extra fragment parameters**
- The plan says "extra parameters … give ''". The implementation parses the fragment with URLSearchParams, so `#code=<hex>&next=/x` answers the code.
- The test pins extended and foreign values (''), and leaves the lenient case unasserted.
- It is recorded in deferred-items.md for the re-review to decide. No production change was made in this tests-only plan.
**6. [Revert-check method] C-CR-02 reverted at the handler level**
- A whole-commit revert of 32f9259 also removes the `#code=` URL, so the test would fail at the link URL regexp before reaching the auth_time assertions.
- Only `BackendLinkConfirm` was restored to its pre-fix `iat` body, which isolates the property.
- A-CR-01's revert conflicted with later commits. They were resolved keeping the later code, and the `ReapNow` seam answers 0.
**Total deviations:** one production defect fixed in its own commits (Rule 1), two test races in this plan's own tests (Rule 1), and plan-detail choices recorded above. **Impact:** no shipped framework code changed. The user plugin gained a `FormAfterDelete` hook, and that change is additive.
## Issues Encountered
- One `go test -short ./...` run failed in `docs/examples/blog` with "module lookup disabled by GOPROXY=off". It has not recurred in more than twenty runs. It is recorded in deferred-items.md.
- The repositories have `git rerere` enabled, so the revert worktrees recorded conflict preimages. `git rerere clear` ran in each worktree before it was removed.
## TDD Gate Compliance
Tasks 2 and 3 carry `tdd="true"`, but they pin behaviour that plans 06-08 had already built ("most pass at once"). There is therefore no `feat(19.7-09)` GREEN commit and no failing RED commit for those tests.
Red evidence instead:
- the three revert-checks;
- the hotkeys revert run of Task 2;
- `TestOrganisationLogo`, which failed for a real defect and went green with fix commits 25b9f2e and 38551a4.
## Known Stubs
None.
## Threat Flags
None. The plan adds tests, a gate and documents. Its only production change, the organisation attachment cleanup, is part of T-19.7-61.
## User Setup Required
None.
## Next Phase Readiness
- Phase 19.7's gap closure is complete. The phase is ready for the fresh re-review of plans 06-09, then the regression gate, msd-verifier, /msd-validate-phase 19.7 and /msd-verify-work 19.7. UAT includes a real MCP client against the local demo.
- Release order and local-only wiring: see deferred-items.md.
- Do not commit sm-oauthserver-plugin's go.work retarget. sm-demo-app's `main.go` and its plugin submodule pointers stay unstaged.
- The release order is: framework v0.5.0, then the plugin tags, then the application bumps (vue-demo-app pushed first), then `PHASE197_RELEASED=1 --consumers`.
- **g15office follow-up for the orchestrator** (CLAUDE.md rule 4): at the phase-advance step, move tasks hmguritm and tqyvnzxx into milestone dcnv4mha and post the short phase comment, "gap closure 06-09 complete, gate green, awaiting re-review and verification".
## Self-Check: PASSED
- FOUND: all 30 created test files across the six repositories, and the modified gate and documents.
- FOUND: the framework commits 0aa48ae, fda4646, ccf77b9, c2d486b, 7f43e0e, 946fb2d, 33faa44, a636933, e1f4890.
- FOUND: the user plugin commits 25b9f2e, e3c164f, 38551a4; oauthserver 65f71dc; journal 2aeeccc; vue-demo-app b973a76; sm-demo-app faeece2, f53d30a.
- Revert-check fix shas 2bf9dca, 671513d and 32f9259 resolve to commits (`git cat-file -t`).
- No commit deleted a tracked file.
---
*Phase: 19.7-generic-mcp-module*
*Completed: 2026-10-23*