--- phase: 02-api-parity-harness-bootstrap plan: 03 subsystem: testing tags: [parity, fixtures, php, nuxt, mcp, oauth, pkce] requires: - phase: 02-api-parity-harness-bootstrap provides: tide record/replay/proxy, capture rules, named vars, manifest --next-batch, summer parity:* provides: - Isolated PHP on 127.0.0.1:8423 with a parity-named SQLite DB - Recorded API seed plus 154 live PHP route fixtures - Real Nuxt and MCP session fixtures including OAuth/PKCE - App-owned check_corpus.go coverage and secret scan affects: [02-04, 02-05, 03] tech-stack: added: [] patterns: - Record against isolated PHP via process env; refuse any DB basename that does not contain parity - Route fixtures come only from summer parity:record; client fixtures only from parity:proxy + unchanged Nuxt/MCP - Secrets live in mode-0600 --vars (and pkce.vars) outside the corpus; YAML holds {{placeholders}} key-files: created: - ../fonoteka.go/parity/manifest.yaml - ../fonoteka.go/parity/routes.snapshot - ../fonoteka.go/parity/capture-rules.yaml - ../fonoteka.go/parity/capture_clients.mjs - ../fonoteka.go/parity/php_parity.sh - ../fonoteka.go/parity/fixtures/seed/bootstrap.yaml - ../fonoteka.go/parity/fixtures/routes/ - ../fonoteka.go/parity/fixtures/nuxt/nuxt-browse.yaml - ../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml - ../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml modified: - ../fonoteka.go/parity/check_corpus.go - ../fonoteka.go/parity/README.md - tide/variables.go - tide/normalize.go - tide/proxy.go - tide/replay.go - tide/manifest.go - cmd/summer/parity.go key-decisions: - "Isolated SQLite at /tmp/summercms-parity/fonoteka-parity.sqlite; artisan serve 127.0.0.1:8423; proxy 8422; never the developer DB" - "154 Route::get/post/put/patch/delete IDs from routes.php are the denominator; Task 2 records in resumable batches of ≤12 routes / ≤15 files" - "Destructive DELETEs use missing ids (999999) so seed rows survive ordered --update recapture" - "Unchanged clients omit X-Parity-Session; capture_clients.mjs runs exclusive proxy --session per journey" - "PKCE verifier is fixture-constant and stays in /tmp/summercms-parity/pkce.vars (0600); merge it after seed replay before mcp-oauth" patterns-established: - "Pattern: php_parity.sh reset|serve|artisan refuses a non-parity DB name" - "Pattern: seed replay, then either 154 routes or client flows — not both on the same mutated DB" - "Pattern: capture-rules.yaml is policy only; live values never land in git" requirements-completed: [QA-01, QA-02, QA-03] duration: 61min completed: 2026-09-17 --- # Phase 2 Plan 3: Record all 154 PHP routes and real Nuxt/MCP flows Summary **Isolated PHP on :8423 yielded a 154-route live fixture corpus plus Playwright Nuxt and MCP SDK OAuth/PKCE sessions, all placeholder-scrubbed and self-replay green** ## Performance - **Duration:** 61 min - **Started:** 2026-09-17T10:46:38Z - **Completed:** 2026-09-17T11:47:00Z - **Tasks:** 3 - **Files modified:** 180+ (154 route fixtures plus seed, manifest, clients, tide fixes) ## Accomplishments - Isolated parity SQLite + `winter:up` + `fonoteka:oauth-client`; PHP `artisan serve` on `127.0.0.1:8423`; no PHP/Nuxt/MCP source edits. - Seed bootstrap recorded from live API calls; vars file mode 0600 under `/tmp/summercms-parity/`. - All 154 `routes.php` IDs have a committed one-step route fixture from `summer parity:record` (batches of ≤12 routes, ≤15 files per commit). - Real client corpus: `nuxt-browse` (56 steps, login/dashboard/albums/genres write), `mcp-tools` (3 steps, list+create genre with `token:mcp-read`), `mcp-oauth` (20 steps, discovery/register/authorize/consent/PKCE token). - `check_corpus.go --require-recorded --require-clients --check-secrets` prints `recorded 154/154`. - Fresh-DB PHP self-replay: seed then `--manifest --self-check --require-recorded` → `recorded 154/154 passing 154 failing 0 unrecorded 0`. Seed then Nuxt/MCP/OAuth (with `pkce.vars` merged) all matched. ## Task Commits Each task was committed atomically (app repo `fonoteka.go` unless noted): 1. **Task 1: Seed an isolated PHP instance and capture its first real route** - `8b5bd23` (feat) 2. **Task 2: Record a case for each of the 154 PHP routes** - `ef68d80`, `66ed0bb`, `0ee0bc7`, `6bc255c`, `29544c4`, `351fd3f`, `3211634`, `fc343ff`, `029d8a8`, `db02ecb`, `c87e78b`, `8869405`, `ddb9ae9`, `f3a3040` (feat batches) 3. **Task 3: Capture unchanged Nuxt and MCP client journeys** - `a7511b5` (feat) Framework tide/CLI fixes in `summercms.go` (same plan): `4451c2f`, `1ea3c59`, `591a0d4`, `5b947c7`, `39ddf78`, `4c1259a`, `0307510`, `8f94b07`, `22f02ee`, `8881df9`, `2eb9b4b`, `7a6c669`. **Plan metadata:** this SUMMARY commit ## Files Created/Modified - `../fonoteka.go/parity/manifest.yaml` — 154 route IDs, auth groups, fixture names, `status: pending` - `../fonoteka.go/parity/routes.snapshot` — canonical IDs + PHP source digest - `../fonoteka.go/parity/fixtures/seed/bootstrap.yaml` — API-created parity dataset - `../fonoteka.go/parity/fixtures/routes/*.yaml` — one live PHP case per route - `../fonoteka.go/parity/capture-rules.yaml` — kept headers and named captures, no values - `../fonoteka.go/parity/capture_clients.mjs` — Playwright + MCP SDK orchestrator - `../fonoteka.go/parity/fixtures/nuxt/nuxt-browse.yaml` — real Nuxt multi-step flow - `../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml` — personal-token MCP tools - `../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml` — OAuth discovery/PKCE - `../fonoteka.go/parity/check_corpus.go` — 154-ID audit, `--require-clients`, `--check-secrets` - `../fonoteka.go/parity/php_parity.sh` — reset/serve/artisan with parity-name refuse - `../fonoteka.go/parity/README.md` — isolation, record, replay, client capture commands - `tide/variables.go` — PHP-JSON `/` scrub, `response.json.query`, form-field PKCE/code scrub - `tide/normalize.go` — string `client_id` and unix `*_issued_at` masks - `tide/proxy.go` — 502 bodies include record-step errors ## Decisions Made - Record/reset refuse any `DB_DATABASE` whose basename does not contain `parity`. - Ordered `--update` recapture is required so 154 fixtures match a fresh-DB replay; DELETEs target missing ids so seed rows remain. - Cookie request headers are not kept; Laravel session cookies must not enter the corpus. - OAuth authorization codes are captured from consent `$.data.redirect_to` query `code` so replay can fill `{{oauth:code}}` before the token POST. - The S256 `code_verifier` cannot be reconstructed from the recorded `code_challenge`; `capture_clients.mjs` writes `/tmp/summercms-parity/pkce.vars` (0600, not committed) and README merges it after seed replay. ## Deviations from Plan ### Auto-fixed Issues **1. PHP `json_encode` slash-escaping left OAuth codes in consent JSON** - **Found during:** Task 3 (client secret scan) - **Issue:** Captured redirect URLs used `/` while PHP bodies used `\/`, so `{{oauth:redirect}}` never replaced `?code=` - **Fix:** `replaceAll` also substitutes `phpJSONEscape` (`/` → `\/`) - **Files modified:** `tide/variables.go`, `tide/capture_test.go` - **Verification:** recapture consent body is `{"data":{"redirect_to":"{{oauth:redirect}}"}}`; `--check-secrets` green - **Committed in:** `8f94b07` **2. RFC 7591 registration fields failed JSON compare on client replay** - **Found during:** Task 3 (OAuth self-replay) - **Issue:** `client_id` is a string and `client_id_issued_at` is unix, not Carbon `*_at` - **Fix:** mask string `client_id` and integer `*_issued_at` - **Files modified:** `tide/normalize.go`, `tide/normalize_test.go` - **Verification:** `mcp-oauth.yaml` replay matched - **Committed in:** `22f02ee` **3. Token capture 502 when authorization code was a substring of the PKCE verifier** - **Found during:** Task 3 (recapture after json.query) - **Issue:** value-based `replaceAll` corrupted `code_verifier` then `rejectUnclassifiedCredentials` failed the proxy - **Fix:** after value replace, scrub `request.form` fields by name - **Files modified:** `tide/variables.go`, `tide/capture_test.go`, `tide/proxy.go` - **Verification:** live recapture wrote `mcp-oauth`; token step has `code_verifier={{pkce:mcp}}` - **Committed in:** `7a6c669` (scrub), `2eb9b4b` (502 detail), `8881df9` (json.query) **4. Short captured numeric ids rewrote `127.0.0.1`** - **Found during:** Task 2/3 replay - **Issue:** isolated replace of id `1` inside IPv4 literals - **Fix:** earlier `1ea3c59` token-boundary replace; remaining `127.0.0.{{id:*}}` expands back to `1` on this seed - **Files modified:** `tide/variables.go` - **Verification:** client and 154-route self-replay green - **Committed in:** `1ea3c59` --- **Total deviations:** 4 auto-fixed (secret scrub, OAuth JSON mask, form-field PKCE, short-id isolation) **Impact on plan:** Required for a secret-clean, self-replayable corpus. No scope creep onto PHP/Nuxt/MCP. ## Issues Encountered - Playwright `@playwright/test` has no `chromium` export; resolve nested `playwright` via `createRequire`. - Nuxt login required waiting for `#__nuxt.__vue_app__` before filling `#email`. - Seed replay on a mutated DB returns 409; every recapture resets `php_parity.sh` first. - Client flows must replay on a freshly seeded DB, not after the 154 mutating route cases. ## User Setup Required None - no external service configuration required. Local PHP, Node (Nuxt/MCP deps), and `/tmp/summer` are enough. Recreate `pkce.vars` with `capture_clients.mjs --capture` before OAuth token replay if that file was deleted. ## Next Phase Readiness Ready for `02-04` (in-process Go replay with testcontainers Postgres). The PHP corpus is complete and self-consistent; route rows stay `pending` until ported. --- *Phase: 02-api-parity-harness-bootstrap* *Completed: 2026-09-17*