docs(12.2-05): summary of the phase 12.2 test and gate plan (awaiting the v0.1.1 checkpoint)

This commit is contained in:
Jakub Zych
2026-10-02 20:55:29 +02:00
parent 912a46603f
commit 1757545ecb

View File

@@ -0,0 +1,282 @@
---
phase: 12.2-admin-form-fields-date-file-upload-relation-editing-with-def
plan: 05
subsystem: testing
tags: [cabana, lagoon, attach, conga, pact, admin-spa, vitest, security, deferred-binding, gate]
requires:
- phase: 12.2-01
provides: deferred_bindings store, attach.Store and IsAllowedImage, PurgeDeferred and deferred:purge, lagoon.Date/TimeOfDay, the framework schedule entry, the relation child hooks
- phase: 12.2-02
provides: fileupload and datepicker fields, the record file routes, commitDeferred
- phase: 12.2-03
provides: relation child CRUD, pivot forms, the child file routes, deferral on id 0
- phase: 12.2-04
provides: the SPA session key, DatepickerField, FileuploadField, the relation modals
provides:
- test-only fixture plugin acme.deferred over modules/cabana/testdata/deferred (two controllers, recording hooks, two admins)
- D-15 security suite (TestRelationChildScope*) and D-10 protected file suite (TestProtectedFile*), shown to fail with the parent predicate removed
- full Go tests for every behaviour of the phase (lagoon, attach, conga, pact, cabana)
- SPA unit tests and the four UI-SPEC backstops
- scripts/check-phase12.2.sh with nine stages and a detector self-test
- 12.2-SECURITY-REVIEW.md, the filled 12.2-VALIDATION.md (nyquist_compliant), deferred-items.md
affects: [v0.1.1 release (user tags at the checkpoint), /gsd-verify-work 12.2]
actuals:
tokens: 81700
tasks: 3
commits: 6
plan_head_before: 162a8ec5a1e8f79b0765339690b3e8996ca15d55
plan_head_after: 912a466
tech-stack:
added: []
patterns:
- "A test fixture plugin over os.DirFS(testdata/...) with dfOverlay for per-test YAML edits and a recorder for Form and Relation hooks"
- "Security tests snapshot every fixture table, the fixture's files and bindings, and the blob count before a refused request and compare after"
- "Gate stages run in their own process from --all so errexit holds inside each stage"
key-files:
created:
- modules/cabana/phase122_fixture_test.go
- modules/cabana/testdata/deferred/ (controllers gadgets and locked, models gadget, part, member)
- modules/cabana/relation_child_scope_test.go
- modules/cabana/protected_file_test.go
- modules/cabana/relation_child_test.go
- modules/cabana/fileupload_test.go
- modules/cabana/deferred_commit_test.go
- modules/cabana/datepicker_test.go
- modules/lagoon/purge_test.go
- modules/lagoon/attach/guard_test.go
- admin/tests/app/sessionKey.test.ts
- admin/tests/app/dateFormat.test.ts
- admin/tests/form/DatepickerField.test.ts
- admin/tests/form/FileuploadField.test.ts
- admin/tests/relation/RelationChildModal.test.ts
- admin/tests/relation/RelationPivotModal.test.ts
- admin/tests/fixtures/deferred.files.json
- admin/tests/fixtures/deferred.relation-schema.json
- scripts/check-phase12.2.sh
- .planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/12.2-SECURITY-REVIEW.md
- .planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/deferred-items.md
modified:
- modules/lagoon/date_test.go
- modules/lagoon/fill_test.go
- modules/lagoon/validate_test.go
- modules/lagoon/deferred_test.go
- modules/lagoon/attach/store_test.go
- modules/conga/schedule_test.go
- modules/pact/capabilities_test.go
- admin/tests/relation/RelationManager.test.ts
- admin/tests/list/CellValue.test.ts
- admin/tests/fixtures/typed.ts
- admin/src/components/form/fields/DatepickerField.vue (fix)
- modules/boardwalk/dist (rebuilt for the fix)
- .planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/12.2-VALIDATION.md
key-decisions:
- "12.2-05: the concurrent first-bind duplicate (no unique index on deferred_bindings) is documented by TestDeferredConcurrentFirstBind, not fixed; a partial unique index needs a user decision and a migration"
- "12.2-05: the header-only image guard accepting a valid GIF header followed by HTML is an accepted residual (served as image/gif with nosniff and sandbox CSP on the protected route); the public static handler's missing nosniff is a deferred hardening item"
- "12.2-05: reorder through another gadget answers the same 422 set mismatch as an id that exists nowhere (no existence oracle), not 404"
patterns-established:
- "Each Phase 12.2 security test names its threat in the security review, and the gate's --security stage refuses a missing, renamed or skipped one"
requirements-completed: [SC-1, SC-2, SC-3, SC-4, SC-5]
coverage:
- id: D1
description: "A child, pivot row or child file of another parent, or of a parent hidden by FormExtendQuery, answers 404 not_found on all 12 child routes and changes nothing; the suite fails when the loadChild parent predicate is removed"
requirement: SC-3
verification:
- kind: integration
ref: "modules/cabana/relation_child_scope_test.go#TestRelationChildScope"
status: pass
human_judgment: false
- id: D2
description: "Record id 0 without a key 404, a malformed key 422 session_key; another admin with the key sees nothing and commits nothing; undeclared toolbar buttons 403 before SQL; the pivot PUT whitelist"
requirement: SC-3
verification:
- kind: integration
ref: "modules/cabana/relation_child_scope_test.go#TestRelationChildScopeSessionKey|TestRelationChildScopeToolbar|TestRelationChildScopePivotWhitelist"
status: pass
human_judgment: false
- id: D3
description: "Protected download and thumb: foreign, pending-foreign and public rows 404; only jpeg, png, gif and webp inline; nosniff, private no-store and sandbox CSP; no url or thumb_url on protected lists"
requirement: SC-2
verification:
- kind: integration
ref: "modules/cabana/protected_file_test.go#TestProtectedFileScope|TestProtectedFileHeaders|TestProtectedFileListHasNoURLs"
status: pass
human_judgment: false
- id: D4
description: "Every Go behaviour of SC-1 to SC-4 (dates, fill, required, bindings, purge, schedule, store and guard, file routes, commit, datepicker, relation child CRUD) has a test"
requirement: SC-4
verification:
- kind: integration
ref: "go test ./modules/lagoon ./modules/lagoon/attach ./modules/conga ./modules/pact ./modules/cabana -count=1"
status: pass
human_judgment: false
- id: D5
description: "SPA unit tests and the four UI-SPEC backstops (Escape focus and disabled days, fixed-zone datetime round trip, protected thumbnails, keyboard reorder)"
requirement: SC-1
verification:
- kind: unit
ref: "admin/tests/form/DatepickerField.test.ts|admin/tests/form/FileuploadField.test.ts|admin/tests/relation/*|admin/tests/app/*"
status: pass
human_judgment: false
- id: D6
description: "One gate proves the phase: go, security, spa, openapi, dist, docs, hygiene and app stages, plus a detector self-test"
requirement: SC-5
verification:
- kind: other
ref: "bash scripts/check-phase12.2.sh --all"
status: pass
human_judgment: false
- id: D7
description: "The user reviews the gate output and the security review and tags v0.1.1"
verification:
- kind: manual
ref: "12.2-05 Task 4 checkpoint"
status: pending
human_judgment: true
duration: 54min
completed: 2026-10-02
status: awaiting-checkpoint
---
# Phase 12.2 Plan 05: Unit, integration and security tests, phase gate and release checklist Summary
**This plan adds an acme.deferred fixture plugin that proves through the real router that no child, pivot row or file of another parent or admin can be reached. That proof fails when the parent scoping is removed. Go and SPA tests now cover every Phase 12.2 behaviour, and a nine-stage gate passes end to end. The user then tags v0.1.1.**
## Performance
- **Duration:** about 54 min
- **Started:** 2026-10-02T18:01:19Z
- **Paused at the checkpoint:** 2026-10-02T18:55:00Z
- **Tasks:** 3 of 4 done. Task 4 is the user's v0.1.1 tag, a blocking-human checkpoint.
- **Files:** 51 created or modified in summercms.go. fonoteka.go was not changed.
## Accomplishments
- **Fixture plugin `acme.deferred`** (test-only, `os.DirFS(testdata/deferred)`). It has two controllers:
- `gadgets`: datepicker date, datetime and time fields; a public attachMany `photos` and a protected attachOne `manual`; a belongsToMany `members` with manage and pivot forms (RelationBeforeLink stamps `role`); and a deferrable hasMany `parts` on a soft-deletable model, whose manage form has an image field, an attachOne field and a datepicker.
- `locked`: a reduced toolbar and a required fileupload.
FormExtendQuery hides rows whose `hidden` column is true. Every Form and Relation hook records its calls and can be made to fail. Admins A and B, a memblob bucket and `dfOverlay` give per-test YAML edits.
- **D-15 and D-10 security suites.** A child, pivot row or file of another parent is 404 `not_found` on all 12 child routes and changes nothing (row, file, binding and blob snapshots are compared). The same holds through a FormExtendQuery-hidden parent and through another admin's session key. An owning-parent control runs the same routes with 2xx, which shows each 404 comes from the scope. The protected routes serve only the four image types inline and always send the D-10 headers.
- **Go coverage of every behaviour.**
- Date and TimeOfDay through real DATE and TIME columns. Fill, without changing earlier conversions. `required` on zero dates.
- The deferred_bindings shape, history and rollback. Store isolation and the envelope.
- PurgeDeferred: cut-off, after-commit blobs, rollback, SKIP LOCKED, the nil bucket, `--days`, and the command end to end.
- attach.Store limits and key shape. IsAllowedImage polyglots and the pixel ceiling.
- The framework schedule and forged jobs. The six hook interfaces.
- In cabana: fileupload and datepicker compile and routes; commit order, rollback, applied-only and concurrent saves; relation contracts, the zero-Kind equivalence, forms, CRUD, deferral and the schema.
- **SPA tests.**
- sessionKey and dateFormat.
- DatepickerField: backstops 1 and 2.
- FileuploadField: backstops 3 and 4, plus states, retry, removal, caption and load failure.
- RelationChildModal and RelationPivotModal.
- RelationManager: row-action order, the pivot button, plural delete and create-screen deferral.
- Date and time cells.
- **Gate** `scripts/check-phase12.2.sh`. Its detector refuses a failure, a skip, zero tests, "no tests to run", a build failure or a missing named test, and `--self-test` proves it on planted inputs. A planted missing security test name was refused with exit 1.
## Gate stage summary
`FORCE_COLOR= bash scripts/check-phase12.2.sh --all` on 2026-10-02 (about 3 min 16 s, Docker up):
```
PASS self-test
PASS go
PASS security
PASS spa (Test Files 60 passed, Tests 768 passed)
PASS openapi
PASS dist
PASS docs
PASS hygiene
PASS app (fonoteka.go build, vet, Admin tests, parity schema and migrate; tree clean)
phase12.2 all stages passed
```
## Failing-when-broken check (Task 1)
The parent predicate in `loadChild` (`modules/cabana/relation_child.go`) was dropped by adding a matching `case true:` ahead of the scoped cases. `go test ./modules/cabana -run '^TestRelationChildScope' -count=1` then failed:
```
relation_child_scope_test.go:97: GET records/{child} through G1: status=200 want 404 body={"data":{"due_on":null,"id":2,"label":"p2"},"meta":{"labels":{}}}
relation_child_scope_test.go:222: B show of A's pending part: status=200 want 404 body={"data":{"due_on":null,"id":1,"label":"pending-..."},"meta":{"labels":{}}}
```
The file was restored with `git checkout -- modules/cabana/relation_child.go`, and `git diff --quiet` confirmed it.
## Task Commits
1. **Task 1: D-15 and D-10 suites with the fixture plugin** - `9d2b1c1` (test)
2. **Task 2: every Go behaviour of the phase** - `c8d41a6` (test)
3. **Task 3: SPA tests and the gate** - `5b06b20` (fix, see Deviations), `d5494fa` (test); planning docs `912a466` (docs)
4. **Task 4: v0.1.1 tag**: checkpoint, awaiting the user. Nothing was tagged or pushed.
The measured count of 6 commits since `plan_head_before` includes `9c87a59 docs(13): create phase plan`. Another session committed it on master during this run, and it is not part of this plan.
## Decisions Made
See `key-decisions` in the frontmatter.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] A partly filled time-mode datepicker was never marked invalid**
- **Found during:** Task 3 (DatepickerField tests)
- **Issue:** `refreshPartial` read only `[data-reka-date-field-segment]`. Reka's TimeField marks its segments `data-reka-time-field-segment`, so in time mode a half-typed value never got `aria-invalid`.
- **Fix:** It now reads both segment attributes. The test "marks a partly filled time invalid" failed before the fix and passes after it. `modules/boardwalk/dist` was rebuilt.
- **Files modified:** admin/src/components/form/fields/DatepickerField.vue, modules/boardwalk/dist
- **Commit:** 5b06b20
### Fixture and scope adjustments
- **The fixture tree is larger than listed.** The plan named seven YAML files. The tree also has `controllers/locked/*` (the "second fixture controller with a reduced toolbar"), `models/gadget/locked_fields.yaml` (a required fileupload) and `models/member/fields.yaml` (a manage form for belongsToMany create, update and delete). The locked list declares `create` so the required-fileupload save can run. `photos` gained `maxFilesize: 0.5`, so the 413 and the 422 size paths can be tested.
- **The fixture controller registers one widget action (`lookup`).** Without it, a widget field in a relation form fails on its action before the D-23 refusal.
- **Reorder through another gadget answers 422, not 404.** Reorder takes the whole id set. A foreign id gives the same 422 set mismatch as an id that exists nowhere. The test asserts that the two answers are identical, so there is no existence oracle.
- **TIME columns in test models use `type:time without time zone`.** GORM reads a bare `type:time` as its own time type, and AutoMigrate would create `timestamptz`. Production migrations are hand-written SQL, so they are unaffected. This is logged in deferred-items.md.
---
**Total deviations:** 1 auto-fixed bug and 4 fixture or scope adjustments.
**Impact on plan:** The fix is in production SPA code, in its own `fix(12.2-05)` commit with the test that proves it. Nothing else touches production code.
## Issues Encountered
- **Shared database.** Other cabana tests leave bindings behind, so the fixture's state snapshot counts only the fixture's own morph types.
- **Shared index.** `git diff --cached` was checked before every commit and each commit used explicit pathspecs. No foreign file was staged, and the other session's phase 13 files were not touched.
- **FORCE_COLOR.** It was unset for every Go run.
## Known Stubs
None.
## Threat Flags
None. The plan adds tests, a gate script and docs. The only production change, the DatepickerField fix, adds no network, auth, file or schema surface.
## User Setup Required
None for the tests. Task 4 needs the user (see below).
## Checkpoint: v0.1.1 tag (Task 4, blocking-human)
1. Read the gate stage summary above, or re-run `FORCE_COLOR= bash scripts/check-phase12.2.sh --all`.
2. Read `12.2-SECURITY-REVIEW.md`. Every high threat has a named passing test.
3. Run `/gsd-verify-work 12.2` for the manual UI checks: calendar keyboard and visual fit, drag reorder and upload progress, and the child and pivot modal flow.
4. summercms.go has no tags yet, because v0.1.0 was deferred to the launch step in Phase 11.2. Decide whether to create v0.1.0 first on its intended commit.
5. Tag and push it yourself: `git tag -a v0.1.1 -m "SummerCMS v0.1.1: datepicker, fileupload, relation child CRUD with deferred binding"` on the phase head, then `git push origin master v0.1.1`.
6. Tell the downstream project that its `TODO: requires SummerCMS change` items for the date field, the file upload field and related-record editing can be resolved on v0.1.1.
Reply "tagged" or "defer tag".
## Self-Check: PASSED
- All created files listed above exist on disk.
- Commits 9d2b1c1, c8d41a6, 5b06b20, d5494fa and 912a466 are on master.
- `git tag --list v0.1.1` is empty.