diff --git a/.convoys/adopt-playwright-smoke.md b/.convoys/adopt-playwright-smoke.md index dc5ef59..0761b87 100644 --- a/.convoys/adopt-playwright-smoke.md +++ b/.convoys/adopt-playwright-smoke.md @@ -13,8 +13,9 @@ skip: - role-a11y-auditor # tested-from-outside; a11y belongs in deeper specs - role-ux-reviewer # no UX surface - role-ia-architect # no IA surface -status: in-progress +status: shipped created: 2026-05-24 +shipped: 2026-05-24 parent: ship-readiness addresses: P1 #10 step 2 (launch sequence step 10) depends_on: @@ -657,3 +658,166 @@ slice_dependencies: Single brief; no `/multitask` fan-out. Conductor dispatches serially. + +## As-shipped + +Shipped 2026-05-24 as squash commit `7b6f751` (PR #18). The convoy +shipped in one PR (PR #18 architect-commit `3ac527e`, implementer-commit +`c72d006`) — Brief 1 as planned, with two small implementer deviations +from the brief's verbatim shape (both lint-baseline-preserving and +behavior-neutral). Capturing the deviations + the cross-validation +finding + empirical CI metrics here so the next architect / reviewer +has the audit trail. + +### Decisions ratified by operator at gate 1 + +Three of six decisions were architect-self-ratifiable (Decisions 2, 3, +5 — see § Decisions). The remaining three needed operator ratification, +and all three were ratified verbatim at gate 1: + +- **Decision 1 — keep `.ts` for Playwright specs.** `tests/smoke/app.smoke.spec.ts` + stayed `.ts`; the new `tests/visual/homepage.spec.ts` also shipped + as `.ts`. Empirically `npx eslint` exits 0 on both files against + the current config (`eslint-config-next@16`'s bundled + `typescript-eslint` chain parses them) — no `eslint.config.mjs` + change needed, lint baseline held at 128 problems. +- **Decision 4 — defer visual baselines to a Linux-Docker follow-up + PR.** No baseline images committed. `tests/visual/__screenshots__/` + does not exist in the tree at merge; the visual workflow's + `--update-snapshots=none` flag + `continue-on-error: true` + the + PR-comment step are the documented Decision-4 safety net. +- **Decision 6 — three simple scripts, no auto-boot wrapper.** + `package.json` got `test:smoke`, `test:visual`, `test:visual:update` + in that order. No `test:smoke:local` / `test:e2e` / `next dev` + auto-boot variant. Operator's local flow is "boot `next dev` in + one terminal, run `BASE_URL=http://localhost:3000 npm run test:smoke` + in another" — same shape as the existing `test` / `test:run` + scripts. + +### Implementer deviations from Brief 1's verbatim shape + +Two small deviations, both surfaced in the implementer's PR report +and both lint-baseline-preserving: + +1. **Removed the brief's `// eslint-disable-next-line no-console` + directive on the dev warn-and-continue branch of + `playwright.config.js`.** The brief specified the directive + verbatim to suppress an expected `no-console` complaint on the + `console.warn(...)` call. Empirically the current ESLint config + does NOT flag `console.warn` at all (only `console.log`), so the + `eslint-disable-next-line` directive itself becomes an unused- + directive lint error (`Unused eslint-disable directive`) and + would have regressed the baseline from 128 → 129. Removing the + directive is the behavior-neutral fix: the `console.warn` line + still runs unchanged, just without the no-longer-needed disable + comment. Lint baseline held at exactly 128 problems + post-implementation. +2. **Placed `@playwright/test` first in `devDependencies` for strict + alphabetical order.** The brief's prose was internally + inconsistent about placement (it called for "alphabetical + position" but then described the wrong neighbors — `@playwright` + sorts before `@testing-library/react` lexically). The implementer + followed the alphabetical rule rather than the prose's example, + so the final ordering is `@playwright/test` → + `@testing-library/dom` → `@testing-library/react` → `autoprefixer` + → ... Behavior-neutral; matches the convention used elsewhere in + the file (`dependencies` is alphabetical too). + +Both deviations are explicitly behavior-neutral — same code paths +execute, same env-var predicates, same lint count. Documenting them +here so a future reviewer comparing the brief's verbatim spec to +the merged diff sees the rationale instead of flagging drift. + +### Cross-validation: smoke test 2 locks in PR #15's "Sign in" CTA + +`tests/smoke/app.smoke.spec.ts`'s second test (`'sign-in page renders'`) +navigates to `/login` and asserts `await expect(page.getByRole('button', +{ name: /sign in/i })).toBeVisible({ timeout: 10_000 })`. That +assertion lights up only because the page renders a sign-in-named +control — which is exactly the `Sign in` +CTA that the `fix-layout-default-user` convoy (PR #15, squash +`ca302a8`) added to `components/Layout.js`'s logged-out branch when +it replaced the leaky maintainer-email default prop. **This convoy +effectively locks in a regression test for that earlier convoy's +work** — if a future change reverts to a hardcoded default user (or +breaks the logged-out CTA wording) the smoke check now fails the +PR. Surfaced organically from CI green; not a planned acceptance +criterion of this convoy but worth noting because P0 #7's resolved +state is now defended by a real CI signal, not just the 5 vitest +assertions in `test/components/Layout.test.js`. + +### As-shipped metrics (from post-merge run 26376162598 on `main`) + +- `Playwright smoke` workflow total runtime: **59 seconds**, exit 0 + (was: fast-fail at "playwright not installed" / "no config" before + this convoy — never reached `npx playwright test`). Comfortably + inside the < 5-minute success metric. +- `Run smoke tests` step: **3/3 tests pass in 2.9s** against the + Vercel preview URL with the `x-vercel-protection-bypass` header + applied: + - `home redirects or renders without 5xx` → ✓ 683ms + - `sign-in page renders` (the cross-validation above) → ✓ 459ms + - `public health endpoint responds` (`/api/health` 2xx) → ✓ 571ms +- Step breakdown: `Wait for Vercel Preview deployment` → success + (~200ms range, per PR #17's plumbing); `npm ci` + `setup-node` + + `playwright install --with-deps chromium` → success; `Run smoke + tests` → **success** (the failure mode shifted from "no config" + in PR #17's end state to "all green" here, which is the convoy's + target end state). +- `Screenshot diff` workflow: **not triggered on PR #18 itself**. + Its `paths:` filter excludes test-infra-only changes (the PR + touched only `package.json`, `package-lock.json`, + `playwright.config.js`, `tests/visual/homepage.spec.ts`, + `.gitignore`, and the convoy/brief docs — none of those are + under `pages/**` / `components/**` / `styles/**` / + `tailwind.config.js` / `postcss.config.js`). First real trigger + fires on the next PR touching any of those paths; at that point + the documented Decision-4 end state (test fails on missing + baseline → `continue-on-error: true` swallows it → comment-on-PR + step posts "Visual Diff — view run" with empty artifacts) gets + its first live exercise. +- Bypass secret leak check: **0 matches** against the raw workflow + log (per AC #7's grep-for-first-8-chars-of-secret pattern). GitHub + Actions auto-masks registered secrets; this convoy's + Decision-2 branches (`throw` in CI, `console.warn` in dev) name + the env var but never interpolate its value into any string, + so the mask never had to engage on output from our code. + +### Operator action required going forward + +`seed-visual-baselines-on-linux` is queued as the follow-up convoy +(see `.convoys/ship-readiness.md` § Queued convoys). Until that +PR lands, every `Screenshot diff` run on a PR touching +`pages/**` / `components/**` / `styles/**` will fail at the test +step and post a comment with empty artifacts. That is the +documented end state of this convoy per Decision 4; no operator +intervention is required to keep `Playwright smoke` green +(smoke runs against the existing spec, which has no baseline +dependency). + +If/when the operator rotates `VERCEL_AUTOMATION_BYPASS_SECRET` +via the Vercel dashboard, both workflows fail with Vercel SSO +challenge pages on every PR until the GitHub secret is re-seeded +(`gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body ""`). +Same human-responsibility pattern as `JWT_SECRET` rotation; +documented in `AGENTS.md` § 7. + +### What did NOT change + +- `tests/smoke/app.smoke.spec.ts` (existed pre-convoy; untouched + per Decision 1). +- `eslint.config.mjs` (Decision 5 + Finding 2; empirically clean + without any change). +- `pages/api/health.js` (already returns 200 anonymously; smoke + test 3 passes against it without any handler change). +- Any source under `pages/**` / `components/**` / `lib/**` / + `scripts/**` (this convoy is test-infra-only by scope). +- `vitest.config.js` / `test/setup.js` / anything under `test/` + (the two runners stay independent per the convoy's Test plan §; + `npm run test:run` still passes 21/21 at merge — no vitest + regression from the new `@playwright/test` install). +- Any `.github/workflows/*.yml` file (owned by PR #16 / PR #17 / + `fix-lint-baseline`; this convoy made the YAML's existing + invocations work, not modified them). +- `tests/visual/__screenshots__/` (does not exist; Decision 4 + defers to `seed-visual-baselines-on-linux`). diff --git a/.convoys/ship-readiness.md b/.convoys/ship-readiness.md index f841869..f9ca102 100644 --- a/.convoys/ship-readiness.md +++ b/.convoys/ship-readiness.md @@ -153,11 +153,11 @@ These MUST land before any anonymous traffic touches the production URL. - **Impact:** The first agent-driven refactor of `getUserFromRequest` (P0 #1) is high-blast-radius with no safety net. - **Fix sequence:** - 1. Install `vitest`. Add `npm run test:run` script. - 2. Install `@playwright/test`. Wire up `tests/smoke/app.smoke.spec.ts` (already drafted; needs `playwright.config.ts`). - 3. Re-enable the `test:` job in `.github/workflows/ci.yml` (commented out at install time). + 1. Install `vitest`. Add `npm run test:run` script. **RESOLVED** by `fix-auth-bypass` Brief 5, commit `1629afb`. + 2. Install `@playwright/test`. Wire up `tests/smoke/app.smoke.spec.ts` (already drafted; needs `playwright.config.ts`). **RESOLVED 2026-05-24** by `adopt-playwright-smoke`, PR #18 squash `7b6f751` — 3/3 smoke tests pass in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys and `.convoys/adopt-playwright-smoke.md` § As-shipped. + 3. Re-enable the `test:` job in `.github/workflows/ci.yml` (commented out at install time). Next remaining step in this fix sequence. 4. Add unit tests for `lib/permission-middleware.js`, `lib/slug-utils.js`, `pages/api/auth-utils.js`. - 5. Wire `preview-smoke.yml` to run against the Vercel preview URL. + 5. Wire `preview-smoke.yml` to run against the Vercel preview URL. **RESOLVED 2026-05-24** by `fix-vercel-deployment-protection-in-ci` (PR #17, `9a3e077`) + `adopt-playwright-smoke` (PR #18, `7b6f751`). - **Owns:** `role-architect` (test strategy) → `role-implementer` (initial suite). ### 11. No migration tool — `scripts/add-*.js` graveyard @@ -281,7 +281,7 @@ Each phase is one Conductor-created convoy. Don't run more than two in parallel 7. **`migration-tool`** (P1 #11). One PR. Backfill + first new migration. 8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written. 9. **`single-auth-provider`** (P1 #9). 3-5 PRs via multitask. -10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR. +10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR. **RESOLVED 2026-05-24** — PR #18 squash `7b6f751`; smoke 3/3 green in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys for the full as-shipped block. 11. **`schema-cleanup`** (P2 #14). Multi-PR convoy via multitask. 12. **`god-component-split`** (P2 #13). One convoy per file; fan out via multitask once architect's `slice_dependencies` are written. 13. **`launch-polish`** (P3). UX/IA/a11y/docs convoy. @@ -299,6 +299,25 @@ Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch seq - **`single-auth-provider`** (priority: P1 quality, also listed as launch sequence step 9). Collapse `lib/auth-context.js` + `lib/admin-auth.js` into `lib/use-auth.js`. Surfaced again as a follow-up in P0 #7 § Flagged-but-deferred (4 pages still import the legacy `useAuth`). - **`cleanup-mobile-nav-dead-props`** (priority: P3 polish). `components/MobileNavigation.js` accepts a dead `user` prop; remove it. Surfaced in P0 #7 § Flagged-but-deferred. May fold into `god-component-split` (P2 #13) if that lands first. - **`bump-eslint-10`** (priority: P2 hygiene; upstream-blocked). Bump ESLint from v9 to v10 once `typescript-eslint` ships a v10-tested release and `eslint-config-next` bundles it. Surfaced in `.convoys/bump-next-js.md` § Decisions D. +- **`seed-visual-baselines-on-linux`** (priority: P2 CI infra; **operator action required**). Generate Linux baselines for `tests/visual/__screenshots__/` in the `mcr.microsoft.com/playwright:v1.60.0-noble` Docker image and commit them in a small follow-up PR. Mac-generated baselines would silently overwrite Linux CI baselines because `playwright.config.js`'s custom `snapshotPathTemplate` has no `{platform}` token (Risk R3 + Boot-the-brief Finding 7 in `.convoys/adopt-playwright-smoke.md`). Until this PR lands, every `Screenshot diff` run on a PR touching `pages/**` / `components/**` / `styles/**` / Tailwind/PostCSS config fails at the test step and posts a "Visual Diff — view run" comment with empty artifacts — that's the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression. One small PR with just the PNG baseline(s). Surfaced 2026-05-24 as the follow-up to `adopt-playwright-smoke` (PR #18). +- **`adopt-playwright-smoke`** (priority: P1 quality, also listed as launch sequence step 10 / P1 #10 step 2) — **RESOLVED 2026-05-24**. + - **Resolved by:** squash commit `7b6f751` (PR #18, architect-commit `3ac527e`, implementer-commit `c72d006`). Brief 1 shipped as planned with two small lint-baseline-preserving deviations from the brief's verbatim shape (documented in the convoy file's § As-shipped). + - **As-shipped surface:** `@playwright/test@^1.60.0` added to `devDependencies`; new `playwright.config.js` at repo root (ESM, two projects partitioned by `testMatch` — `smoke` + `visual`, CI-fail-loud / dev-warn predicate on `VERCEL_AUTOMATION_BYPASS_SECRET` per Decision 2, `snapshotPathTemplate: 'tests/visual/__screenshots__/{arg}{ext}'` aligned with `visual-diff.yml`'s artifact upload path); new `tests/visual/homepage.spec.ts` (1 test, no baseline committed per Decision 4); three new `package.json` scripts (`test:smoke`, `test:visual`, `test:visual:update`); three new `.gitignore` entries (`/playwright-report/`, `/test-results/`, `/.playwright/`). No `eslint.config.mjs` change (Decision 5 + Finding 2 verified clean empirically). No source touched under `pages/**` / `components/**` / `lib/**`. + - **Implementer deviations** (both behavior-neutral, both lint-baseline-preserving): + 1. Removed Brief 1's `// eslint-disable-next-line no-console` directive on `playwright.config.js`'s `console.warn` branch — the current ESLint config does not flag `console.warn` at all, so the disable directive itself would have regressed lint from 128 → 129 as an "Unused eslint-disable directive" error. + 2. Placed `@playwright/test` first in `devDependencies` for strict alphabetical correctness — the brief's prose was internally inconsistent on neighbors (`@playwright` sorts lexically before `@testing-library/react`). + - **As-shipped metrics** (from post-merge `Playwright smoke` run 26376162598 on `main`): + - `Playwright smoke` workflow total runtime: **59 seconds**, exit 0 (was: fast-fail at "playwright not installed" / "no config" before this convoy). + - `Run smoke tests` step: **3/3 tests pass in 2.9s** against the Vercel preview with `x-vercel-protection-bypass` header applied — `home redirects or renders without 5xx` ✓ 683ms / `sign-in page renders` ✓ 459ms / `public health endpoint responds` ✓ 571ms. + - `Screenshot diff` workflow: **not triggered on PR #18 itself** because its `paths:` filter excludes test-infra-only changes; first real trigger fires on the next PR touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js`. At that point the documented Decision-4 end state runs live (test fails on missing baseline → `continue-on-error: true` swallows → comment-on-PR step posts run link with empty artifacts). + - Bypass secret leak check: **0 matches** in the raw workflow log. GitHub Actions auto-masks registered secrets; our Decision-2 branches name the env var but never interpolate the value into any string. + - **Cross-validation finding** (not a planned AC; surfaced organically from CI green): smoke test 2 (`'sign-in page renders'`) asserts `await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible()` against `/login`, which only passes because `components/Layout.js` renders the `Sign in` CTA on the logged-out branch that PR #15 (`fix-layout-default-user`, `ca302a8`) introduced. P0 #7's resolved state is now defended by a live CI signal — if a future PR reverts to a hardcoded default user or breaks the CTA wording, smoke fails the PR (in addition to the 5 vitest assertions in `test/components/Layout.test.js`). + - **Operator action required going forward:** `seed-visual-baselines-on-linux` (above) is the follow-up. Until it lands, `Screenshot diff` runs post a "Visual Diff — view run" comment with empty artifacts on every UI-touching PR — that is the Decision-4 end state, not a regression. No operator action is required to keep `Playwright smoke` green. + - **Flagged-but-deferred** (deliberately out of scope per the convoy file, restated here for the audit trail): + 1. `seed-visual-baselines-on-linux` — see above. + 2. `adopt-test-smoke-local` (possible follow-up) — a `test:smoke:local` wrapper that auto-boots `next dev`. Explicitly rejected by Decision 6; queue only if dev friction proves out. + 3. Deeper E2E coverage beyond the 3 existing smoke checks — per-feature work in feature convoys, not a test-infra concern. + - **Owns:** `role-architect` (3 of 6 decisions self-ratified — D2 CI predicate, D3 two-project shape, D5 no-eslint-change; 3 of 6 operator-ratified — D1 keep `.ts`, D4 defer baselines, D6 simple scripts) → `role-implementer` (Brief 1, plus the two deviations above). - **`fix-vercel-deployment-protection-in-ci`** (priority: P2 CI infra) — **RESOLVED 2026-05-24**. - **Resolved by:** squash commit `9a3e077` (PR #17), comprising three commits, not one. Operator prereq seeded 2026-05-24T20:03:31Z (`gh secret set VERCEL_AUTOMATION_BYPASS_SECRET`; confirmed via `gh secret list`); the implementer dispatch waited on that visibility per the convoy file's "Operator action required" gate. - **Three-commit reality** (Brief 1 + two scope expansions found during CI validation): diff --git a/AGENTS.md b/AGENTS.md index 7090114..468120f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,16 +68,41 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560 ## 6. Testing -- **Unit-test runner:** `vitest@^3.2.4` (installed via `fix-auth-bypass` Brief 5, commit `1629afb`). `npm test` for watch mode; `npm run test:run` for the CI / single-shot mode. Config in `vitest.config.js`, setup in `test/setup.js` (sets `JWT_SECRET` + `NODE_ENV=test` before any module loads). Specs live under `test/` mirroring source layout (`test/lib/*.test.js`, `test/api/*.test.js`). -- **Coverage today:** 16 unit tests covering the post-`fix-auth-bypass` auth surface — `lib/auth-secret.js` (3), `lib/permission-middleware.js::getUserFromRequest` (8, incl. a negative regression against the old synthetic-admin shape — Gotcha #2), and `pages/api/auth-utils.js` (5). These tests lock in the contracts established by Briefs 1 and 2; do not weaken them when refactoring auth. -- **CI:** the `test:` job in `.github/workflows/ci.yml` runs `npm run test:run` on every PR and push to `main` and is **blocking** (no `|| true`, no `continue-on-error`). A red test job blocks merge. -- **E2E / smoke runner:** `@playwright/test` is **still pending** — queued for the `adopt-playwright-smoke` convoy (see `.convoys/ship-readiness.md` § Proposed launch sequence step 10). Until it lands, `preview-smoke.yml` and `visual-diff.yml` are no-ops on the smoke side. -- **Manual QA:** `TESTING_GUIDE.md` still applies for surfaces not yet covered by automated tests (UI flows, scanner camera path, import jobs). +- **Unit-test runner:** `vitest@^3.2.4` (installed via `fix-auth-bypass` Brief 5, commit `1629afb`). `npm test` for watch mode; `npm run test:run` for the CI / single-shot mode. Config in `vitest.config.js`, setup in `test/setup.js` (sets `JWT_SECRET` + `NODE_ENV=test` before any module loads). Specs live under `test/` mirroring source layout (`test/lib/*.test.js`, `test/api/*.test.js`, `test/components/*.test.js`). Last green: 21/21 tests pass. +- **Vitest coverage today:** 21 unit tests — `lib/auth-secret.js` (3), `lib/permission-middleware.js::getUserFromRequest` (8, incl. a negative regression against the old synthetic-admin shape — Gotcha #2), `pages/api/auth-utils.js` (5), and `components/Layout.js` (5 regression-lock assertions for the post-PR-#15 logged-out branch — Gotcha #8). These tests lock in the contracts established by `fix-auth-bypass` Briefs 1 + 2 and `fix-layout-default-user`; do not weaken them when refactoring auth or Layout. +- **E2E / smoke runner:** `@playwright/test@^1.60.0` (installed via `adopt-playwright-smoke`, PR #18 squash `7b6f751`). Config in `playwright.config.js` (root, ESM) declares two projects: + - `smoke` — `tests/smoke/**/*.spec.@(ts|js)`; invoked by `.github/workflows/preview-smoke.yml`. `npm run test:smoke` locally. + - `visual` — `tests/visual/**/*.spec.@(ts|js)`; invoked by `.github/workflows/visual-diff.yml`. `npm run test:visual` locally; `npm run test:visual:update` to (re-)seed baselines. + + Local-run convention: boot `next dev` in one terminal, then in another run `BASE_URL=http://localhost:3000 npm run test:smoke` (or against a deployed preview, `BASE_URL=https://.vercel.app VERCEL_AUTOMATION_BYPASS_SECRET= npm run test:smoke`). No `next dev` auto-boot in the test scripts (Decision 6 of `adopt-playwright-smoke`). +- **Browsers must be installed once locally:** `npx playwright install --with-deps chromium`. CI re-runs this on every workflow run (it's cached when possible). +- **Visual baselines:** none committed yet. `tests/visual/__screenshots__/` is intentionally absent and intentionally NOT in `.gitignore` (baselines, when they exist, must be committed). First-run baseline generation MUST happen in a Linux environment so the PNG matches what CI produces. Recommended path is the Playwright Docker image: + + ``` + docker run --rm -v "$PWD":/work -w /work \ + mcr.microsoft.com/playwright:v1.60.0-noble \ + sh -c "npm ci && BASE_URL= \ + VERCEL_AUTOMATION_BYPASS_SECRET= \ + npm run test:visual:update" + ``` + + Mac-generated baselines will NOT match Linux CI — `playwright.config.js`'s custom `snapshotPathTemplate` has no `{platform}` token, so a Mac update silently overwrites the canonical Linux baseline. Tracked as the queued `seed-visual-baselines-on-linux` convoy (see `.convoys/ship-readiness.md` § Queued convoys). +- **CI behavior:** + - **Vitest:** the `test:` job in `.github/workflows/ci.yml` runs `npm run test:run` on every PR and push to `main` and is **blocking** (no `|| true`, no `continue-on-error`). A red test job blocks merge. + - **Playwright smoke:** runs on every PR via `preview-smoke.yml`. Gate skip via `pipeline: skip smoke` in the PR body (handled in the `gate:` job's Decide step via env-var routing — see § 7's shell-injection note). Last measured runtime: 59s end-to-end, 3/3 tests pass in 2.9s (PR #18 post-merge run). + - **Screenshot diff:** runs only on PRs touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js` via `visual-diff.yml`. **First `Screenshot diff` run after `adopt-playwright-smoke` will fail at the test step** because no baseline exists yet; `continue-on-error: true` swallows the failure and the comment-on-PR step posts "Visual Diff — view run" with empty artifacts. That is the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression — it stays that way until `seed-visual-baselines-on-linux` lands. +- **Manual QA:** `TESTING_GUIDE.md` still applies for flows not yet covered by automated tests (scanner camera path, card-import jobs, multi-step UI wizards). The automated smoke + visual suite is steadily eclipsing it; `TESTING_GUIDE.md` will be renamed to `docs/MANUAL_QA.md` and trimmed to truly-manual-only flows in a future cleanup convoy (see `.convoys/ship-readiness.md` § Role-doc-writer findings). ## 7. Deployment - **Vercel** auto-deploys `main` and creates Preview deployments for every PR. `vercel.json` and `.vercel/` are committed. CI in `.github/workflows/` runs lint + types (no duplicate build — Vercel handles it). -- **Preview protection bypass for automation.** The project has a Protection Bypass for Automation token exposed locally as `VERCEL_AUTOMATION_BYPASS_SECRET` in `.env.local` (not committed) and seeded into GitHub Actions as a repo secret (`gh secret set VERCEL_AUTOMATION_BYPASS_SECRET`, 2026-05-24). The `preview-smoke` and `visual-diff` workflows pass the bypass as a **query parameter** on `wait-for-vercel-preview@v1.3.2`'s `path:` input — `path: '/?x-vercel-protection-bypass=…'`, bare form, **without** `&x-vercel-set-bypass-cookie=true` (the cookie variant returns 307 + Set-Cookie and axios in Node has no cookie jar, so it 401s on the redirect). The header form (`x-vercel-protection-bypass: `) is reserved for the future `playwright.config.js` that `adopt-playwright-smoke` will ship (Playwright has its own request shape and `extraHTTPHeaders` is cleaner there). Do not log or echo the value. If the operator rotates the token in the Vercel dashboard, re-seed the GitHub secret via `gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body ""`. See `.convoys/fix-vercel-deployment-protection-in-ci.md`. +- **Preview protection bypass for automation.** The project has a Protection Bypass for Automation token exposed locally as `VERCEL_AUTOMATION_BYPASS_SECRET` in `.env.local` (not committed) and seeded into GitHub Actions as a repo secret (`gh secret set VERCEL_AUTOMATION_BYPASS_SECRET`, 2026-05-24). The secret is consumed in two shapes: + 1. **Query parameter** on `wait-for-vercel-preview@v1.3.2`'s `path:` input in both `preview-smoke.yml` and `visual-diff.yml` — `path: '/?x-vercel-protection-bypass=…'`, bare form, **without** `&x-vercel-set-bypass-cookie=true` (the cookie variant returns 307 + Set-Cookie and axios in Node has no cookie jar, so it 401s on the redirect). Plumbed by PR #17 (`fix-vercel-deployment-protection-in-ci`, squash `9a3e077`). + 2. **HTTP header** in `playwright.config.js`'s `use.extraHTTPHeaders` — `'x-vercel-protection-bypass': `. Playwright's browser context has a real cookie jar so this shape works there, and the testOptions surface forwards the header to the test-level `request` fixture's `APIRequestContext` as well, so both `page.goto(...)` calls and `request.get('/api/health')` calls hit the protected preview correctly without per-spec header injection. Plumbed by PR #18 (`adopt-playwright-smoke`, squash `7b6f751`) per Decision 2 of that convoy. + + Decision 2 also wires a **fail-loud-in-CI / warn-in-dev** predicate: `if (process.env.CI === 'true' && !process.env.VERCEL_AUTOMATION_BYPASS_SECRET) throw ...` (with an error message that names the env var, the `gh secret set` rotation command, and points at this section); otherwise `console.warn` once and continue with `extraHTTPHeaders` undefined. Same fail-closed / warn-and-no-op shape as `lib/rate-limit.js`'s Upstash predicate — see Gotcha #12. + + Do not log or echo the value. If the operator rotates the token in the Vercel dashboard, re-seed the GitHub secret via `gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body ""`. See `.convoys/fix-vercel-deployment-protection-in-ci.md` and `.convoys/adopt-playwright-smoke.md`. - **Shell-injection hardening in workflow YAML.** Never inline `${{ github.event.* }}` directly into a `run:` block — route the value through the step's `env:` block and quote it (`"$VAR_NAME"`) in shell. PR #17's CI validation caught a real syntax error from a PR body containing `(` because the gate-job's Decide step inlined `${{ github.event.pull_request.body }}` straight into bash; commit `b6f8688` swept both `preview-smoke.yml` and `visual-diff.yml` to the `env:` + quoted-shell pattern. This is GitHub's official Security Hardening guidance ("Security hardening for GitHub Actions" → "Using a third-party action"). Apply to any new workflow that reads PR body / title / branch name / commit messages in shell. ## 8. Code graph