|
|
|
|
@@ -0,0 +1,239 @@
|
|
|
|
|
---
|
|
|
|
|
phase: 14.2-local-fonoteka-frontend-on-local-summercms-backend
|
|
|
|
|
plan: 04
|
|
|
|
|
subsystem: testing
|
|
|
|
|
tags: [unit-tests, gate, dev-seed, surf, uploads, access-log, playwright, contracts, fonoteka]
|
|
|
|
|
|
|
|
|
|
requires:
|
|
|
|
|
- phase: 14.2-01
|
|
|
|
|
provides: fonoteka:dev-seed, commandDatabase, the local marker, storage.uploads.serve, http.access_log
|
|
|
|
|
- phase: 14.2-02
|
|
|
|
|
provides: dev-stack.sh guard, e2e, e2e-verdict, status, up --detach
|
|
|
|
|
- phase: 14.2-03
|
|
|
|
|
provides: the E2E_BACKEND=go switch in runTinker.ts and the Go-gate spec set
|
|
|
|
|
provides:
|
|
|
|
|
- scripts/check-phase14.2.sh (--self-test --go --docs --parity --contracts --guard --e2e --all)
|
|
|
|
|
- console tests TestDevSeedRefusesProduction, TestDevSeedShape, TestDevSeedIdempotent
|
|
|
|
|
- plugin tests TestDevSeedOpensDatabase, TestReindexOpensDatabase, TestCommandsOpenDatabase, TestDevMarkerRoute
|
|
|
|
|
- surf tests TestUploadsMount, TestAccessLog
|
|
|
|
|
- vue-fonoteka-app tests/contracts/e2e-backend-switch.test.mjs
|
|
|
|
|
- surf: the uploads route owns its prefix (a plugin route at or under it fails BuildRouter)
|
|
|
|
|
affects: [/msd-verify-work 14.2, /msd-validate-phase 14.2, phase 20 cutover]
|
|
|
|
|
|
|
|
|
|
actuals:
|
|
|
|
|
tokens: 20256
|
|
|
|
|
tasks: 3
|
|
|
|
|
commits: 3
|
|
|
|
|
plan_head_before: dbdf15da76098beb315a61fa4719e05ab1ca13f2
|
|
|
|
|
plan_head_after: 78d72418cb731263572b75294846841a23a853d3
|
|
|
|
|
fonoteka_head_before: 807dfdbfcc6e323d327c29295d23c76887e4dc89
|
|
|
|
|
fonoteka_head_after: 49af991a8f3a9b5c75af29a0734d2d583b83449a
|
|
|
|
|
fonoteka_commits: 2
|
|
|
|
|
vue_fonoteka_app_head_before: c1f323a0ff3a51e1ca6f5db80581a0db0d644d6a
|
|
|
|
|
vue_fonoteka_app_head_after: 6d60e42022e4252cbe0d73b7fb5196eb000d093b
|
|
|
|
|
vue_fonoteka_app_commits: 1
|
|
|
|
|
|
|
|
|
|
tech-stack:
|
|
|
|
|
added: []
|
|
|
|
|
patterns:
|
|
|
|
|
- "Fresh-process command tests: party.Activate on an app with only database.dsn, run the plugin's own Command, assert the published handle carries the OnDatabase GORM callback"
|
|
|
|
|
- "Gate guard cases run dev-stack.sh in env -i with planted guard-*.conf files, require a case-specific refusal reason, and keep a passing SummerCMS control case"
|
|
|
|
|
- "Report-only suites are printed with their counts next to a blocking, phase-scoped run"
|
|
|
|
|
|
|
|
|
|
key-files:
|
|
|
|
|
created:
|
|
|
|
|
- scripts/check-phase14.2.sh
|
|
|
|
|
- modules/surf/uploads_test.go
|
|
|
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/console/dev_seed_test.go
|
|
|
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/dev_stack_test.go
|
|
|
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/dev_marker_test.go
|
|
|
|
|
- /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/tests/contracts/e2e-backend-switch.test.mjs
|
|
|
|
|
modified:
|
|
|
|
|
- modules/surf/uploads.go
|
|
|
|
|
- modules/surf/accesslog_test.go
|
|
|
|
|
- modules/surf/README.md
|
|
|
|
|
- docs/services/storage.md
|
|
|
|
|
- .planning/phases/14.2-local-fonoteka-frontend-on-local-summercms-backend/deferred-items.md
|
|
|
|
|
|
|
|
|
|
key-decisions:
|
|
|
|
|
- "The uploads route owns storage.uploads.public_path_prefix: BuildRouter fails when a plugin route sits at or under it (the plan's truth; before, only an identical pattern failed, and only at Assemble)"
|
|
|
|
|
- "--contracts blocks on the phase's own contract test and reports the whole pnpm test:contracts suite, because missing-catalog.test.mjs fails on vue-fonoteka-app HEAD from before this phase"
|
|
|
|
|
- "The gate leaves TMPDIR to the caller: a TMPDIR inside fonoteka.go made internal/build scaffold tests edit fonoteka.go/go.work and made go test ./... there pick up build directories"
|
|
|
|
|
- "--go also runs go test ./... in the fonoteka plugin module, because ./... at the fonoteka.go root covers only the root module"
|
|
|
|
|
- "The go.log scan reads the whole log, not only lines from this run, so a 5xx left from an earlier session also fails the gate until the log is read and rotated"
|
|
|
|
|
|
|
|
|
|
requirements-completed: [QA-05]
|
|
|
|
|
|
|
|
|
|
coverage:
|
|
|
|
|
- id: D1
|
|
|
|
|
description: "Seed refusal: unset SUMMER_ENV, production, development, testing and staging refused before anything is opened; local and dev pass the gate; DevEnvironment agrees"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: unit
|
|
|
|
|
ref: "../fonoteka.go/plugins/golem15/fonoteka/console/dev_seed_test.go#TestDevSeedRefusesProduction"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D2
|
|
|
|
|
description: "Seed shape, demo@example.com/changeme fallback, DEV_SEED_API_TOKEN prefix refusal and hash storage; idempotent rerun and password-only update"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: integration
|
|
|
|
|
ref: "../fonoteka.go/plugins/golem15/fonoteka/console/dev_seed_test.go#TestDevSeedShape,TestDevSeedIdempotent"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D3
|
|
|
|
|
description: "dev-seed, fonoteka:reindex, fonoteka:oauth-client and fonoteka:prune-notifications open and publish their own database with the OnDatabase hooks"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: integration
|
|
|
|
|
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_stack_test.go#TestDevSeedOpensDatabase,TestReindexOpensDatabase,TestCommandsOpenDatabase"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D4
|
|
|
|
|
description: "Marker route only in local/dev, no auth middleware, exact body and no-store; absent and 404 in production, development and testing; not in parity/manifest.yaml"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: unit
|
|
|
|
|
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_marker_test.go#TestDevMarkerRoute"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D5
|
|
|
|
|
description: "Uploads mount (off, on, nosniff, HEAD, private/missing/traversal 404, unpublished bucket or database, custom prefix, prefix ownership) and access log (pattern/status, unmatched path, locale prefix, integer duration, no secrets, flush)"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: integration
|
|
|
|
|
ref: "modules/surf/uploads_test.go#TestUploadsMount"
|
|
|
|
|
status: pass
|
|
|
|
|
- kind: unit
|
|
|
|
|
ref: "modules/surf/accesslog_test.go#TestAccessLog"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D6
|
|
|
|
|
description: "E2E_BACKEND=go switch contract: argv, cwd, fail-closed exits, runTinker throws, PHP path unchanged, auth.setup.ts seeds only in Go mode"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: unit
|
|
|
|
|
ref: "/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/tests/contracts/e2e-backend-switch.test.mjs"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
- id: D7
|
|
|
|
|
description: "check-phase14.2.sh --all: self-test, go, docs, parity 175/175, contracts, guard refusals, Go-gate Playwright 17/0/0/0 and go.log scan; last line Phase 14.2 gate passed"
|
|
|
|
|
requirement: QA-05
|
|
|
|
|
verification:
|
|
|
|
|
- kind: other
|
|
|
|
|
ref: "bash scripts/check-phase14.2.sh --all"
|
|
|
|
|
status: pass
|
|
|
|
|
human_judgment: false
|
|
|
|
|
|
|
|
|
|
duration: 60min
|
|
|
|
|
completed: 2026-10-11
|
|
|
|
|
status: complete
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Phase 14.2 Plan 04: Unit tests and the fail-closed phase gate Summary
|
|
|
|
|
|
|
|
|
|
**Every Go piece the phase added now fails a named test when it regresses: the seed's environment gate, shape and idempotency, the commands' own database, the local marker, the uploads mount and the access log. A Nuxt contract pins the `E2E_BACKEND=go` switch. `scripts/check-phase14.2.sh --all` runs all of it plus parity, the guard refusals and the Go-gate Playwright subset, and ends with `Phase 14.2 gate passed`.**
|
|
|
|
|
|
|
|
|
|
## Performance
|
|
|
|
|
|
|
|
|
|
- **Duration:** about 60 min (2026-10-11T21:06Z to 22:06Z)
|
|
|
|
|
- **Tasks:** 3 (tracer, auto TDD, auto)
|
|
|
|
|
- **Repos:** summercms.go (3 code commits), fonoteka.go (2), vue-fonoteka-app (1)
|
|
|
|
|
|
|
|
|
|
## Accomplishments
|
|
|
|
|
|
|
|
|
|
- **Seed tests (console package, shared testcontainer):**
|
|
|
|
|
- `TestDevSeedRefusesProduction` needs no database: the refused environments return `refused in environment`, print nothing and publish no handle. `local` and `dev` get past the gate and fail on a closed port.
|
|
|
|
|
- `TestDevSeedShape` checks the user flags and bcrypt hash, the context collection, three albums with one public PNG cover each whose blob is in the mem bucket, the wishlist, the rating, the token hash against the printed `inv_` secret and `search_use_typesense`. It also covers the `demo@example.com` / `changeme` fallback and `DEV_SEED_API_TOKEN` values without the prefix (`tok_…`, bare `inv_`, `INV_…`): each is refused with no row in any of nine tables. A value with the prefix is stored by its hash and never printed.
|
|
|
|
|
- `TestDevSeedIdempotent`: the rerun prints the same entities, all `unchanged`, with no secret and no new row. A new `E2E_PASSWORD` prints only `user: updated`.
|
|
|
|
|
- **Own-database tests (plugin root):** the four commands are taken from the plugin's own `Commands()` and run in fresh processes. Each publishes a `*gorm.DB` carrying the `fonoteka:album_artist_resolver` callback, which proves the `OnDatabase` hooks ran. None prints `database is not configured`. `TestDevMarkerRoute` covers local, dev, unset, production, development and testing, and reads `parity/manifest.yaml`.
|
|
|
|
|
- **Framework tests (surf):**
|
|
|
|
|
- `TestUploadsMount` uses its own Postgres container with `system_files` and a mem bucket.
|
|
|
|
|
- `TestAccessLog` runs next to `TestAccessLogSmoke` with the `acme.blog` fixture and the in-package path locale resolver.
|
|
|
|
|
- **Gate script:** every stage named in the plan. Self-test plants detector events, Playwright reports, Go logs and TAP summaries. The guard stage plants six refusal cases plus a passing control case.
|
|
|
|
|
|
|
|
|
|
## Gate result
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
PASS self-test
|
|
|
|
|
PASS go
|
|
|
|
|
PASS docs
|
|
|
|
|
PASS parity (recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0)
|
|
|
|
|
PASS contracts (blocking: e2e-backend-switch 9/9; report only: pnpm test:contracts 287/288)
|
|
|
|
|
PASS guard (php-port, internal-unset, mismatch, production, fake-php, other-marker refused; control passed)
|
|
|
|
|
PASS e2e (Go gate expected=17 unexpected=0 skipped=0 flaky=0; D-15 report only expected=19 unexpected=22)
|
|
|
|
|
Phase 14.2 gate passed
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Task Commits
|
|
|
|
|
|
|
|
|
|
1. **Task 1 (tracer): the gate runs the seed refusal through the detector**
|
|
|
|
|
- fonoteka.go `916d9cf` test(14.2-04): dev-seed refuses every environment but local and dev before opening anything
|
|
|
|
|
- summercms.go `46eee17` test(14.2-04): check-phase14.2.sh runs the seed refusal through the skip-intolerant detector
|
|
|
|
|
- Tracer gate: the verify passed, a re-run of `--all` passed (end-of-phase mode, automated verify), then I expanded.
|
|
|
|
|
2. **Task 2: unit tests for the seed, the commands, the marker, uploads and the access log**
|
|
|
|
|
- fonoteka.go `49af991` test(14.2-04): dev-seed shape and idempotency, own-database commands, local marker
|
|
|
|
|
- summercms.go `2c5c058` test(14.2-04): uploads mount and access log unit tests; the uploads route owns its prefix
|
|
|
|
|
3. **Task 3: Nuxt contract and the remaining gate stages**
|
|
|
|
|
- vue-fonoteka-app `6d60e42` test(14.2-04): contract for the E2E_BACKEND=go seed switch
|
|
|
|
|
- summercms.go `78d7241` test(14.2-04): check-phase14.2.sh docs, parity, contracts, guard and e2e stages
|
|
|
|
|
|
|
|
|
|
## Decisions Made
|
|
|
|
|
|
|
|
|
|
See `key-decisions` in the frontmatter.
|
|
|
|
|
|
|
|
|
|
## Deviations from Plan
|
|
|
|
|
|
|
|
|
|
### Auto-fixed Issues
|
|
|
|
|
|
|
|
|
|
1. **[Rule 1 - Bug] A plugin route under the uploads prefix did not fail BuildRouter.**
|
|
|
|
|
- **Found during:** Task 2, by `TestUploadsMount` (written first, failing).
|
|
|
|
|
- **Issue:** only an identical `GET {prefix}/{path...}` pattern failed, and only later at `Assemble` (duplicate route). A plugin route like `/storage/uploads/avatars/{id}` was accepted and took over part of the uploads URL space.
|
|
|
|
|
- **Fix:** `mountUploads` refuses any route at or under the prefix, so `BuildRouter` returns the error, as `checkAdminPrefix` does for the admin prefix. A sibling path (`/storage/uploadsx/…`) and the key off are unaffected. The surf README and docs/services/storage.md now say this. The real fonoteka binary still boots with the key on (`dev-stack.sh up` after the change).
|
|
|
|
|
- **Files:** modules/surf/uploads.go, modules/surf/README.md, docs/services/storage.md. **Commit:** 2c5c058.
|
|
|
|
|
2. **[Rule 3 - Blocking] `--contracts` scoped to the phase's own contract test.** The whole `pnpm test:contracts` suite fails 1 of 288 on `missing-catalog.test.mjs`, which predates this phase (deferred-items.md, plan 03). Following the coordinator's instruction, the blocking run is `node --test tests/contracts/e2e-backend-switch.test.mjs` with a strict TAP check: tests > 0, all pass, and no fail, skip, todo or cancel. The whole suite still runs, and its counts and the failing test name are printed, but it does not block. **Commit:** 78d7241.
|
|
|
|
|
3. **[Rule 3 - Blocking] TMPDIR not forced into the fonoteka.go tree.**
|
|
|
|
|
- **Issue:** with `TMPDIR=fonoteka.go/tmp/dev-stack/go-tmp` (the coordinator's instruction), the framework's `internal/build` scaffold tests and `TestWatchHelloWorkspaceRebuildLatency` failed. One scaffold test added a `use ./tmp/dev-stack/go-tmp/TestScaffoldCoreArtifacts…` line to `fonoteka.go/go.work`; I reverted it with `git checkout -- go.work` before any commit. A concurrent `go test ./...` in fonoteka.go also listed the build directories as packages.
|
|
|
|
|
- **Fix:** the gate leaves TMPDIR to the caller (`PHASE142_TMPDIR` overrides). The Go tests do not need the on-device TMPDIR: their buckets live in `t.TempDir()`. `dev-stack.sh` already sets its own TMPDIR for the Go processes and for Playwright. Logged in deferred-items.md.
|
|
|
|
|
4. **[Rule 2 - Missing coverage] `--go` tests the fonoteka plugin module too.** `go -C fonoteka.go test ./...` covers only the root module (`app`, `parity`, `scripts`). The plugin is a separate workspace module, so the stage also runs `go vet` and `go test ./...` in `plugins/golem15/fonoteka`.
|
|
|
|
|
5. **[Rule 2 - Stronger evidence] Guard cases check the refusal reason, and a control case must pass.** Each planted case must exit non-zero and print a `refuse:` line naming its own reason (port 8422, unset, differ, not loopback, X-Powered-By: PHP, the summercms marker). A fake SummerCMS marker must pass the same guard. All cases run in `env -i`, so the developer's shell cannot change them.
|
|
|
|
|
6. **[Plan text vs. behaviour] Raw `..` traversal is a ServeMux redirect, not a 404.** ServeMux answers a raw dot-dot path with a 307 to the cleaned path before any handler runs. The test therefore follows the redirect, requires a `Location` without `..`, and requires that the cleaned path is refused (404) with no blob bytes. Encoded traversal (`%2f`, `%2e%2e`, `%5c`) reaches the handler and gets a 404 with nosniff.
|
|
|
|
|
|
|
|
|
|
### Acceptance criterion not met as written
|
|
|
|
|
|
|
|
|
|
- **`grep -c 't.Skip' modules/surf/uploads_test.go` prints 1, not 0.**
|
|
|
|
|
- The Postgres helper `uploadsGorm` calls `t.Skip` only under `-short`. The project's CLAUDE.md requires Docker-backed tests to skip under `-short`, and CLAUDE.md overrides the plan.
|
|
|
|
|
- An unavailable Docker daemon is still a `t.Fatalf`, never a skip.
|
|
|
|
|
- The gate never runs `-short`, and its detector refuses any skipped test or subtest (planted case `subtest-skip`).
|
|
|
|
|
- `dev_stack_test.go` and `dev_marker_test.go` contain no `t.Skip`. They reach the existing `bootDB` helper, which skips under `-short` only.
|
|
|
|
|
|
|
|
|
|
**Total deviations:** 5 auto-fixed (1 bug, 2 blocking, 2 coverage), 1 plan-text clarification, 1 acceptance grep deviation driven by CLAUDE.md. **Impact:** one framework behaviour change (uploads prefix ownership), documented. No other production code changed.
|
|
|
|
|
|
|
|
|
|
## Issues Encountered
|
|
|
|
|
|
|
|
|
|
- **Vite cold start after a fresh `up`.** The first `--all` run came right after I restarted the stack, so the binary would include this plan's code. Its `--e2e` stage failed: `auth.setup.ts` got Nuxt's 500 page "Failed to fetch dynamically imported module …/layouts/public.vue", so 16 tests were skipped and `e2e-verdict` refused the run, as designed. go.log showed no 5xx. On the warm server the same specs passed 17/0/0/0, and the final `--all` run passed. The gate does not retry. Logged in deferred-items.md with options.
|
|
|
|
|
- **D-15 report-only specs:** expected=19 unexpected=22 on Go. They are reported, not gated, per D-15. go.log had no 5xx and no `album search engine failed` after them.
|
|
|
|
|
- **Pre-existing:** gofmt drift in `fonoteka.go/plugins/golem15/fonoteka/household_smoke_test.go`, out of scope.
|
|
|
|
|
|
|
|
|
|
## Verification
|
|
|
|
|
|
|
|
|
|
- `bash scripts/check-phase14.2.sh --all`: exit 0. Last line: `Phase 14.2 gate passed`.
|
|
|
|
|
- `--go` covers `go vet ./...` and `go test ./... -count=1` in summercms.go (Docker), in the fonoteka.go root module and in the fonoteka plugin module. The named tests passed through the detector: console (4), plugin (5) and surf (3).
|
|
|
|
|
- `shellcheck scripts/check-phase14.2.sh` is clean, and `bash -n` passes on it and on dev-stack.sh.
|
|
|
|
|
- Plan acceptance greps pass, except the `t.Skip` grep noted above.
|
|
|
|
|
- `git status` after the run:
|
|
|
|
|
- fonoteka.go: only the other sessions' submodule pointers (feedback, golem, user). `go.work` is unchanged.
|
|
|
|
|
- vue-fonoteka-app: clean.
|
|
|
|
|
- summercms.go: only `.planning/state.json` and `.msd/` (other sessions), plus this plan's planning docs.
|
|
|
|
|
|
|
|
|
|
## Left running
|
|
|
|
|
|
|
|
|
|
The Go and Nuxt dev stack is up (`dev-stack.sh status`: Go on 127.0.0.1:8423, Nuxt on :3119). I restarted it once with `down` + `up --detach` so the binary includes this plan's framework change, and left it running.
|
|
|
|
|
|
|
|
|
|
## Next Phase Readiness
|
|
|
|
|
|
|
|
|
|
`/msd-verify-work 14.2` can use `bash scripts/check-phase14.2.sh --all`. The services must be up and the Nuxt server warm (load one page, or run `--e2e` twice after a fresh `up`). `14.2-VALIDATION.md` statuses are left for `/msd-validate-phase 14.2`, as the plan says.
|
|
|
|
|
|
|
|
|
|
## Self-Check: PASSED
|
|
|
|
|
|
|
|
|
|
All created files exist. Commits: summercms.go 46eee17, 2c5c058, 78d7241. fonoteka.go 916d9cf, 49af991. vue-fonoteka-app 6d60e42.
|