diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48ff1e7489..9609bc0542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -555,31 +555,15 @@ jobs: # report as `cancelled` and re-create exactly the ambiguity #095 removed. fail-fast: false matrix: - # THREE, and the count is measured rather than chosen. `fullyParallel: false` - # makes a spec file the indivisible unit, so shard sizes are lumpy and more - # shards is not monotonically faster. Re-measured 2026-07-30 against this - # merged tree — 342 non-quarantine/non-mockup chromium tests - # (`--list --shard=i/N`): - # N=3 -> 121/111/110 largest 121 (35%) - # N=4 -> 121/106/98/17 largest 121 (35%) — same bound, one more runner - # Earlier on a 340-test tree, N=5 -> 121/106/0/96/17 and N=8 gave two empty - # shards. N=4 buys nothing over N=3 because one 121-test spec group bounds - # both, and any N with an empty shard would go RED: `test:e2e:pr` - # deliberately omits `--pass-with-no-tests` (only the advisory lane has it). - # Re-measure before changing this number, and keep every shard non-empty: - # npm run ensure - # PLAYWRIGHT_BASE_URL= npx playwright test --project=chromium \ - # --grep-invert "@quarantine|@mockup" --shard=i/N --list - # - # MEASURED on the first real sharded run (CI 30530618838, 2026-07-30): - # shard 1 -> 121 tests, 9m36 - # shard 2 -> 111 tests, 6m54 - # shard 3 -> 110 tests, 6m20 - # Per-test cost is NOT uniform — 111 tests took 6m54 while 121 took 9m36 — - # so counting tests UNDERSTATES the largest shard. A count-balanced split is - # the best `--shard` can do; balancing by duration would mean splitting the - # slow spec files themselves. Predicting from test count alone was wrong by - # ~40% here, so trust a measured run over the arithmetic. + # THREE explicit duration-aware file groups (scripts/playwright-pr-shards.mjs), + # not Playwright `--shard=i/N`. Count-balanced `--shard` packed the slow + # phone-scroll family into one runner (measured 9m36 vs 6m54/6m20 on + # CI 30530618838). Explicit groups mix slow-per-test specs with faster + # mega-specs. `tests/playwright-pr-shards.test.ts` fails closed if any + # production e2e:pr file is missing, duplicated, or leaves a shard empty. + # Re-measure wall time after suite growth before reshuffling membership: + # node scripts/playwright-pr-shards.mjs --list + # node scripts/playwright-pr-shards.mjs --validate shard: [1, 2, 3] steps: - name: Checkout @@ -607,7 +591,7 @@ jobs: env: PLAYWRIGHT_BUILD_ROOT_ID: ci-production PLAYWRIGHT_KEEP_BUILD_ROOT: "true" - run: npm run test:e2e:pr -- --shard=${{ matrix.shard }}/3 + run: npm run test:e2e:pr:shard -- --shard ${{ matrix.shard }} - name: Classify exact failed test identities if: failure() diff --git a/AGENTS.md b/AGENTS.md index 865167cd66..e5cd17c0eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1061,7 +1061,7 @@ Durable notes for Cloud Agents. Standard commands live in `README.md` and `packa - Live-mode caveat: `RAG_PROVIDER_MODE=auto` attempts OpenAI (fast → strong route); if generation fails the built-in quality gates it silently degrades to a deterministic "Source-only" answer that still cites real documents — this is expected, not a failure. The header sign-in UI exposes magic-link + OAuth only (no password field), but the `/api/answer` + retrieval flow works server-side without a browser session. - What still won't run in this VM even with secrets: `npm run worker` also needs the Python OCR stack (`worker/python/requirements.txt`) and heavy parsing deps; Supabase edge functions need Deno v2.x + deployment. `verify:release` additionally runs governance/eval gates. Treat missing-secret failures of `check:supabase-project`/`verify:release` in demo mode as expected, not regressions. - Dev server: `npm run dev` selects a stable per-project localhost port (e.g. `4461`), binds `0.0.0.0`, and prints the exact URL. Never assume port 3000/3001/3002. `npm run ensure` starts/verifies it in the background. -- Verification without secrets: `npm run lint`, `npm run typecheck`, and `npm run test` (vitest) all pass offline. `npm run verify:cheap` also runs runtime, GitHub Actions pin, CI-scope, and sitemap checks. `npm run verify:pr-local` adds format, conditional build/client-bundle scanning, and RAG fixture/manifest validation without repeating unit tests; browser, Docker/Supabase, audit, and provider checks remain separate. See `docs/testing.md` for lock, live-test, Playwright, and flake-ledger rules. +- Verification without secrets: `npm run lint`, `npm run typecheck`, and `npm run test` (vitest) all pass offline. `npm run verify:cheap` also runs runtime, GitHub Actions pin, CI-scope, and sitemap checks. `npm run verify:pr-local` adds format, conditional build/client-bundle scanning, and RAG fixture/manifest validation without repeating unit tests; browser, Docker/Supabase, audit, and provider checks remain separate. See `docs/testing.md` for lock, live-test, Playwright, and flake-ledger rules. If `check:installed-lock-parity` or `check:playwright-browser-revision` reports Playwright/image drift (`#255`), do not force a mismatched Chromium path — delegate browser proof to CI Production UI (see `docs/testing.md` § Testing speed playbook). - For GitHub-related work authorised in this session, prefer the connected GitHub connector/MCP tools first for PR, issue, comment, review-thread, and Actions tasks they support (including run/job/log/artifact inspection and review-thread replies/resolution). diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index fd2d7f55a8..dd5be127f7 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -700,34 +700,20 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-07 | cursor/viewer-phase0-gesture-a11y-1db8 (PR #1660) | 097dfd245f798f8105eeb6c1cf4fc077f969496f | prlanded | MERGED; squash tip empty vs branch tip 810cfc9b4a1c476a0dcc995bffb00d7329a85686; Phase 0 gesture INP, preview a11y, image decode | content tree empty vs squash; no provider-backed checks run | | 2026-08-07 | claude/pr-handoff-loop-prevention-54y5zr (PR #1670) | dfe2946110e0ff93bd4acc571ae79c26b79a7a85 | PR #1670 heavy review-and-fix | synced origin/main (behind-but-clean DIRTY cleared); fixed CodeRabbit checks-cell to name exact #1649 gates + incomplete verify:pr-local/ui + no provider checks; Bugbot none; no P0/P1; #258/#ledger delta accurate; merge-tree clean; threads cleared | verify:cheap 519 files/5493 passed; verify:pr-local docs scope (format+docs+ledger+outstanding-issues); check:branch-review-ledger; check:outstanding-issues; no provider gates | | 2026-08-07 | claude/handover-review-nlhuln | 978623337c12dc1721fe5236eadbf9a5ad929f03 | mode nav remaining modes: factsheets adoption (PR #1674) | Adopted the shared ModeNav for factsheets (Topics + Search); replaced the action-only entry, added the activeId branch, q/category/run carry, BookOpenText icon; three pinned adopted-mode lists updated together; record-route protection pinned at render now the item-count protection has expired | lint clean; typecheck clean; test 518/519 files (pr-handoff-stop failure confirmed pre-existing via stashed re-run); focused 5 files 95 tests; ui-mode-nav-density 55 passed incl 7 new factsheets rows; two mutation checks confirmed red; format committed; verify:pr-local blocked at check:installed-lock-parity (playwright 1.62.0 vs 1.62.1) | -| 2026-08-07 | cursor/privacy-live-signal-variants-bc81 (PR #1676) | f63eba10f5e9b6db047302c34412ea5261cd410b | PR #1676 unblock | before: PR policy fail (missing Clinical Governance Preflight; privacy* mockup paths trip clinicalRisk), behind-but-clean then main advanced; after: PR body preflight completed (policy green), tip synced to main via merge f63eba10 (duplicate local merge discarded), merge-tree clean, 0 unresolved threads, required CI in progress on synced tip; no code fix needed | local evaluatePullRequestPolicy ok after body; PR policy run 31172248093/31172327100 success; merge-tree clean; no provider-backed checks run | | 2026-08-07 | claude/handover-review-nlhuln | 4ff613c10fbf734b1e740a31611296c17c791ec7 | mode nav remaining modes: vestigial strip removal (PR #1679) | Removed the single-button action strip from answer/documents/services/forms/favourites/prescribing/tools; deleted the registry index-0 fallback (TS2493-forced) and the dead documents clause; stripped modeItems/onSearch/modeAriaLabel/stickyTop from PageSecondaryNavigation, keeping the empty-registry return below the information-section branch; kept the action kind with a no-live-consumer note. Completes the 13-mode navigation rollout. | lint exit 0; typecheck clean; focused 5 files 97 tests; test 518/519 files (pr-handoff-stop re-confirmed pre-existing on this base via stashed re-run); ui-mode-nav-density + ui-accessibility 71 passed (landmark scan green); branch-order guard mutation-checked (hoisting it fails 2 tests); format committed; verify:pr-local blocked at check:installed-lock-parity (playwright 1.62.0 vs 1.62.1) | -| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | dec8f7489b4e5492e924af30dec85859932af3bb | review-and-fix | synced origin/main (behind-but-clean DIRTY; merge-tree clean); Bugbot mid-table finding dispositioned (row was tip-append before #1679; post-merge order correct; ledger guard passed); no P0/P1; 0 threads; no code fix; merge left to user | check:branch-review-ledger pass; ledger:dedupe none; merge-tree clean vs origin/main; format unchanged; no provider gates | -| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | 574702a681cbb4d455151da023428d16c22fb460 | review-and-fix | late-synced origin/main after CI green (brought #1678 cn/tailwind-merge; remote merge 574702a6); prior sync cleared DIRTY; Bugbot mid-table finding dispositioned (tip-append before #1679; post-merge order correct); no P0/P1; 0 threads; no code fix; merge left to user | check:branch-review-ledger pass; ledger:dedupe none; merge-tree clean; prior tip required CI green; format unchanged; no provider gates | | 2026-08-05 | HEAD | 2a8881afad230880166de60a533e17588d9920ec | seven-report repo-wide audit | P2 confirmed: live drift and live-only migration; mobile CLS; ACL runner; OOXML declared-size robustness; assertion false positives. Numerous report claims stale, narrowed, or unsafe. | verify:cheap (5088 pass, 3 skip, 1 timeout); focused retry 21/21 pass; build pass; bundle budget pass; offline RAG 574/574; assertions 98%; live drift 34; browser CLS 0.228/0.218 | | 2026-08-07 | codex/consolidated-ledger-updates (PR #1683) | 413e679bb92cb19717d6d8301764df44694eb73e | review-and-fix PR #1683 | synced origin/main (behind-but-clean DIRTY cleared); restored main ledger order + sole seven-report row; Bugbot none; no P0/P1; merge-tree clean | verify:pr-local docs scope PASS (format:changed Prettier; check:branch-review-ledger 648; docs links 1650; outstanding-issues 258); merge-tree clean | -| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | fddf495b5176f570a5238b5c17326f64f333ecff | review-and-fix | synced origin/main (behind-but-clean DIRTY; merge-tree clean; main advanced with #1684/#1666); 0 review threads; no P0/P1; no code fix; merge left to user | merge-tree clean vs origin/main; ledger:dedupe none; prior tip required CI green except stale PR mergeability; format unchanged; no provider gates | +| 2026-08-07 | PR #1686 / cursor/site-testing-speed-08c1 | 4dca89079dbfb5bb676ccc385d7dbafcd8ab90f5 | testing-speed: phone-chrome keep-root, pr-local #167, explicit UI shards, viewport trim, playwright revision #255 | implemented; focused + ci-workflow contracts green; Production UI wall-time confirmation pending first CI run | vitest focused+ci-workflows; playwright-pr-shards --validate; check:playwright-browser-revision; check:outstanding-issues | | 2026-08-07 | cursor/inpage-nav-default-235a | c6d72e406c03e205bac86d23e84813c41332c205 | docs: default in-page nav DocumentViewer chrome + PhoneHeaderCollapsePortal | docs-only; verify:pr-local passed (low-risk docs scope) | verify:pr-local --files AGENTS.md,docs/search-chrome-behaviour.md | +| 2026-08-07 | cursor/privacy-live-signal-variants-bc81 (PR #1676) | f63eba10f5e9b6db047302c34412ea5261cd410b | PR #1676 unblock | before: PR policy fail (missing Clinical Governance Preflight; privacy* mockup paths trip clinicalRisk), behind-but-clean then main advanced; after: PR body preflight completed (policy green), tip synced to main via merge f63eba10 (duplicate local merge discarded), merge-tree clean, 0 unresolved threads, required CI in progress on synced tip; no code fix needed | local evaluatePullRequestPolicy ok after body; PR policy run 31172248093/31172327100 success; merge-tree clean; no provider-backed checks run | +| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | dec8f7489b4e5492e924af30dec85859932af3bb | review-and-fix | synced origin/main (behind-but-clean DIRTY; merge-tree clean); Bugbot mid-table finding dispositioned (row was tip-append before #1679; post-merge order correct; ledger guard passed); no P0/P1; 0 threads; no code fix; merge left to user | check:branch-review-ledger pass; ledger:dedupe none; merge-tree clean vs origin/main; format unchanged; no provider gates | +| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | 574702a681cbb4d455151da023428d16c22fb460 | review-and-fix | late-synced origin/main after CI green (brought #1678 cn/tailwind-merge; remote merge 574702a6); prior sync cleared DIRTY; Bugbot mid-table finding dispositioned (tip-append before #1679; post-merge order correct); no P0/P1; 0 threads; no code fix; merge left to user | check:branch-review-ledger pass; ledger:dedupe none; merge-tree clean; prior tip required CI green; format unchanged; no provider gates | +| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | fddf495b5176f570a5238b5c17326f64f333ecff | review-and-fix | synced origin/main (behind-but-clean DIRTY; merge-tree clean; main advanced with #1684/#1666); 0 review threads; no P0/P1; no code fix; merge left to user | merge-tree clean vs origin/main; ledger:dedupe none; prior tip required CI green except stale PR mergeability; format unchanged; no provider gates | | 2026-08-07 | claude/issues-256-section-nav-clean | 169323053db5c572d183d59c113ecd0c76e7aca5 | issues #256: forms section anchors + differentials presentation set (PR #1697) | Wired all six formSections anchors in form-detail-page.tsx (four direct ids, two breakpoint pairs via existing mobile wrappers and single-child desktop wrappers, no component signature change); deleted differentialPresentationSections and declared /differentials/presentations/ locally-owned instead, since three of its six sections declared a -mobile targetId ReviewPanels can never satisfy and the page owns MobileTabs below xl plus the xl review sidebar. Added a registered browser spec because source-text and jsdom guards both structurally cannot see breakpoint-variant resolution. | lint exit 0; typecheck clean; test 519/520 files (pr-handoff-stop confirmed pre-existing via stashed re-run); check:gate-manifest and check:ci-scope pass with the new spec in both playwright allowlists; ui-forms-section-nav + ui-accessibility 18 passed incl real-record nav with 6 links and exactly one variant per pair visible at 390px and 1280px; binding guard mutation-checked red on one removed id; browser spec observed failing when nav genuinely absent; format clean. Environment: npm ci blocked (main lockfile needs Node >=24.15, container has 24.13), tailwind-merge@3.6.0 materialised from tarball only | | 2026-08-07 | claude/handover-review-nlhuln | de8b74e2fb94d1ec9b1982c15a2dea43421c3eee | outstanding-issues ledger capture after the mode-nav rollout (PR #1685) | Confirmed #256's two remaining suspected section sets are dead (/forms/ and /differentials/presentations/ draw no section nav; form-decision-context-mobile is a testId not an id; ruled out sectionId indirection in both files). Added #261 (delete-or-keep the consumer-less action kind) and #262 (addon-slot single-owner rule held by two lists agreeing by coincidence). #207/#226/#231 reviewed and deliberately left untouched as existing P1 rows. | check:outstanding-issues passed (260 rows, 119 open, unique ids, no ids deleted from base 1ff9ed206456); prettier --check clean on the changed file; rows written via scripts/outstanding-issues.mjs, never hand-edited; no code gates run - docs-only diff | | 2026-08-07 | claude/handover-review-nlhuln | de8b74e2fb94d1ec9b1982c15a2dea43421c3eee | outstanding-issues ledger capture after the mode-nav rollout (PR #1685) (supersedes 2026-08-07) | Confirmed #256's two remaining suspected section sets are dead (/forms/ and /differentials/presentations/ draw no section nav; form-decision-context-mobile is a testId not an id; ruled out sectionId indirection in both files). Added #271 (delete-or-keep the consumer-less action kind) and #272 (addon-slot single-owner rule held by two lists agreeing by coincidence) — renumbered from this PR's original #261/#262 because main claimed #261-#270 via PR #1678 design-system tracks in the interim. #207/#226/#231 reviewed and deliberately left untouched as existing P1 rows. | check:outstanding-issues passed (270 rows, 129 open, 141 archived, unique ids, next-id=273 above the highest, no ids deleted from base d32dd549a3dd); prettier --check clean on the changed file; rows written via scripts/outstanding-issues.mjs, never hand-edited; no code gates run - docs-only diff | -| 2026-08-07 | cursor/document-citation-landing-7bc3 | 82378a2bb4b875f1b610ef60c0ec3c94ee461f10 | document-viewer citation landing | ship: PDF-first citation landing; excerpt chip; indexed text collapsed until inspect/search; phone overview condensed; rail pin removed | unit 5539 pass; playwright critical citation+mobile PDF-first 2 pass; browser QA desktop/phone pass; typecheck; lint; build ALLOW_BUILD_WITH_DEV_SERVER=1; eval:rag:offline 36 golden; verify:pr-local stages green (first run flaked design-system-adoption timeout, retry green) | +| 2026-08-07 | cursor/site-testing-speed-08c1 | 91bac89827ae2f4f0e59aeed7de6344fe8779a95 | PR #1686 Autopilot+Bugbot review-and-fix: conflicts, threads, Static PR checks, CI/testing selection | fixed: merged origin/main (outstanding-issues #167/#255 archive + #256 keep); removed unused pathToFileURL; added ui-forms-section-nav to PR UI shards (21 specs); no unresolved threads; Bugbot unavailable (usage limit). Local: eslint file max-warnings0, vitest 36/36 focused, shard --validate OK, check:outstanding-issues OK. verify:cheap/pr-local blocked by foreign worktree heavy lock (PID 26228). | eslint scripts/playwright-pr-shards.mjs --max-warnings 0; vitest 36 passed; playwright-pr-shards --validate 21; check:outstanding-issues; verify:cheap/pr-local lock-blocked | | 2026-08-07 | claude/search-bar-mobile-layout-buu0io | 9d64388c0ce530d0c20bb7efe8ffb32cd928319c | phone results-filter idiom: 7 modes off MobileResultFilterControl onto ResultFilterTrigger + ResultFilterSheet; band, docs, tests | changes-shipped | typecheck; lint; test 5538 passed (1 pre-existing pr-handoff-stop failure, baselined on unmodified tree); build; check:rag:fixtures; check:bundle-budget +6.3% within tolerance; targeted Playwright: ui-accessibility 16, ui-specifiers+ui-formulation 12, ui-tools 5, ui-smoke 2, ui-stress 3 | -| 2026-08-08 | claude/differentials-inpage-navigation-h2u8fq (PR #1715) | c67c4927d8a088be267b8bd280a06f300aad60bc | differentials detail: adopt PR #1688 default in-page navigation template | Header rebuilt to the four template slots (back / title + active-section chevron sheet / ellipsis actions / weighted segment track); new detail-section-index.ts shapes the five tabs as DocumentSections reusing DocumentSectionTrack + DocumentSectionList; labelled strip gated to sm+; max-sm:static -> relative so the absolutely-positioned track keeps a positioned ancestor; tab panel renamed via aria-label since the sm-hidden strip cannot label it; sheets kept as siblings of PhoneHeaderCollapsePortal; no scroll spy (discrete panels). Adoption manifest regenerated - the dropped Tabs/ui-tools association was a coincidental capital-T comment match, not lost coverage. | typecheck exit 0; lint clean --max-warnings 0; verify:pr-local 5557 passed, stops only at pre-existing pr-handoff-stop (confirmed by stashed re-run on clean base); verify:phone-chrome lock-parity+runtime PASS, contracts 119 passed, changed-browser ui-tools passed, focused-browser 7 passed; new differential-section-nav.dom 9 passed; 40 passed re-run post-format; live browser 0 h-overflow at 320/390/768, collapse matches DocumentViewer (data-scroll-hidden=true, stack bottom 0) | -| 2026-08-07 | cursor/phone-mode-dense-production-05c0 (PR #1648) | 4e0cca2ccbc19ed676765b029642da0afea6215a | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/tools-search-mockups-72e1 (PR #1653) | a7fbc26a917b347d90c6bab1e6c1b2ede6422263 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/ship-first-redesign-mockups-2398 (PR #1654) | 9d9eb0be47073a7f051a5885359b82f2ff978a85 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/settings-features-mockups-97ac (PR #1657) | d64288ce0f2f111b1838ded31a23d0020b38da8b | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/clinician-workflow-mockups-2b63 (PR #1662) | 42ccad8ecc2889612e8f25ba79bb26a19e0a8baa | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | dependabot/npm_and_yarn/js-yaml-4.3.1 (PR #1668) | a7dde7e6101ed69fb43f004981be9500ba773105 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/privacy-live-signal-variants-bc81 (PR #1676) | 7dd4ea9b1ce17777f2d0ac6bd95bb916cd69758f | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 2, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | 89e25e97d442ebbbc7d33e87edec37ef42090486 | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 1, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | claude/handover-review-nlhuln (PR #1685) | e200e589938c8dac2d3f76ef42ed1639b3fde18e | Run PR sweep: CI fix + threads + drift | before: behind 1, checks green, 0 threads → after: merged origin/main (conflicts: none); PR required SUCCESS; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/site-testing-speed-08c1 (PR #1686) | 71b57ea5ce0c46c16c42c07e66933836ae609b6a | Run PR sweep: CI fix + threads + drift | before: DIRTY + Static PR fail (docs inventory stale) + PR required fail, merge-tree CLEAN → after: merged origin/main, docs:update inventory, pushed; CI re-running; 0 threads | docs:check-inventory fail→docs:update; format via Prettier; no provider-backed checks run | -| 2026-08-07 | cursor/viewer-phase2a-frame-controls-1db8 (PR #1687) | 5c10730be1641a386ee8c8476778933588a822fc | Run PR sweep: CI fix + threads + drift | before: up to date with main, Static PR fail (format:changed pdf-canvas-viewer), 1 outdated CodeRabbit thread (ref sync) already fixed on head → after: prettier format fix pushed; thread left open (no review-write API as cursor[bot]); CI re-running | format:changed fail→prettier --write pdf-canvas-viewer.tsx; format:changed PASS locally; no provider-backed checks run | -| 2026-08-07 | claude/search-bar-mobile-layout-buu0io (PR #1689) | a152ffd89c962e3589509c0c3740dc429264063a | Run PR sweep: CI fix + threads + drift | before: behind 1, required CI green (advisory lighthouse fail ignored), 1 CodeRabbit lighthouse baseline thread (human disagreement in progress) → after: waited for CI settle, disabled automerge, merged origin/main, pushed, re-enabled automerge; thread left open for human | settled CI then merge+push; no provider-backed checks run; advisory lighthouse not chased | -| 2026-08-07 | claude/issues-256-section-nav-clean (PR #1697) | 2b99db1f00c42fd7c11c4bf8acbad904ef5003e1 | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 3, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | -| 2026-08-07 | cursor/remove-specifiers-back-arrow-f1c4 | 095791235d58a6309b21b139911a1aad9fe9086b | specifiers-search-results-breadcrumb | removed lone ← Specifiers crumb from search results; deep pages unchanged | format,typecheck,lint,ensure+phone-spot-check | | 2026-08-07 | cursor/phone-mode-dense-production-05c0 (PR #1648) | 1091b17933beba655dac3e37f0e5c1bc4cdfb679 | Run PR sweep: CI fix + threads + drift | No action needed: PR required green, no unresolved review threads, not behind main. Only advisory Lighthouse job failing (never chased). | get_check_runs (PR required: success), get_review_comments (0 unresolved threads) | | 2026-08-07 | cursor/tools-search-mockups-72e1 (PR #1653) | 1df72ba6119226aee92b203db8188f58851a6d3c | Run PR sweep: CI fix + threads + drift | No action needed: PR required green, no unresolved review threads, not behind main. Only advisory Lighthouse job failing (never chased). | get_check_runs (PR required: success), get_review_comments (0 unresolved threads) | | 2026-08-07 | cursor/ship-first-redesign-mockups-2398 (PR #1654) | d6610b20a6b51bcfda266e1645fe695014c4b627 | Run PR sweep: CI fix + threads + drift | No action needed: PR required green, no unresolved review threads, not behind main. Only advisory Lighthouse job failing (never chased). | get_check_runs (PR required: success), get_review_comments (0 unresolved threads) | @@ -745,6 +731,22 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-07 | claude/ds-a4-component-defects (PR #1712) | c6b2fbfd5ac45fe7f13fd786c40afa200a702051 | design-system Track A4 - COMPONENTS section 0.4 open-defect rows (#263) | Authored and self-verified, not an independent review. Eleven defects closed across ten registered components (Button ref+testId, Progress shimmer token, StageList step clamp and scoped live region, StatusMark app-type decoupling, PageHeader title floor, Disclosure print, AccessibleTable dense-header title and Button expander, Tabs invalid-value tab order, Pagination clamp/wrap/focus/announce, Links download and gap, Checkbox/RadioGroup raw dimensions). Three listed sub-defects re-measured as already fixed and were not work. Button left the reference-only adoption snapshot because AccessibleTable genuinely adopts it. Ratchets fell and none rose: edge conflicts 28 to 27, legacy shadow aliases 229 to 228; debt baseline deliberately not lowered. Merged origin/main (10 commits) and hand-resolved the docs/outstanding-issues.md conflict: id #273 had been taken on main, so the LinkAction finding was renumbered to #276; proved no id lost from either side and no duplicates. Auto-merge deliberately NOT armed - four surfaces change visibly and issue #118 means zero visual baselines, so CI compares no pixels; owner visual review requested before merge. | check:design-system-contract exit 0 (all three sub-checks); tsc --noEmit exit 0; lint exit 0 at --max-warnings 0; prettier --check . clean; verify:ui 407 passed (14.1m) exit 0 zero not-ok; 260 unit tests across 8 component/token suites plus 191 across 7 further suites; all re-run green after the main merge (135 passed). verify:pr-local aggregate NOT run - vitest fork workers would not start under box load; components run individually. | | 2026-08-08 | claude/ds-a4-component-defects | a029a543f744eb80e608ec482aacdbdc5f5599c2 | unblock PR #1712 | Merged origin/main (ef28960e) to clear dirty mergeable_state: real conflict in docs/branch-review-ledger.md auto-merged via merge=ledger driver. Prior tip 9ba483d3 was 1 behind main. Static PR and PR required failures were dirty-state blockers (GitHub could not build refs/pull/1712/merge). Proved post-merge: merge-tree clean, check:branch-review-ledger, check:design-system-contract. | merge-tree clean; ledger:dedupe; check:branch-review-ledger; check:design-system-contract | | 2026-08-08 | claude/ds-a4-component-defects | d3a697aa8784c9cbdecfba24402cf2269bfe15d4 | heavy review-and-fix PR #1712 | Lint blocker fixed (react-hooks/refs in Pagination); CodeRabbit threads dispositioned; synced main (4a9d81d3 Lighthouse pin); merge-tree clean; verify:cheap 5567 passed; verify:pr-local green; check:design-system-contract passed | lint; typecheck; prettier --check .; verify:cheap (5567 passed); verify:pr-local; check:design-system-contract; check:branch-review-ledger; vitest ui-v2-components.dom (75 passed) | +| 2026-08-07 | cursor/document-citation-landing-7bc3 | 82378a2bb4b875f1b610ef60c0ec3c94ee461f10 | document-viewer citation landing | ship: PDF-first citation landing; excerpt chip; indexed text collapsed until inspect/search; phone overview condensed; rail pin removed | unit 5539 pass; playwright critical citation+mobile PDF-first 2 pass; browser QA desktop/phone pass; typecheck; lint; build ALLOW_BUILD_WITH_DEV_SERVER=1; eval:rag:offline 36 golden; verify:pr-local stages green (first run flaked design-system-adoption timeout, retry green) | +| 2026-08-08 | claude/differentials-inpage-navigation-h2u8fq (PR #1715) | c67c4927d8a088be267b8bd280a06f300aad60bc | differentials detail: adopt PR #1688 default in-page navigation template | Header rebuilt to the four template slots (back / title + active-section chevron sheet / ellipsis actions / weighted segment track); new detail-section-index.ts shapes the five tabs as DocumentSections reusing DocumentSectionTrack + DocumentSectionList; labelled strip gated to sm+; max-sm:static -> relative so the absolutely-positioned track keeps a positioned ancestor; tab panel renamed via aria-label since the sm-hidden strip cannot label it; sheets kept as siblings of PhoneHeaderCollapsePortal; no scroll spy (discrete panels). Adoption manifest regenerated - the dropped Tabs/ui-tools association was a coincidental capital-T comment match, not lost coverage. | typecheck exit 0; lint clean --max-warnings 0; verify:pr-local 5557 passed, stops only at pre-existing pr-handoff-stop (confirmed by stashed re-run on clean base); verify:phone-chrome lock-parity+runtime PASS, contracts 119 passed, changed-browser ui-tools passed, focused-browser 7 passed; new differential-section-nav.dom 9 passed; 40 passed re-run post-format; live browser 0 h-overflow at 320/390/768, collapse matches DocumentViewer (data-scroll-hidden=true, stack bottom 0) | +| 2026-08-07 | cursor/phone-mode-dense-production-05c0 (PR #1648) | 4e0cca2ccbc19ed676765b029642da0afea6215a | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/tools-search-mockups-72e1 (PR #1653) | a7fbc26a917b347d90c6bab1e6c1b2ede6422263 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/ship-first-redesign-mockups-2398 (PR #1654) | 9d9eb0be47073a7f051a5885359b82f2ff978a85 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/settings-features-mockups-97ac (PR #1657) | d64288ce0f2f111b1838ded31a23d0020b38da8b | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/clinician-workflow-mockups-2b63 (PR #1662) | 42ccad8ecc2889612e8f25ba79bb26a19e0a8baa | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | dependabot/npm_and_yarn/js-yaml-4.3.1 (PR #1668) | a7dde7e6101ed69fb43f004981be9500ba773105 | Run PR sweep: CI fix + threads + drift | before: behind 17, checks green, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/privacy-live-signal-variants-bc81 (PR #1676) | 7dd4ea9b1ce17777f2d0ac6bd95bb916cd69758f | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 2, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/pr-1676-unblock-ledger-ef51 (PR #1677) | 89e25e97d442ebbbc7d33e87edec37ef42090486 | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 1, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | claude/handover-review-nlhuln (PR #1685) | e200e589938c8dac2d3f76ef42ed1639b3fde18e | Run PR sweep: CI fix + threads + drift | before: behind 1, checks green, 0 threads → after: merged origin/main (conflicts: none); PR required SUCCESS; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/site-testing-speed-08c1 (PR #1686) | 71b57ea5ce0c46c16c42c07e66933836ae609b6a | Run PR sweep: CI fix + threads + drift | before: DIRTY + Static PR fail (docs inventory stale) + PR required fail, merge-tree CLEAN → after: merged origin/main, docs:update inventory, pushed; CI re-running; 0 threads | docs:check-inventory fail→docs:update; format via Prettier; no provider-backed checks run | +| 2026-08-07 | cursor/viewer-phase2a-frame-controls-1db8 (PR #1687) | 5c10730be1641a386ee8c8476778933588a822fc | Run PR sweep: CI fix + threads + drift | before: up to date with main, Static PR fail (format:changed pdf-canvas-viewer), 1 outdated CodeRabbit thread (ref sync) already fixed on head → after: prettier format fix pushed; thread left open (no review-write API as cursor[bot]); CI re-running | format:changed fail→prettier --write pdf-canvas-viewer.tsx; format:changed PASS locally; no provider-backed checks run | +| 2026-08-07 | claude/search-bar-mobile-layout-buu0io (PR #1689) | a152ffd89c962e3589509c0c3740dc429264063a | Run PR sweep: CI fix + threads + drift | before: behind 1, required CI green (advisory lighthouse fail ignored), 1 CodeRabbit lighthouse baseline thread (human disagreement in progress) → after: waited for CI settle, disabled automerge, merged origin/main, pushed, re-enabled automerge; thread left open for human | settled CI then merge+push; no provider-backed checks run; advisory lighthouse not chased | +| 2026-08-07 | claude/issues-256-section-nav-clean (PR #1697) | 2b99db1f00c42fd7c11c4bf8acbad904ef5003e1 | Run PR sweep: CI fix + threads + drift | before: DIRTY/PR mergeability fail, behind 3, merge-tree CLEAN, 0 threads → after: merged origin/main (conflicts: none), CI re-running; 0 threads | merge-tree clean; git merge origin/main + push; no provider-backed checks run | +| 2026-08-07 | cursor/remove-specifiers-back-arrow-f1c4 | 095791235d58a6309b21b139911a1aad9fe9086b | specifiers-search-results-breadcrumb | removed lone ← Specifiers crumb from search results; deep pages unchanged | format,typecheck,lint,ensure+phone-spot-check | | 2026-08-08 | cursor/run-pr-sweep-ledger-d56c (PR #1698) | ccf7284cbbe2315e5dc6a1bf126403a8a2205fa7 | Run PR sweep: CI fix + threads + drift | before: DIRTY/CONFLICTING, PR mergeability fail, behind 33, 0 threads, fake single-parent merge tip → after: real merge origin/main (conflicts resolved: docs/outstanding-issues.md + lighthouse-budget.json took main), merge-tree clean, unique diff ledger-only, 0 threads | check:outstanding-issues pass; check:branch-review-ledger pass; ledger:dedupe none; merge-tree clean; format; no provider-backed checks run | | 2026-08-08 | claude/differentials-inpage-navigation-h2u8fq (PR #1715) | f0b27ec857a70130d1616ddc1ccae1c9952c697b | heavy review-and-fix PR #1715 differentials in-page navigation | merge-blocker cleared (origin/main sync); no P0/P1 findings; phone-chrome + verify:pr-local green on f0b27ec8 | merge-tree clean post-sync; verify:phone-chrome lock-parity+runtime PASS, contracts 119 passed, changed-browser ui-tools passed, focused-browser 7 passed (19.4s); verify:pr-local Test Files 523 passed (523), Tests 5547 passed \| 4 skipped (5551); lint+typecheck+build+rag fixtures green; 0 unresolved review threads | | 2026-08-08 | claude/ds-doc-corrections | 534405600dca67317b4d60266cda03ec95f028e7 | M1 stranded doc corrections (docs/outstanding-issues.md #262/#266, docs/design-system/COMPONENTS.md TextField row + section 4) | authored and handed off as PR #1719; every inherited figure re-measured against origin/main rather than copied forward, and the stranded version's 'eight shadow tokens, focus 2' claim was found wrong — LEGACY_SHADOW_ALIAS matches seven tokens and has never included focus | check:outstanding-issues pass (274 rows, unique ids, no ids deleted from base); prettier --check . pass whole-tree; legacyShadowAliases re-measured 228 via the contract's own analyzers; docs-only diff so no unit/lint/typecheck/browser gate applies | @@ -752,24 +754,38 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | claude/ds-tap-and-linkaction | 6916c80526603514d91bd29d224959dd420af59c | M5 LinkAction tone refusal plus re-measured corrections to outstanding-issues #270, #118 and #269 — final reviewed head, adds the tone?: never fix, its type-contract test and both regenerated manifests | PR #1720, superseding the 824c1b74a record. Codex found the Omit form still accepted tone through a spread; verified with a focused tsc probe before changing anything (Omit accepted the spread with no diagnostic, tone?: never rejected it with TS2345), because excess-property checking only fires on object literals. Fixed with tone?: never plus a type-level contract test that stops compiling if the prop widens back. CodeRabbit's future-dated finding fixed in ff307cc5b. CodeRabbit's ledger-scope finding does not apply: that row records a different ref and head and was accurate as written, but a superseding row for the final #1719 head was appended anyway since its scope grew after the review pass | tsc -p tsconfig.typecheck.json --noEmit exit 0 zero diagnostics; lint exit 0; check:design-system-contract exit 0 (676 production files, legacy shadow aliases 228 confirming the #262 re-measure, adoption 53 components 55 roots, design-sync 53 components and 7 guidelines); check-icon-scale.mjs --strict exit 0; vitest threads pool 3 files 164 tests passed; check:outstanding-issues pass; check:branch-review-ledger pass; prettier --check . pass whole-tree; main merged in with merge-tree proven clean first and an id-set proof over both merge parents showing 274 ids each side, none lost, none invented | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 75c89993f3ea23b70a250f605b21437b4ea9aac8 | PR #1724 review-and-fix | fixed Codex P2 wrong #118 Lighthouse cause (150 overwrite vs 151 pin); dispositioned CodeRabbit #276 archive claim as false (issues:done move); merge-tree clean; required CI was green on prior tip 8ae8c48f; no Bugbot findings | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; no provider-backed checks | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 4baa9a1b42fa05731a6f983b3e0d0ebbd37f5271 | PR #1724 review-and-fix | synced origin/main (#1725 conflict on outstanding-issues resolved by preferring main queue then re-applying #276 done + corrected #118 diagnosis); Codex P2 fixed; CodeRabbit #276 archive claim dispositioned false; merge-tree clean after sync | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; merge-tree clean vs origin/main; no provider-backed checks | -| 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 3a0bdd62466080ad713873cdd690ae600635a979 | mode routing: one shared home page at /, mode pill retargets the composer, /documents + /medications mode homes | handoff — PR #1744 opened; 2 pre-existing failures verified at base bc33d41 | test:e2e:pr 406 passed/2 failed (both fail at base); vitest 5608 passed/1 failed (pre-existing); lint clean; tsc clean; sitemap:check, docs:check-index, docs:check-inventory, check:design-system-contract, check:outstanding-issues pass; verify:pr-local and verify:ui blocked by pre-existing installed-lock-parity (playwright 1.62.0 vs locked 1.62.1) | -| 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 468cc3fce85726a66098af0600d2b5d5951e3213 | bug-hunt | findings: P1 documents home autoRun on keystroke; P2 stale PWA /?mode=prescribing; P2 landing vs lastAppMode race; P2 /medications?q&run deep-link lost | vitest app-modes+search-route-ownership 36 pass; static ownership/ask-routing proof; no browser/UI/provider | +| 2026-08-08 | PR #1740 / claude/inpage-nav-info-pages-v8rhnd | b67f33f65e00529eb0dd1682d6925e708243ee93 | Extract InPageNavHeader (default in-page nav template) + convert differentials detail; PR 1 of 3 | HANDOFF. Template extracted from the duplicated DocumentViewer/differential-detail markup into src/components/in-page-nav/ (InPageNavHeader, PageSection/toDocumentSections, usePageSectionWeights); differential-detail-page converted (-207 lines), behaviour-neutral. section-index.ts untouched so document tests unaffected. DocumentViewer deliberately NOT converged (owns h1, edge-glass-header, visual baselines) - follow-up. Anchor-offset hook generalisation deferred to PR 2 where it is consumed. 3 source-scanning contracts + addon-slot guard updated to follow the markup and additionally assert adoption; addon-slot scan widened to InPageNavHeader or it would go silent for every future adopter. Single failing test (pr-handoff-stop) is a root-uid artifact: chmod 0555 does not block root, reproduced with work stashed on clean tree. | verify:cheap 5618 passed/1 failed (root artifact); verify:pr-local same, short-circuits at test so build not reached; build run separately - Compiled successfully in 53s + client bundle secret check passed; verify:phone-chrome EXIT=0 (stage1 119 passed, stage2 7 passed 23.5s, full UI policy auto not selected); lint/typecheck/prettier --check . clean. No provider-backed gates. Deps installed with engine check relaxed (user-approved; Node 24.13.0 vs jsdom floor 24.15) - lockfile untouched. | | 2026-08-08 | claude/document-image-mobile-view-30xzw8 | 2394d903a6ca1ba7a84e380c9ed5cada038fa5c0 | document-viewer phone image layout + lightbox geometry (PR #1737) | implemented: capped rail/body grid tracks, removed aspect-ratio min-height transfer, rebuilt phone image viewer (legible open scale, rotation re-fit, clamped pan, double-tap, footer controls) | lint, typecheck, test (5647 pass / 1 pre-existing fail), build, eval:rag:offline, check:bundle-budget, all verify:pr-local static steps by hand; browser gates blocked by #255 | | 2026-08-08 | claude/document-image-mobile-view-30xzw8 | d257df7e11913db1d367535171fac726f47e7f1c | PR #1737 document-viewer phone image review-and-fix | fixed P1 expand fixture/threshold + P2 double-tap stage coords/pointer-up + resize re-clamp; Production UI timeout root cause cleared; merge-tree clean | verify:pr-local PASS (525 files/5653 tests); lint; typecheck; focused vitest 64/64; Production UI delegated to CI | -| 2026-08-08 | PR #1740 / claude/inpage-nav-info-pages-v8rhnd | b67f33f65e00529eb0dd1682d6925e708243ee93 | Extract InPageNavHeader (default in-page nav template) + convert differentials detail; PR 1 of 3 | HANDOFF. Template extracted from the duplicated DocumentViewer/differential-detail markup into src/components/in-page-nav/ (InPageNavHeader, PageSection/toDocumentSections, usePageSectionWeights); differential-detail-page converted (-207 lines), behaviour-neutral. section-index.ts untouched so document tests unaffected. DocumentViewer deliberately NOT converged (owns h1, edge-glass-header, visual baselines) - follow-up. Anchor-offset hook generalisation deferred to PR 2 where it is consumed. 3 source-scanning contracts + addon-slot guard updated to follow the markup and additionally assert adoption; addon-slot scan widened to InPageNavHeader or it would go silent for every future adopter. Single failing test (pr-handoff-stop) is a root-uid artifact: chmod 0555 does not block root, reproduced with work stashed on clean tree. | verify:cheap 5618 passed/1 failed (root artifact); verify:pr-local same, short-circuits at test so build not reached; build run separately - Compiled successfully in 53s + client bundle secret check passed; verify:phone-chrome EXIT=0 (stage1 119 passed, stage2 7 passed 23.5s, full UI policy auto not selected); lint/typecheck/prettier --check . clean. No provider-backed gates. Deps installed with engine check relaxed (user-approved; Node 24.13.0 vs jsdom floor 24.15) - lockfile untouched. | +| 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 1ebc84bb288b516bb322c09cde2889e981d302a4 | AGENTS.md reasoning-effort calibration section (docs-only) | Authored and handed off as PR #1730; docs-only, pr-policy classifier returns clinicalRisk/operationalRisk/ragRanking false | prettier --check . (repo-wide, pass); docs:check-links (1665 refs resolve, pass); pr-policy classifyPullRequestFiles(AGENTS.md) | +| 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 2b0ad7d41d841c13515f10de7c41e449470dfa78 | pr-1730 review-and-fix | Deep review + Bugbot: no P0/P1; fixed 2 scoped P2 clarity risks (version-bump under-planning; live-state vs provider boundary). Residual: OPENAI_*_REASONING_EFFORT vocab overlap. Merge-tree clean; required CI was green pre-push. | prettier --check AGENTS.md; docs:check-links (1667); verify:pr-local (docs route pass); verify:cheap (524 files / 5607 tests pass); pr-policy classify clinical/operational/rag false; Bugbot no P0-P2 | +| 2026-08-08 | cursor/safety-plan-phone-safe-area-624a (PR #1711) | ad1b1f5db24ed68ee4c0d5963620e4562829884e | heavy review-and-fix PR #1711 | fixed CodeRabbit sm:py guard parity; late-synced #1720 behind-but-clean; no P0/P1; Bugbot none; threads cleared; merge-tree clean; required CI green on 78c14205 pre-sync | vitest safety-plan+standalone 18p; verify:cheap 523/5582; verify:pr-local format+lint+typecheck+test+build+rag-fixtures; Production UI critical+(1)(2)(3)+PR required SUCCESS on 78c14205; no provider gates | | 2026-08-08 | claude/document-viewer-optimization-tu8tnj | 98b799a372b1e341c86e8807d5cf37e987413e49 | document viewer phone/PWA rework: CSP-blocked native reader removed, one toolbar, fit-mode pinch, canvas pixel budget, source-first phone order, in-window detail-refetch guard, pdf.js on-demand fetch + teardown, image/signed-URL wins | ship: PR #1741 | lint, typecheck, test 5625 pass (1 pre-existing root-container failure), build, check:rag:fixtures, check:bundle-budget 1499.8 KiB vs base 1500.0 KiB, check:runtime, check:installed-lock-parity, format:changed; verify:ui not run (container Chromium 141 cannot raster pdfjs 6, see #278) | | 2026-08-08 | claude/document-viewer-optimization-tu8tnj | 2359e158cb7bca5954e9c5ee84ca0766964ad901 | PR #1741 document-viewer phone/PWA review-and-fix | supersede: fixed Production UI phone Zoom/section-trigger; handlePdfLoadSuccess clamp; prior P1/P2 fixes retained; merge-tree clean | prior verify:cheap+pr-local green; ui-smoke selectors fixed for overflow Zoom + revealPhoneHeaderControl; no provider gates | +| 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 3a0bdd62466080ad713873cdd690ae600635a979 | mode routing: one shared home page at /, mode pill retargets the composer, /documents + /medications mode homes | handoff — PR #1744 opened; 2 pre-existing failures verified at base bc33d41 | test:e2e:pr 406 passed/2 failed (both fail at base); vitest 5608 passed/1 failed (pre-existing); lint clean; tsc clean; sitemap:check, docs:check-index, docs:check-inventory, check:design-system-contract, check:outstanding-issues pass; verify:pr-local and verify:ui blocked by pre-existing installed-lock-parity (playwright 1.62.0 vs locked 1.62.1) | +| 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 468cc3fce85726a66098af0600d2b5d5951e3213 | bug-hunt | findings: P1 documents home autoRun on keystroke; P2 stale PWA /?mode=prescribing; P2 landing vs lastAppMode race; P2 /medications?q&run deep-link lost | vitest app-modes+search-route-ownership 36 pass; static ownership/ask-routing proof; no browser/UI/provider | | 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 6d1099b479358caa05c92f236848117feb920d4e | shared-home mode-routed search navigation | no high-confidence P0-P2 PR-introduced defects; prior bug-hunt P1/P2s appear fixed on tip; residual: prescribing submit-from-shared-home URL omits run=1 (pre-existing path), seed effect untested behaviourally, no browser/UI proof this pass | vitest app-modes+search-route-ownership+audit-navigation+pwa-manifest 61 pass; static read of focus files vs origin/main; ledger:lookup NOT REVIEWED; no provider/UI | -| 2026-08-08 | cursor/safety-plan-phone-safe-area-624a (PR #1711) | ad1b1f5db24ed68ee4c0d5963620e4562829884e | heavy review-and-fix PR #1711 | fixed CodeRabbit sm:py guard parity; late-synced #1720 behind-but-clean; no P0/P1; Bugbot none; threads cleared; merge-tree clean; required CI green on 78c14205 pre-sync | vitest safety-plan+standalone 18p; verify:cheap 523/5582; verify:pr-local format+lint+typecheck+test+build+rag-fixtures; Production UI critical+(1)(2)(3)+PR required SUCCESS on 78c14205; no provider gates | -| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | 63be5e932dc0410f172375edf779190c6a1aadae | differentials Safety Snapshot mobile density redesign | ship; phone visual PASS at ~400px (compact labels, equal 3-col metrics, no redundant summary); unit 21/21; verify:pr-local tests+fixtures+format PASS; build PASS with ALLOW_BUILD_WITH_DEV_SERVER=1 | test:differential-detail,verify:pr-local(partial-build-retry),phone-visual | +| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 | bed84986742ca85b3724dd3ccc0758d4b9649934 | differentials diagnoses query-lit stream | implemented query-lit Diagnoses stream with match jump, related clusters, compare select, browse chapters; PR #1757 | unit:pass;lint:pass;typecheck:pass;verify:ui:not-run | +| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 | 106124d8084a1eab2eddab828076d10f96d3cedc | differentials diagnoses query-lit stream | implemented query-lit Diagnoses stream with match jump, related clusters, compare select, browse chapters; PR #1757 | unit:pass;lint:pass;typecheck:pass;verify:ui:not-run | +| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 (PR #1757) | e7529dd2dd847b0bfd4b53daa723a8f5a329a50e | heavy review-and-fix | synced main; fixed P1 compare id drop + P2 mobile threshold + P2 query normalize; 3 threads need reply (API 403) | vitest differential-stream+differentials-navigation+differentials 42 passed; no provider-backed checks | +| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 (PR #1757) | d76c4cc8b8633a65df66ea46b090c2864a2c1592 | heavy review-and-fix | CI fix on tip: type-scale text-3xs; presentations redirect lowercases+drops unknown while preserving valid cross-workflow ids; prior P1/P2 fixes retained | check:type-scale; vitest audit-nav+differentials-nav+stream; no provider | | 2026-08-08 | dependabot/npm_and_yarn/js-yaml-4.3.1 | 072b83f79a70037a04a8412844c041db43c9ce48 | PR #1668 unblock | synced main; merge-tree clean; required CI was green on prior tip e9516021; js-yaml 4.3.1 + nanoid 3.3.18 preserved; no unresolved threads; CI re-run after sync | pre-sync PR required pass; Production UI skipped (deps); post-sync pending | -| 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 1ebc84bb288b516bb322c09cde2889e981d302a4 | AGENTS.md reasoning-effort calibration section (docs-only) | Authored and handed off as PR #1730; docs-only, pr-policy classifier returns clinicalRisk/operationalRisk/ragRanking false | prettier --check . (repo-wide, pass); docs:check-links (1665 refs resolve, pass); pr-policy classifyPullRequestFiles(AGENTS.md) | -| 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 2b0ad7d41d841c13515f10de7c41e449470dfa78 | pr-1730 review-and-fix | Deep review + Bugbot: no P0/P1; fixed 2 scoped P2 clarity risks (version-bump under-planning; live-state vs provider boundary). Residual: OPENAI_*_REASONING_EFFORT vocab overlap. Merge-tree clean; required CI was green pre-push. | prettier --check AGENTS.md; docs:check-links (1667); verify:pr-local (docs route pass); verify:cheap (524 files / 5607 tests pass); pr-policy classify clinical/operational/rag false; Bugbot no P0-P2 | | 2026-08-08 | dependabot/npm_and_yarn/js-yaml-4.3.1 | a79943df33e653d2a65d4db2f192ee77c22ab75a | PR #1668 unblock | late-synced main after CI green on f04a96c3; merge-tree clean (GitHub DIRTY was stale); js-yaml 4.3.1 + nanoid 3.3.18 preserved; no unresolved threads; CI re-run after push | pre-late-sync: PR required pass on f04a96c3; Production UI skipped; post-sync pending | +| 2026-08-08 | cursor/fix-differentials-compare-5c66 | fd4801b07309625780b06afef718f93799655885 | differentials-compare-selection-handoff | fixed: preserve cross-presentation compare ids via ad-hoc /differentials/compare; URL ids sync; ModeNav Compare wired | verify:pr-local:5709 passed; test:focused:255 passed | +| 2026-08-08 | cursor/fix-differentials-compare-5c66 (PR #1756) | c739708981083b816843ceec5e50ea00818996b5 | heavy review-and-fix | merged origin/main (docs/adoption/sitemap regenerated); fixed P1 cold-load URL wipe + P2 unsupported criterion + lowercase ids; CodeRabbit empty-state/auto-seed left as intentional ModeNav handoff; threads unreplied (403) | vitest differentials+navigation+compare-selection DOM 37 pass; related nav tests 44 pass; eslint touched files; no provider-backed checks | +| 2026-08-08 | cursor/fix-differentials-compare-5c66 (PR #1756) | bd62d3a23b888d30112fdc11e86fe1811f1919bc | heavy review-and-fix | merged origin/main (docs/adoption/sitemap regenerated); fixed P1 cold-load URL wipe (state-captured ids + defer sync while loading) + P2 unsupported criterion + lowercase ids; CodeRabbit empty-state/auto-seed left as intentional ModeNav handoff; threads unreplied (403) | vitest differentials+navigation+compare-selection DOM 37 pass; related nav tests 44 pass; eslint touched files clean; no provider-backed checks | | 2026-08-08 | cursor/safety-plan-copy-timer-a650 | cf57b34a36b768e150cd776f7e19acfd984245f7 | PR #1717 unblock | fixed missing it() closer from Copilot autofix; merged origin/main after #1668; merge-tree clean; no unresolved threads | local: vitest patient-safety-plan.dom.test.tsx (8/8); format ok; pending hosted CI after push | | 2026-08-08 | cursor/safety-plan-copy-timer-a650 | 3142eb9a93275ce2c2435523560b4ed6624d8f53 | PR #1717 unblock | fixed parse + no-explicit-any from Copilot autofix; merged origin/main after #1668; merge-tree clean; 0 threads | local: vitest 8/8; eslint file clean; format ok; pending hosted CI | -| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | d7fcbc2095ae3cfbefabfad1333b85de0ca42a4b | differentials Safety Snapshot mobile density redesign | ship; tightened Watch-for wrap; phone 390 screenshot + unit 21/21 | test:differential-detail,phone-visual-390 | -| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | ef9a5ed85a3e16dd38bca39d5a6a7e0056c7d42d | differentials Safety Snapshot mobile density redesign | ship; final tip after Watch-for tighten + ledger supersede; phone 390 PASS; unit 21/21 | test:differential-detail,phone-visual-390,build | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | f5dd1dea495e8d6e9bd5106dcf0a4d062ee02292 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | 8f25e6c482d8e4cd879098d7cfd73b7f8603e478 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 (PR #1735) | 9e1390d73ebbae0bbfc0f81bf3b3921dadf24577 | heavy review-and-fix | CONFLICT merge-tree on docs/design-system/adoption-manifest.json resolved by regenerating (DisclosureGroup form-detail import + main documents/medications routes); product forms DisclosureGroup intent preserved; 0 unresolved threads; no ambiguous clinical/auth conflicts | check:design-system-adoption PASS (53 components, 57 roots); vitest forms-information-disclosure.dom 2/2 PASS; no provider-backed checks | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 32474bcd20d5fa39097a3f75b85d3af81b404320 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish (supersedes 2026-08-08) | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 96c4d3a3a1a46efedfa5b43c4bf1de227c1d19a6 | PR #1734 confirm checklist | clean; no P0/P1/P2 in ConfirmCalloutText/confirmCheckParts/Avoid row | diff vs main; form-1a catalog wiring; vitest form-confirm-callout.dom.test.tsx PASS | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 89cc8711dd0536c32818cbbd493edff860763a61 | PR #1734 unblock | synced origin/main (behind-but-clean DIRTY; merge-tree clean); no product conflict; advisory lighthouse ignored | merge-tree clean vs origin/main; ledger:dedupe none | +| 2026-08-08 | cursor/compact-services-result-text-9b7d (PR #1731) | f07828041199458bd756090d04fb5105f41e3ca4 | PR #1731 unblock | before: MERGEABLE/BEHIND(1) merge-tree CLEAN tip 14fd8aa9; required CI green (PR required + Production UI 1/2/3 + critical); 0 threads; autoMerge SQUASH armed. after: late-synced origin/main (aa6cf68c from #1668/#1717) via worktree merge (update-branch 403); merge-tree clean; 0 behind; CI will re-run on sync tip; autoMerge left armed; no product code change | gh pr checks --watch: PR required SUCCESS; Production UI (1)(2)(3)+critical SUCCESS; merge-tree clean; ledger:dedupe none; no provider gates | +| 2026-08-08 | cursor/services-content-cleanup-1c73 | 1b62fdabbabcfbb05ccbbae08b070bf7740426d5 | services content cleanup: compact catalogue fields + hide empty detail sections | APPROVE pending required CI; verify:pr-local passed; UI spot-check recommended | verify:pr-local (lint/typecheck/test/build/rag-fixtures) | | 2026-08-08 | claude/ds-visual-advisory-off-prs (PR #1755) | e6d24190eb1e02c435003d429d88f2d293b14867 | heavy review-and-fix | merged origin/main; fixed Bugbot/Codex P2 merge_group exclusion via event allowlist; synced docs/testing.md + #118 note; CodeRabbit date nit dispositioned (owner +0800); threads unreplied (403) | vitest ci-cache-safety; check:github-actions; no provider-backed checks | | 2026-08-08 | claude/document-viewer-optimization-tu8tnj | 9a5f79ab133c6ab9ea2a47e93b0101df8db44607 | docs-only: one outstanding-issues row (#285) recording the lowercase authorizationHeader trap surfaced by PR #1741 review | ship: PR #1754 | check:outstanding-issues (283 rows, unique ids, next-id above highest), prettier --check on the changed file; no source touched so lint/typecheck/test/build have no changed failure path | | 2026-08-08 | claude/document-viewer-optimization-tu8tnj (PR #1754) | 41b4bccf7d7229920c33344bec0d46e0f2e48b97 | heavy review-and-fix | CONFLICT merge-tree on docs/outstanding-issues.md resolved: kept main #285 (Node/jsdom floor) + renumbered authorizationHeader trap to #286, next-id=287; merged origin/main; 1 unresolved review thread (comments 403 — skipped); no product code change | check:outstanding-issues PASS (284 rows, next-id=287); prettier --check docs/outstanding-issues.md PASS; ledger:dedupe none; no provider-backed checks | @@ -795,27 +811,13 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | cursor/site-testing-speed-08c1 (PR #1686) | 11e35727a6a9b3b776e890e6a63e43fea9b0a437 | Run PR sweep: CI fix + threads + drift | post-merge Unit coverage: added ui-phone-scroll-document-rail.spec.ts to playwright PR shard 1; CI re-running | vitest playwright-pr-shards 4 passed; playwright-pr-shards --validate OK; no provider-backed checks | | 2026-08-08 | origin/main (PR #1722 follow-up) | eda8fe872de040e304621bce49535e1dfebb091e | Lighthouse testing thorough review | P1 fixed locally: stale Chrome 150 baseline, unbounded runner phases, and live input/process limits hardened; advisory policy retained | offline review; check:ci-scope PASS; test:ci-workflows 13 files 248 passed 11 skipped; check:github-actions PASS; verify:lighthouse dry-run PASS; no provider-backed checks run | | 2026-08-08 | codex/lighthouse-hardening (PR #1746) | 038058ea63837e7c4a90e84b7f8f860aacc51e13 | heavy review-and-fix | CONFLICT merge-tree on lighthouse-budget.json resolved: kept main baseline measurements + this PR Lighthouse hardening (scripts/workflows/ci-change-scope); visual-baseline policy matches main; 3 unresolved threads (comments 403 — skipped) | ci-change-scope --self-test PASS; vitest check-lighthouse-budget+ci-cache-safety 84/84 PASS; no provider-backed checks | -| 2026-08-08 | cursor/forms-info-disclosure-68d6 | f5dd1dea495e8d6e9bd5106dcf0a4d062ee02292 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | -| 2026-08-08 | cursor/forms-info-disclosure-68d6 | 8f25e6c482d8e4cd879098d7cfd73b7f8603e478 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | -| 2026-08-08 | cursor/forms-info-disclosure-68d6 (PR #1735) | 9e1390d73ebbae0bbfc0f81bf3b3921dadf24577 | heavy review-and-fix | CONFLICT merge-tree on docs/design-system/adoption-manifest.json resolved by regenerating (DisclosureGroup form-detail import + main documents/medications routes); product forms DisclosureGroup intent preserved; 0 unresolved threads; no ambiguous clinical/auth conflicts | check:design-system-adoption PASS (53 components, 57 roots); vitest forms-information-disclosure.dom 2/2 PASS; no provider-backed checks | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 32474bcd20d5fa39097a3f75b85d3af81b404320 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish (supersedes 2026-08-08) | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 96c4d3a3a1a46efedfa5b43c4bf1de227c1d19a6 | PR #1734 confirm checklist | clean; no P0/P1/P2 in ConfirmCalloutText/confirmCheckParts/Avoid row | diff vs main; form-1a catalog wiring; vitest form-confirm-callout.dom.test.tsx PASS | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 89cc8711dd0536c32818cbbd493edff860763a61 | PR #1734 unblock | synced origin/main (behind-but-clean DIRTY; merge-tree clean); no product conflict; advisory lighthouse ignored | merge-tree clean vs origin/main; ledger:dedupe none | -| 2026-08-08 | cursor/compact-services-result-text-9b7d (PR #1731) | f07828041199458bd756090d04fb5105f41e3ca4 | PR #1731 unblock | before: MERGEABLE/BEHIND(1) merge-tree CLEAN tip 14fd8aa9; required CI green (PR required + Production UI 1/2/3 + critical); 0 threads; autoMerge SQUASH armed. after: late-synced origin/main (aa6cf68c from #1668/#1717) via worktree merge (update-branch 403); merge-tree clean; 0 behind; CI will re-run on sync tip; autoMerge left armed; no product code change | gh pr checks --watch: PR required SUCCESS; Production UI (1)(2)(3)+critical SUCCESS; merge-tree clean; ledger:dedupe none; no provider gates | -| 2026-08-08 | cursor/services-content-cleanup-1c73 | 1b62fdabbabcfbb05ccbbae08b070bf7740426d5 | services content cleanup: compact catalogue fields + hide empty detail sections | APPROVE pending required CI; verify:pr-local passed; UI spot-check recommended | verify:pr-local (lint/typecheck/test/build/rag-fixtures) | | 2026-08-08 | cursor/presentations-catalogue-tab-fb39 | 3872ea0854da2ce4e3b99ec182bb94a4cb807958 | differentials presentations catalogue ModeNav tab | shipped Presentations catalogue at /differentials/presentations; Compare entry moved to /differentials/compare; verify:pr-local passed; UI smoke confirmed 4 tabs | verify:pr-local; vitest design-system-adoption; curl presentations+compare; browser ModeNav QA | | 2026-08-08 | cursor/presentations-catalogue-tab-fb39 | 59dceae612315e95a1114a215d2d8319e439880d | differentials presentations catalogue ModeNav tab | shipped Presentations catalogue at /differentials/presentations; Compare entry moved to /differentials/compare; verify:pr-local passed; UI smoke confirmed 4 tabs | verify:pr-local; vitest design-system-adoption; curl presentations+compare; browser ModeNav QA | | 2026-08-08 | cursor/forms-results-bar-documents-style-13dc | fd148ca931ab9c023619deed98bae5af787e4253 | sync | PR #1751 unblock: CONFLICTING→MERGEABLE; 11 behind main; merge-tree clean; merge commit fd148ca9 pushed; all static+unit+build+critical CI green; Production UI (1/2/3) pending | merge-tree:clean,static-pr:pass,unit-coverage:pass,build:pass,production-ui-critical:pass,safety:pass,pr-policy:pass,production-ui-1/2/3:pending | | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | e965d96258cdd3d5ad6f520616de0f32bf02498e | Form 1A priority facts: condense cards + Act section detail sheets | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | 7040b850e655dc7ba9a23ad3e3db765cc1ad7755 | Form 1A priority facts: condense cards + Act section detail sheets | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | e965d96258cdd3d5ad6f520616de0f32bf02498e | Form 1A priority facts: condense cards + Act section detail sheets (supersedes 2026-08-08) | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | -| 2026-08-08 | cursor/fix-differentials-compare-5c66 | fd4801b07309625780b06afef718f93799655885 | differentials-compare-selection-handoff | fixed: preserve cross-presentation compare ids via ad-hoc /differentials/compare; URL ids sync; ModeNav Compare wired | verify:pr-local:5709 passed; test:focused:255 passed | -| 2026-08-08 | cursor/fix-differentials-compare-5c66 (PR #1756) | c739708981083b816843ceec5e50ea00818996b5 | heavy review-and-fix | merged origin/main (docs/adoption/sitemap regenerated); fixed P1 cold-load URL wipe + P2 unsupported criterion + lowercase ids; CodeRabbit empty-state/auto-seed left as intentional ModeNav handoff; threads unreplied (403) | vitest differentials+navigation+compare-selection DOM 37 pass; related nav tests 44 pass; eslint touched files; no provider-backed checks | -| 2026-08-08 | cursor/fix-differentials-compare-5c66 (PR #1756) | bd62d3a23b888d30112fdc11e86fe1811f1919bc | heavy review-and-fix | merged origin/main (docs/adoption/sitemap regenerated); fixed P1 cold-load URL wipe (state-captured ids + defer sync while loading) + P2 unsupported criterion + lowercase ids; CodeRabbit empty-state/auto-seed left as intentional ModeNav handoff; threads unreplied (403) | vitest differentials+navigation+compare-selection DOM 37 pass; related nav tests 44 pass; eslint touched files clean; no provider-backed checks | | 2026-08-08 | claude/mode-routing-search-pages-jabe17 (PR #1760) | ce880f23f7ae5cbf9bc28a8a87f1de6585a4343e | home-mode seed navigation context, its contract test, and three outstanding-issues rows | handoff: PR #1760 opened; carries focus/scope context through the cold-/ replaceState, repoints the contract test at behaviour, records #285/#286 and a third #255 reproduction | lint clean; tsc --noEmit clean; unit 5710 passed/1 pre-existing pr-handoff-stop failure (#286); maintainability budgets passed; check:outstanding-issues 284 rows unique; format committed; UI delegated to CI (mismatched Chromium, #255) | -| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 | bed84986742ca85b3724dd3ccc0758d4b9649934 | differentials diagnoses query-lit stream | implemented query-lit Diagnoses stream with match jump, related clusters, compare select, browse chapters; PR #1757 | unit:pass;lint:pass;typecheck:pass;verify:ui:not-run | -| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 | 106124d8084a1eab2eddab828076d10f96d3cedc | differentials diagnoses query-lit stream | implemented query-lit Diagnoses stream with match jump, related clusters, compare select, browse chapters; PR #1757 | unit:pass;lint:pass;typecheck:pass;verify:ui:not-run | -| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 (PR #1757) | e7529dd2dd847b0bfd4b53daa723a8f5a329a50e | heavy review-and-fix | synced main; fixed P1 compare id drop + P2 mobile threshold + P2 query normalize; 3 threads need reply (API 403) | vitest differential-stream+differentials-navigation+differentials 42 passed; no provider-backed checks | -| 2026-08-08 | cursor/differentials-query-lit-stream-8bc0 (PR #1757) | d76c4cc8b8633a65df66ea46b090c2864a2c1592 | heavy review-and-fix | CI fix on tip: type-scale text-3xs; presentations redirect lowercases+drops unknown while preserving valid cross-workflow ids; prior P1/P2 fixes retained | check:type-scale; vitest audit-nav+differentials-nav+stream; no provider | +| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | 63be5e932dc0410f172375edf779190c6a1aadae | differentials Safety Snapshot mobile density redesign | ship; phone visual PASS at ~400px (compact labels, equal 3-col metrics, no redundant summary); unit 21/21; verify:pr-local tests+fixtures+format PASS; build PASS with ALLOW_BUILD_WITH_DEV_SERVER=1 | test:differential-detail,verify:pr-local(partial-build-retry),phone-visual | +| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | d7fcbc2095ae3cfbefabfad1333b85de0ca42a4b | differentials Safety Snapshot mobile density redesign | ship; tightened Watch-for wrap; phone 390 screenshot + unit 21/21 | test:differential-detail,phone-visual-390 | +| 2026-08-08 | cursor/safety-snapshot-mobile-4ab3 | ef9a5ed85a3e16dd38bca39d5a6a7e0056c7d42d | differentials Safety Snapshot mobile density redesign | ship; final tip after Watch-for tighten + ledger supersede; phone 390 PASS; unit 21/21 | test:differential-detail,phone-visual-390,build | diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index c8af4b9269..a754e6da93 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -56,6 +56,21 @@ unversioned installer. Set `CODEX_CLOUD_SKIP_BROWSER_INSTALL=1` only for an explicitly source-only environment; that environment is not full browser-ready. +### Playwright browser readiness (#255) + +Browser gates need the **locked** Playwright package and a Chromium revision that matches +`node_modules/playwright-core/browsers.json`. When a remote/Cloud image forces +`PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` and that +tree only ships an older revision (for example 1194 while the lock expects 1234): + +1. Run `npm run check:installed-lock-parity` and `npm run check:playwright-browser-revision`. +2. Do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at the mismatched shell. +3. Delegate browser proof to CI Production UI, or refresh the environment image / install matching + browsers into a managed cache, or unset the container browser env vars so + `npx playwright install` can populate the managed path. + +`CODEX_CLOUD_SKIP_BROWSER_INSTALL=1` remains source-only: that environment is not browser-ready. + ## Access profiles ### Offline (default) diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 4f71bdc1cc..63c6c6a63d 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -2,6 +2,13 @@ This document turns the current process review into phased, durable repo practice. It separates changes that already take effect from work that should stay explicit until it is implemented. +## Testing speed playbook (pointer) + +Day-to-day selection, local Playwright keep-root, and refuted speed levers live in +[`docs/testing.md`](testing.md) § "Testing speed playbook". Measured CI shape and the +Production UI critical-path numbers remain in "CI shape and cost" below; do not raise +Playwright workers or revive the refuted cache/shard hacks listed there. + ## Risk-routed local and CI verification (2026-08-02) - `ci-change-scope.mjs` is the shared fail-closed classifier for local PR handoff and hosted CI. Only recognised documentation and workflow/policy paths take a light route; product code, tests, executable config, dependencies, database/container/RAG surfaces, mixed scope, and unknown non-document paths retain heavy verification. @@ -201,12 +208,10 @@ API rather than estimated: ``` Shard 1 is slow because it holds ten _slow-per-test_ files while shards 2 and 3 are dominated - by one fast-per-test file each. **The only lever that would actually rebalance is assigning - explicit spec groups per shard in `ci.yml` instead of `--shard=i/N`.** That is not obviously - worth it: perfect balance is ~7m37 against a measured 9m36 largest shard, so ~2 min of a - 13m39 critical path, bought with a hand-maintained file list in the **required** UI job whose - miss mode is a spec silently running nowhere. Do not build it without deciding that trade - first, and not without a guard asserting every collected spec appears in exactly one group. + by one fast-per-test file each. **Shipped:** explicit duration-aware file groups in + `scripts/playwright-pr-shards.mjs` (CI `test:e2e:pr:shard -- --shard N`) with + `tests/playwright-pr-shards.test.ts` asserting every production e2e:pr spec is in exactly one + non-empty group. Re-measure wall time after suite growth before reshuffling membership. **Stop:** do not "fix" this by renaming spec files so they sort into different shards. It works, and it encodes undocumented scheduler behaviour into filenames where the next reader diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 85d9b9f575..75f50d7cb0 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (222 files) and the `package.json` script surface (232 entries), +Curated map of `scripts/` (224 files) and the `package.json` script surface (235 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/docs/testing.md b/docs/testing.md index a6b42757d2..73a75b57fe 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -20,6 +20,34 @@ Start with the cheapest check that can fail for the changed behavior. Add anothe `npm run verify:pr-local -- --dry-run --files ` shows the local plan. Recognised documentation and workflow/policy-only scopes stay focused. Product code, tests, executable configuration, dependencies, database/container surfaces, mixed scope, and unknown non-document paths fail closed to the heavy plan. Provider, physical-device, and release-only acceptance remain separate and require their normal approval or task context. +## Testing speed playbook + +Production Chromium is ~85% of UI-scoped PR wall clock; cancelled mid-UI runs waste more than missing parallelism. Prefer selection and build reuse over raising Playwright workers (`workers: 1`, `fullyParallel: false`, `retries: 0` stay required — see `#093` and [process-hardening.md](process-hardening.md)). + +| Change type | Run this | Avoid | +| ------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------- | +| Lib/helper, no UI | `npm run test:focused -- --files ` or one Vitest file | `verify:ui` | +| Component interaction | `.dom.test.tsx` + focused Vitest | full Chromium | +| Phone chrome / scroll / composer | `npm run verify:phone-chrome -- --dry-run`, then without dry-run | immediate `verify:ui` | +| Shared shell / header / `globals.css` | phone-chrome, then `verify:ui` once at handoff | stacking `verify:cheap` + `verify:pr-local` + `verify:ui` | +| Docs / ledger only | `npm run verify:pr-local -- --dry-run` (confirm docs route) | full unit + UI | +| PR ready | `npm run format` (commit it) + `verify:pr-local` once | mid-CI pushes that cancel Production UI | + +**Local Playwright keep-root (iterative UI work).** Each `run-playwright.mjs` invocation otherwise builds a unique `.next-playwright//` and deletes it. For a focused session, reuse one root the same way CI does: + +```bash +export PLAYWRIGHT_BUILD_ROOT_ID=local-ui +export PLAYWRIGHT_KEEP_BUILD_ROOT=true +# then: focused greps / verify:phone-chrome / single-spec runs +# rebuild after product source changes; unset both vars (or delete `.next-playwright/local-ui`) when done +``` + +`verify:phone-chrome` sets a session keep-root automatically when it runs two or more browser stages, then cleans that root on exit. + +**Refuted levers (do not revive):** persistent Actions cache for the Next webpack tree (~804 MB, evicts browser cache); splitting `ui-phone-scroll*` to rebalance `--shard` (siblings still co-land); renaming specs to game alphabetical shard order; Playwright `workers > 1` or blocking retries; dropping Production UI from ordinary UI PRs; Firefox/WebKit on every PR (main/weekly matrix only). + +**Remote / Cloud browser drift.** When `check:installed-lock-parity` fails on `playwright`, or `check:playwright-browser-revision` reports `/opt/pw-browsers` revision drift, do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at a mismatched shell. Delegate browser proof to CI Production UI (or refresh the image/install matching browsers). See `#255` and [codex-cloud.md](codex-cloud.md). + Codex Cloud agents remain provider-free. Run authenticated Supabase tests through the manual `.github/workflows/authenticated-live-tests.yml` workflow, which requires the explicit `run-authenticated-live-tests` dispatch confirmation, records the run against the @@ -213,7 +241,7 @@ evidence-based verdict rather than disguising a public-network failure as a loca PR CI uses the same fail-closed classifier as `verify:pr-local`. `static-pr` always proves runtime/install parity, the classifier and verification-plan invariants, and changed-file formatting. Recognised documentation changes add documentation integrity checks; recognised workflow/policy-only changes add action/policy self-tests and `test:ci-workflows`. Executable, test, build/config, dependency, database/container, mixed, or unknown non-document paths set `static_heavy_changed`, retaining lint, typecheck, safety/config/RAG, and the full unit coverage job. Build, migration, Docker, and browser jobs remain separately scoped. A dependency audit blocks on lockfile/npm-config changes and the scheduled full-run sentinel, instead of making a low-value registry request on every PR. -UI scope runs a fail-fast `@critical` Chromium job on pull requests, then one required full production Chromium invocation (`test:e2e:pr`) for non-quarantined journeys, plus one advisory invocation for quarantined and mockup journeys. `src/app/api/**` does not set `ui_changed` or `db_changed` — API handlers stay on unit/coverage (and offline RAG when retrieval-scoped). The `PR required` aggregate keeps `if: always()` and distinguishes `cancelled` from `failure` in its messages (stays red; a skipped required check would count as passing). Secret Scan pins Gitleaks to the workflow event base/head SHAs and the checked-out commit, and verifies the linux_x64 release tarball against a pinned SHA-256 before install. The weekly `release-browser-matrix` depends on static/build/UI success, not on the full aggregate. Container scope calls the reusable Docker workflow and requires both app and worker image builds through the aggregate. +UI scope runs a fail-fast `@critical` Chromium job on pull requests, then required production Chromium journeys (`test:e2e:pr:shard`) across three **duration-aware explicit file groups** (`scripts/playwright-pr-shards.mjs`), plus one advisory invocation for quarantined and mockup journeys. Playwright `--shard=i/N` is not used for the PR gate — count-balanced sharding packed the slow phone-scroll family into one runner. `src/app/api/**` does not set `ui_changed` or `db_changed` — API handlers stay on unit/coverage (and offline RAG when retrieval-scoped). The `PR required` aggregate keeps `if: always()` and distinguishes `cancelled` from `failure` in its messages (stays red; a skipped required check would count as passing). Secret Scan pins Gitleaks to the workflow event base/head SHAs and the checked-out commit, and verifies the linux_x64 release tarball against a pinned SHA-256 before install. The weekly `release-browser-matrix` depends on static/build/UI success, not on the full aggregate. Container scope calls the reusable Docker workflow and requires both app and worker image builds through the aggregate. PR body synchronization is skipped unless the checked-out head actually contains `PR_POLICY_BODY.md`. The eval-canary liveness API probe runs once with the daily Ops Digest cadence rather than on every PR. These remove repeated provider-side work without weakening a required result. diff --git a/package.json b/package.json index 9948e35186..d4487d64b3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,9 @@ "test:e2e:critical": "node scripts/run-playwright.mjs --project=chromium --grep @critical", "test:e2e:regression": "node scripts/run-playwright.mjs --project=chromium --grep-invert \"@critical|@quarantine|@mockup\"", "test:e2e:pr": "node scripts/run-playwright.mjs --project=chromium --grep-invert \"@quarantine|@mockup\"", + "test:e2e:pr:shard": "node scripts/playwright-pr-shards.mjs", + "check:playwright-pr-shards": "node scripts/playwright-pr-shards.mjs --validate", + "check:playwright-browser-revision": "node scripts/check-playwright-browser-revision.mjs", "test:e2e:quarantine": "node scripts/run-playwright.mjs --project=chromium --grep @quarantine --pass-with-no-tests", "test:e2e:mockups": "node scripts/run-playwright.mjs --project=chromium-mockups", "test:e2e:advisory": "node scripts/run-playwright.mjs --project=chromium --project=chromium-mockups --grep \"@quarantine|@mockup\" --pass-with-no-tests", diff --git a/scripts/check-playwright-browser-revision.mjs b/scripts/check-playwright-browser-revision.mjs new file mode 100644 index 0000000000..5dda976d24 --- /dev/null +++ b/scripts/check-playwright-browser-revision.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * Fail closed when a designated container browser root cannot satisfy the + * Playwright revision pinned by the installed playwright-core package (#255). + * + * Local managed caches (`~/.cache/ms-playwright`) are fine. The trap is + * PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD + * where a newer lock expects chromium-1234 but the image only ships 1194 — + * pointing PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH at the stale shell is forbidden. + */ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const DEFAULT_CONTAINER_ROOT = "/opt/pw-browsers"; + +export function readExpectedChromiumRevision(projectRoot = process.cwd()) { + const browsersJsonPath = path.join(projectRoot, "node_modules", "playwright-core", "browsers.json"); + if (!existsSync(browsersJsonPath)) { + return { ok: false, reason: `playwright-core browsers.json missing at ${browsersJsonPath}` }; + } + let payload; + try { + payload = JSON.parse(readFileSync(browsersJsonPath, "utf8")); + } catch { + return { ok: false, reason: `playwright-core browsers.json is malformed at ${browsersJsonPath}` }; + } + const chromium = (payload.browsers ?? []).find((entry) => entry.name === "chromium"); + if (!chromium?.revision) { + return { ok: false, reason: "playwright-core browsers.json has no chromium.revision" }; + } + return { ok: true, revision: String(chromium.revision), browsersJsonPath }; +} + +export function listInstalledChromiumRevisions(browsersRoot) { + if (!browsersRoot || !existsSync(browsersRoot)) return []; + const revisions = new Set(); + for (const entry of readdirSync(browsersRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const match = /^(?:chromium|chromium_headless_shell)-(\d+)$/.exec(entry.name); + if (match) revisions.add(match[1]); + } + return [...revisions].sort(); +} + +/** + * @param {{ + * projectRoot?: string, + * env?: NodeJS.ProcessEnv, + * containerBrowsersRoot?: string, + * }} [options] + */ +export function playwrightBrowserRevisionCheck(options = {}) { + const projectRoot = options.projectRoot ?? process.cwd(); + const env = options.env ?? process.env; + const containerBrowsersRoot = options.containerBrowsersRoot ?? DEFAULT_CONTAINER_ROOT; + const expected = readExpectedChromiumRevision(projectRoot); + if (!expected.ok) { + return { + ok: false, + status: "missing-playwright-core", + message: expected.reason, + expectedRevision: null, + installedRevisions: [], + }; + } + + const exposedRoot = env.PLAYWRIGHT_BROWSERS_PATH?.trim().replace(/\/+$/, "") ?? ""; + const downloadsDisabled = /^(?:1|true)$/i.test(env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD?.trim() ?? ""); + const designatedContainer = + exposedRoot.replaceAll("\\", "/") === containerBrowsersRoot.replaceAll("\\", "/") && downloadsDisabled; + + if (!designatedContainer) { + return { + ok: true, + status: "managed-or-unconstrained", + message: + "No designated container browser root is forced; use the Playwright-managed cache or install matching browsers.", + expectedRevision: expected.revision, + installedRevisions: [], + }; + } + + const installed = listInstalledChromiumRevisions(exposedRoot); + if (installed.includes(expected.revision)) { + return { + ok: true, + status: "container-aligned", + message: `Container browsers at ${exposedRoot} include chromium revision ${expected.revision}.`, + expectedRevision: expected.revision, + installedRevisions: installed, + }; + } + + return { + ok: false, + status: "container-revision-drift", + message: [ + `Playwright browser revision drift (#255): lock/playwright-core expects chromium-${expected.revision},`, + `but ${exposedRoot} only has: ${installed.length ? installed.map((r) => `chromium-${r}`).join(", ") : "(none)"}.`, + "Do not set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to a mismatched shell.", + "Delegate browser proof to CI Production UI, or refresh the image / run `npx playwright install` into a matching cache,", + "or unset PLAYWRIGHT_BROWSERS_PATH and PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD so the managed cache can be used.", + ].join(" "), + expectedRevision: expected.revision, + installedRevisions: installed, + }; +} + +function parseArgs(args) { + const options = { json: false, root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") }; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--root") { + const value = args[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--root requires a project directory."); + options.root = path.resolve(value); + index += 1; + continue; + } + if (token === "--help" || token === "-h") { + console.log("Usage: npm run check:playwright-browser-revision -- [--json] [--root directory]"); + process.exit(0); + } + throw new Error(`Unknown option: ${token}`); + } + return options; +} + +function isDirectRun() { + return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +} + +if (isDirectRun()) { + const options = parseArgs(process.argv.slice(2)); + const result = playwrightBrowserRevisionCheck({ projectRoot: options.root }); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + } else if (result.ok) { + console.log(`Playwright browser revision check OK (${result.status}): ${result.message}`); + } else { + console.error(result.message); + } + process.exit(result.ok ? 0 : 1); +} diff --git a/scripts/guard-next-build.mjs b/scripts/guard-next-build.mjs index 4e344fe9b6..fe82fea90a 100644 --- a/scripts/guard-next-build.mjs +++ b/scripts/guard-next-build.mjs @@ -36,6 +36,9 @@ export function formatLowRamBuildMessage(totalRamBytes = os.totalmem()) { ].join("\n"); } +/** Distinct from generic failures so verify:pr-local can name a refused build (#167). */ +export const DEV_SERVER_BUILD_REFUSED_EXIT_CODE = 76; + function requestJson(port) { return new Promise((resolve) => { let settled = false; @@ -115,9 +118,10 @@ async function main() { [ `Refusing to run next build while ${appName} dev server is running at http://localhost:${runningPort}.`, "Stop the dev server first, or set ALLOW_BUILD_WITH_DEV_SERVER=1 if this cache churn is intentional.", + `BUILD_REFUSED_DEV_SERVER exit=${DEV_SERVER_BUILD_REFUSED_EXIT_CODE}`, ].join("\n"), ); - process.exit(1); + process.exit(DEV_SERVER_BUILD_REFUSED_EXIT_CODE); } } diff --git a/scripts/playwright-pr-shards.mjs b/scripts/playwright-pr-shards.mjs new file mode 100644 index 0000000000..885db4d4e8 --- /dev/null +++ b/scripts/playwright-pr-shards.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Duration-aware Production UI shard groups for required Chromium PR journeys. + * + * Playwright `--shard=i/N` balances by test *count* in collection (alphabetical) + * order, which packs the slow phone-scroll family into one shard. Explicit + * groups mix slow-per-test and faster mega-specs so wall time is closer across + * runners. Every production `test:e2e:pr` file must appear in exactly one group + * — `tests/playwright-pr-shards.test.ts` fails closed on orphans/duplicates. + * + * Do not rename specs to game alphabetical sharding. Re-measure after suite + * growth before changing group membership. + */ +import { readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { childProcessExitCode } from "./child-process-result.mjs"; + +/** Same matcher as playwright.config.ts `productionSpecPattern` (keep in sync). */ +export const productionSpecFilePattern = + /^(?:answer-progress-ui-smoke|ui-(?:smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts$/; + +/** + * Explicit shard membership. Ordered to mix the measured slow phone-scroll + * family away from chrome-scroll/accessibility while keeping mega-specs apart. + */ +export const prUiShardGroups = { + 1: [ + "tests/ui-phone-scroll.spec.ts", + "tests/ui-phone-scroll-page-owned.spec.ts", + "tests/ui-phone-scroll-document-rail.spec.ts", + "tests/ui-phone-scroll-routes.spec.ts", + "tests/ui-route-coverage.spec.ts", + "tests/ui-specifiers.spec.ts", + "tests/ui-forms-section-nav.spec.ts", + "tests/ui-pwa.spec.ts", + "tests/ui-hydration.spec.ts", + "tests/ui-style-contract.spec.ts", + "tests/ui-visual-artifacts.spec.ts", + "tests/ui-mode-nav-density.spec.ts", + ], + 2: [ + "tests/ui-smoke.spec.ts", + "tests/ui-chrome-scroll.spec.ts", + "tests/answer-progress-ui-smoke.spec.ts", + "tests/ui-formulation.spec.ts", + ], + 3: [ + "tests/ui-tools.spec.ts", + "tests/ui-accessibility.spec.ts", + "tests/ui-overlap.spec.ts", + "tests/ui-universal-search.spec.ts", + "tests/ui-stress.spec.ts", + "tests/ui-therapy-nav-scroll.spec.ts", + ], +}; + +export function listProductionSpecFiles(testsDir = path.join(process.cwd(), "tests")) { + return readdirSync(testsDir) + .filter((file) => productionSpecFilePattern.test(file)) + .map((file) => `tests/${file}`) + .sort(); +} + +export function validatePrUiShardGroups(groups = prUiShardGroups, { listFiles = listProductionSpecFiles } = {}) { + const onDisk = listFiles(); + const assigned = []; + const duplicates = []; + for (const shard of Object.keys(groups).sort((a, b) => Number(a) - Number(b))) { + const files = groups[shard]; + if (!Array.isArray(files) || files.length === 0) { + throw new Error(`PR UI shard ${shard} is empty — empty shards fail test:e2e:pr (no --pass-with-no-tests).`); + } + for (const file of files) { + if (assigned.includes(file)) duplicates.push(file); + assigned.push(file); + } + } + const assignedSorted = [...assigned].sort(); + const missing = onDisk.filter((file) => !assigned.includes(file)); + const extra = assignedSorted.filter((file) => !onDisk.includes(file)); + return { + ok: missing.length === 0 && extra.length === 0 && duplicates.length === 0, + onDisk, + assigned: assignedSorted, + missing, + extra, + duplicates: [...new Set(duplicates)].sort(), + shardCount: Object.keys(groups).length, + }; +} + +export function filesForPrUiShard(shard, groups = prUiShardGroups) { + const key = String(shard); + const files = groups[key] ?? groups[Number(key)]; + if (!files?.length) { + throw new Error(`Unknown or empty PR UI shard: ${shard}`); + } + return files; +} + +function parseArgs(args) { + const options = { shard: undefined, list: false, validate: false }; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--list") { + options.list = true; + continue; + } + if (token === "--validate") { + options.validate = true; + continue; + } + if (token === "--shard") { + const value = args[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--shard requires a shard number (1..N)."); + options.shard = value; + index += 1; + continue; + } + if (token === "--help" || token === "-h") { + console.log( + "Usage: node scripts/playwright-pr-shards.mjs --validate | --list | --shard N\n" + + " --validate Assert every production e2e:pr spec is in exactly one group.\n" + + " --list Print shard membership.\n" + + " --shard N Run test:e2e:pr for that explicit file group.", + ); + process.exit(0); + } + throw new Error(`Unknown option: ${token}`); + } + return options; +} + +function isDirectRun() { + return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +} + +if (isDirectRun()) { + const options = parseArgs(process.argv.slice(2)); + if (options.validate || options.list) { + const result = validatePrUiShardGroups(); + if (options.list) { + for (const [shard, files] of Object.entries(prUiShardGroups)) { + console.log(`shard ${shard} (${files.length} files):`); + for (const file of files) console.log(` ${file}`); + } + } + if (!result.ok) { + console.error( + [ + "PR UI shard groups are out of sync with production specs.", + result.missing.length ? `missing from groups: ${result.missing.join(", ")}` : null, + result.extra.length ? `unknown in groups: ${result.extra.join(", ")}` : null, + result.duplicates.length ? `duplicated: ${result.duplicates.join(", ")}` : null, + ] + .filter(Boolean) + .join("\n"), + ); + process.exit(1); + } + console.log(`PR UI shard parity OK: ${result.onDisk.length} production specs across ${result.shardCount} groups.`); + process.exit(0); + } + + if (!options.shard) { + console.error("Provide --shard N, --validate, or --list."); + process.exit(2); + } + + const files = filesForPrUiShard(options.shard); + const result = spawnSync( + process.execPath, + ["scripts/run-playwright.mjs", ...files, "--project=chromium", "--grep-invert", "@quarantine|@mockup"], + { stdio: "inherit", env: process.env }, + ); + process.exit(childProcessExitCode(result)); +} + +export const playwrightPrShardsInternals = { productionSpecFilePattern, prUiShardGroups }; diff --git a/scripts/verify-phone-chrome.mjs b/scripts/verify-phone-chrome.mjs index 251b80c270..c7c7451609 100644 --- a/scripts/verify-phone-chrome.mjs +++ b/scripts/verify-phone-chrome.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env node import { execFileSync, spawnSync } from "node:child_process"; -import { pathToFileURL } from "node:url"; +import { rmSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { childProcessExitCode } from "./child-process-result.mjs"; import { phoneChromePlan, renderPhoneChromeCommand } from "./phone-chrome-plan.mjs"; @@ -47,29 +49,92 @@ function changedFiles(explicitFiles) { return JSON.parse(result).files; } -export function runPhoneChromeCommand(command, { spawn = spawnSync } = {}) { +/** Stages that invoke an isolated Playwright production build. */ +export function isPhoneChromeBrowserStage(stage) { + const args = stage?.command?.args ?? []; + if (args.includes("scripts/run-playwright.mjs")) return true; + if (stage?.command?.executable === "npm" && args.includes("verify:ui")) return true; + return false; +} + +export function phoneChromeSharedBuildEnv(browserStageCount, { pid = process.pid, env = process.env } = {}) { + if (browserStageCount < 2) return null; + if (env.PLAYWRIGHT_BUILD_ROOT_ID?.trim() && env.PLAYWRIGHT_KEEP_BUILD_ROOT?.trim() === "true") { + // Caller already opted into a keep-root session; do not override or auto-clean it. + return null; + } + return { + PLAYWRIGHT_BUILD_ROOT_ID: `phone-chrome-${pid}`, + PLAYWRIGHT_KEEP_BUILD_ROOT: "true", + }; +} + +export function runPhoneChromeCommand(command, { spawn = spawnSync, env = process.env } = {}) { const executable = process.platform === "win32" && command.executable === "npm" ? "cmd.exe" : command.executable; const args = executable === "cmd.exe" ? ["/d", "/s", "/c", [command.executable, ...command.args].join(" ")] : command.args; - const result = spawn(executable, args, { stdio: "inherit" }); + const result = spawn(executable, args, { stdio: "inherit", env }); return childProcessExitCode(result); } +function cleanupSharedBuildRoot(buildRootId, { projectRoot = process.cwd(), rm = rmSync, log = console.log } = {}) { + if (!buildRootId) return; + const absoluteRunRoot = path.join(projectRoot, ".next-playwright", buildRootId); + try { + rm(absoluteRunRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + log(`[phone-chrome] cleaned shared Playwright build root (.next-playwright/${buildRootId})`); + } catch (error) { + log( + `[phone-chrome] warning: could not clean shared build root ${absoluteRunRoot}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} + export function runPhoneChromeStages( stages, - { runCommand = runPhoneChromeCommand, exit = process.exit, log = console.log } = {}, + { + runCommand = runPhoneChromeCommand, + exit = process.exit, + log = console.log, + env = process.env, + sharedBuildEnv = phoneChromeSharedBuildEnv, + cleanupBuildRoot = cleanupSharedBuildRoot, + } = {}, ) { - for (const stage of stages) { - log(`\n[phone-chrome:${stage.id}] ${stage.label}`); - log(`> ${renderPhoneChromeCommand(stage.command)}`); - const exitCode = runCommand(stage.command); - if (exitCode !== 0) { - // Announce on stderr so a piped caller without pipefail still has an - // unambiguous failure line in the captured log (outstanding-issues #120). - console.error(`[phone-chrome] stage "${stage.id}" failed; exiting with code ${exitCode}`); - exit(exitCode); - return exitCode; + const browserStages = stages.filter(isPhoneChromeBrowserStage); + const shared = sharedBuildEnv(browserStages.length, { env }); + const runEnv = shared ? { ...env, ...shared } : env; + if (shared) { + log( + `[phone-chrome] reusing Playwright build root ${shared.PLAYWRIGHT_BUILD_ROOT_ID} across ${browserStages.length} browser stages`, + ); + } + + let exitCode = 0; + try { + for (const stage of stages) { + log(`\n[phone-chrome:${stage.id}] ${stage.label}`); + log(`> ${renderPhoneChromeCommand(stage.command)}`); + exitCode = runCommand(stage.command, { env: runEnv }); + if (exitCode !== 0) { + // Announce on stderr so a piped caller without pipefail still has an + // unambiguous failure line in the captured log (outstanding-issues #120). + console.error(`[phone-chrome] stage "${stage.id}" failed; exiting with code ${exitCode}`); + break; + } } + } finally { + // Clean before process.exit — Node does not run finally after exit(). + if (shared?.PLAYWRIGHT_BUILD_ROOT_ID) { + const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + cleanupBuildRoot(shared.PLAYWRIGHT_BUILD_ROOT_ID, { log, projectRoot }); + } + } + if (exitCode !== 0) { + exit(exitCode); + return exitCode; } return 0; } @@ -84,6 +149,10 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) if (options.dryRun) { console.log("\nPhone chrome verification plan (dry run):"); for (const stage of plan.stages) console.log(`- [${stage.id}] ${renderPhoneChromeCommand(stage.command)}`); + const browserCount = plan.stages.filter(isPhoneChromeBrowserStage).length; + if (browserCount >= 2) { + console.log(`- shared Playwright build root will be reused across ${browserCount} browser stages (then cleaned)`); + } process.exit(0); } diff --git a/scripts/verify-pr-local.mjs b/scripts/verify-pr-local.mjs index 5feb5e6c23..f5ce440fd6 100644 --- a/scripts/verify-pr-local.mjs +++ b/scripts/verify-pr-local.mjs @@ -1,6 +1,9 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { childProcessExitCode } from "./child-process-result.mjs"; +import { DEV_SERVER_BUILD_REFUSED_EXIT_CODE } from "./guard-next-build.mjs"; const isWindows = process.platform === "win32"; // Live Supabase audits (check:locality-metadata) stay out of this unconditional gate. @@ -85,7 +88,7 @@ function readScope(files) { return JSON.parse(result.stdout); } -function selectedScripts(scope, extended) { +export function selectedScripts(scope, extended) { const scripts = []; const add = (...items) => { for (const item of items) if (!scripts.includes(item)) scripts.push(item); @@ -106,6 +109,60 @@ function selectedScripts(scope, extended) { return scripts; } +/** + * Closing summary so a selected-but-not-executed step cannot look like a green pass (#167). + * @param {string[]} scripts + * @param {{ completed: string[], failedScript: string | null, failedExitCode: number }} progress + */ +export function summarizePrLocalRun(scripts, progress) { + const completed = progress.completed ?? []; + const failedScript = progress.failedScript ?? null; + const failedExitCode = progress.failedExitCode ?? 0; + const notReached = failedScript + ? scripts.slice(scripts.indexOf(failedScript) + 1) + : scripts.filter((script) => !completed.includes(script)); + + const lines = ["", "PR-local verification summary:"]; + lines.push(`- completed: ${completed.length ? completed.join(", ") : "(none)"}`); + if (failedScript) { + lines.push(`- failed: ${failedScript} (exit ${failedExitCode})`); + if (failedScript === "build" && failedExitCode === DEV_SERVER_BUILD_REFUSED_EXIT_CODE) { + lines.push( + `- note: production build was refused while the Clinical KB dev server is running (BUILD_REFUSED_DEV_SERVER). This is a failed gate, not a skip.`, + ); + } + } else { + lines.push("- failed: (none)"); + } + lines.push(`- not reached: ${notReached.length ? notReached.join(", ") : "(none)"}`); + return lines.join("\n"); +} + +export function runPrLocalScripts( + scripts, + { runScript = runNpmScript, log = console.log, error = console.error } = {}, +) { + const completed = []; + let failedScript = null; + let failedExitCode = 0; + + for (const script of scripts) { + const exitCode = runScript(script); + if (exitCode !== 0) { + failedScript = script; + failedExitCode = exitCode; + break; + } + completed.push(script); + } + + const summary = summarizePrLocalRun(scripts, { completed, failedScript, failedExitCode }); + if (failedExitCode !== 0) error(summary); + else log(summary); + + return failedExitCode; +} + function assertPlan(name, scope, expected, extended = false) { const actual = selectedScripts(scope, extended); if (JSON.stringify(actual) !== JSON.stringify(expected)) { @@ -140,32 +197,34 @@ if (process.argv.includes("--self-test")) { process.exit(0); } -const options = parseArgs(process.argv.slice(2)); -const scope = readScope(options.files); -const scripts = selectedScripts(scope, options.extended); -console.log(`Changed files: ${scope.files.length > 0 ? scope.files.join(", ") : "(none detected)"}`); - -if (options.dryRun) { - console.log("\nPR-local verification plan (dry run):"); - for (const script of scripts) console.log(`- npm run ${script}`); - if (!scope.static_heavy_changed) - console.log("- lint, typecheck, full unit suite and RAG fixture scan skipped: recognised low-risk scope"); - if (!scope.build_changed) console.log("- build skipped: no build-affecting changes detected"); - if (!scope.static_heavy_changed) console.log("- offline RAG checks skipped: no executable product scope"); - else if (!scope.rag_eval_changed) - console.log("- offline RAG production contracts skipped: no RAG-scoped changes (fixtures still selected)"); - if (options.extended && !scope.ui_changed) - console.log("- Chromium UI gate skipped: no UI-affecting changes detected"); - process.exit(0); +function isDirectRun() { + return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); } -let exitCode = 0; -for (const script of scripts) { - exitCode = runNpmScript(script); - if (exitCode !== 0) break; -} +if (isDirectRun()) { + const options = parseArgs(process.argv.slice(2)); + const scope = readScope(options.files); + const scripts = selectedScripts(scope, options.extended); + console.log(`Changed files: ${scope.files.length > 0 ? scope.files.join(", ") : "(none detected)"}`); + + if (options.dryRun) { + console.log("\nPR-local verification plan (dry run):"); + for (const script of scripts) console.log(`- npm run ${script}`); + if (!scope.static_heavy_changed) + console.log("- lint, typecheck, full unit suite and RAG fixture scan skipped: recognised low-risk scope"); + if (!scope.build_changed) console.log("- build skipped: no build-affecting changes detected"); + if (!scope.static_heavy_changed) console.log("- offline RAG checks skipped: no executable product scope"); + else if (!scope.rag_eval_changed) + console.log("- offline RAG production contracts skipped: no RAG-scoped changes (fixtures still selected)"); + if (options.extended && !scope.ui_changed) + console.log("- Chromium UI gate skipped: no UI-affecting changes detected"); + process.exit(0); + } -if (exitCode !== 0) process.exit(exitCode); + const exitCode = runPrLocalScripts(scripts); -if (!scope.build_changed) - console.log("\nSkipping build: no build-affecting source, config, package, or container changes detected."); + if (exitCode !== 0) process.exit(exitCode); + + if (!scope.build_changed) + console.log("\nSkipping build: no build-affecting source, config, package, or container changes detected."); +} diff --git a/tests/check-playwright-browser-revision.test.ts b/tests/check-playwright-browser-revision.test.ts new file mode 100644 index 0000000000..f3f802ba33 --- /dev/null +++ b/tests/check-playwright-browser-revision.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { + listInstalledChromiumRevisions, + playwrightBrowserRevisionCheck, + readExpectedChromiumRevision, +} from "../scripts/check-playwright-browser-revision.mjs"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +describe("check-playwright-browser-revision", () => { + it("reads the chromium revision from the installed playwright-core browsers.json", () => { + const expected = readExpectedChromiumRevision(process.cwd()); + expect(expected.ok).toBe(true); + expect(expected.revision).toMatch(/^\d+$/); + }); + + it("passes when no designated container browser root is forced", () => { + const result = playwrightBrowserRevisionCheck({ + env: { NODE_ENV: "test" }, + }); + expect(result.ok).toBe(true); + expect(result.status).toBe("managed-or-unconstrained"); + }); + + it("fails closed on /opt/pw-browsers revision drift without suggesting a mismatched executable (#255)", () => { + const root = mkdtempSync(path.join(tmpdir(), "pw-browsers-")); + mkdirSync(path.join(root, "chromium_headless_shell-1194")); + const projectRoot = mkdtempSync(path.join(tmpdir(), "pw-project-")); + mkdirSync(path.join(projectRoot, "node_modules", "playwright-core"), { recursive: true }); + writeFileSync( + path.join(projectRoot, "node_modules", "playwright-core", "browsers.json"), + JSON.stringify({ browsers: [{ name: "chromium", revision: "1234" }] }), + ); + + const result = playwrightBrowserRevisionCheck({ + projectRoot, + containerBrowsersRoot: root, + env: { + NODE_ENV: "test", + PLAYWRIGHT_BROWSERS_PATH: root, + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1", + }, + }); + + expect(result.ok).toBe(false); + expect(result.status).toBe("container-revision-drift"); + expect(result.expectedRevision).toBe("1234"); + expect(result.installedRevisions).toEqual(["1194"]); + expect(result.message).toContain("Delegate browser proof to CI"); + expect(result.message).not.toMatch(/set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to .*1194/); + }); + + it("lists installed chromium revisions from a browsers root", () => { + const root = mkdtempSync(path.join(tmpdir(), "pw-list-")); + mkdirSync(path.join(root, "chromium-1234")); + mkdirSync(path.join(root, "chromium_headless_shell-1234")); + mkdirSync(path.join(root, "firefox-1000")); + expect(listInstalledChromiumRevisions(root)).toEqual(["1234"]); + }); +}); diff --git a/tests/guard-next-build.test.ts b/tests/guard-next-build.test.ts index 6e8c17fbf3..e397f7978d 100644 --- a/tests/guard-next-build.test.ts +++ b/tests/guard-next-build.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { evaluateNextBuildRamGuard } from "../scripts/guard-next-build.mjs"; +import { DEV_SERVER_BUILD_REFUSED_EXIT_CODE, evaluateNextBuildRamGuard } from "../scripts/guard-next-build.mjs"; const eightGiB = 8 * 1024 * 1024 * 1024; const twelveGiB = 12 * 1024 * 1024 * 1024; @@ -25,3 +25,9 @@ describe("evaluateNextBuildRamGuard", () => { expect(evaluateNextBuildRamGuard({ totalRamBytes: eightGiB, env: { ALLOW_LOW_RAM_BUILD: "1" } })).toBe("warn"); }); }); + +describe("DEV_SERVER_BUILD_REFUSED_EXIT_CODE", () => { + it("uses a dedicated non-zero code so refused builds cannot look green (#167)", () => { + expect(DEV_SERVER_BUILD_REFUSED_EXIT_CODE).toBe(76); + }); +}); diff --git a/tests/playwright-pr-shards.test.ts b/tests/playwright-pr-shards.test.ts new file mode 100644 index 0000000000..18aff038cd --- /dev/null +++ b/tests/playwright-pr-shards.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { + filesForPrUiShard, + listProductionSpecFiles, + prUiShardGroups, + productionSpecFilePattern, + validatePrUiShardGroups, +} from "../scripts/playwright-pr-shards.mjs"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +describe("playwright PR UI shard groups", () => { + it("covers every on-disk production spec exactly once", () => { + const result = validatePrUiShardGroups(); + expect(result.missing, `orphaned production specs: ${result.missing.join(", ")}`).toEqual([]); + expect(result.extra, `unknown group entries: ${result.extra.join(", ")}`).toEqual([]); + expect(result.duplicates, `duplicated specs: ${result.duplicates.join(", ")}`).toEqual([]); + expect(result.ok).toBe(true); + expect(result.shardCount).toBe(3); + expect(result.onDisk.length).toBeGreaterThan(10); + }); + + it("keeps the file pattern aligned with playwright.config.ts productionSpecPattern", () => { + const config = readFileSync(path.resolve("playwright.config.ts"), "utf8"); + expect(config).toContain("const productionSpecPattern ="); + expect(config).toContain("phone-scroll(?:-[a-z0-9-]+)?"); + expect(productionSpecFilePattern.test("ui-phone-scroll-page-owned.spec.ts")).toBe(true); + expect(productionSpecFilePattern.test("ui-tools-collapse.spec.ts")).toBe(false); + }); + + it("keeps every shard non-empty and returns files for CI runners", () => { + for (const shard of [1, 2, 3] as const) { + const files = filesForPrUiShard(shard); + expect(files.length).toBeGreaterThan(0); + expect(prUiShardGroups[shard]).toEqual(files); + } + }); + + it("lists only production basename matches under tests/", () => { + const files = listProductionSpecFiles(); + expect(files.every((file) => file.startsWith("tests/") && file.endsWith(".spec.ts"))).toBe(true); + expect(files).not.toContain("tests/ui-tools-collapse.spec.ts"); + }); +}); diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 33b06c6af4..6f9f163184 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -775,6 +775,9 @@ describe("provider-safe test environment", () => { expect(preflight).toContain("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"); expect(runner).toContain("process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = preinstalledChromium.path"); expect(packageJson.scripts["test:e2e:pr"]).toContain('--grep-invert "@quarantine|@mockup"'); + expect(packageJson.scripts["test:e2e:pr:shard"]).toContain("scripts/playwright-pr-shards.mjs"); + expect(ciWorkflow).toContain("npm run test:e2e:pr:shard -- --shard ${{ matrix.shard }}"); + expect(ciWorkflow).not.toContain("--shard=${{ matrix.shard }}/3"); expect(packageJson.scripts["test:e2e:regression"]).toContain('--grep-invert "@critical|@quarantine|@mockup"'); expect(baseUrl.indexOf("if (!allowEnsure)")).toBeLessThan(baseUrl.indexOf("findExistingLocalProjectUrl();")); expect(ragRunner).toContain("cwd: projectRoot"); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 68d38e8a89..e135850807 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -20,9 +20,10 @@ import { serviceRecords } from "../src/lib/services"; import { recentQueryStorageKey } from "../src/lib/recent-query-storage"; const dashboardViewports = [ + // Representative owners only: one phone (<640), tablet (≤768), laptop, and + // landscape. Extra phone widths (375/414) duplicated the same overflow + Ask + // asserts without a distinct layout branch. { name: "small-mobile", width: 320, height: 720 }, - { name: "standard-mobile", width: 375, height: 760 }, - { name: "large-mobile", width: 414, height: 820 }, { name: "tablet", width: 768, height: 900 }, { name: "laptop", width: 1280, height: 900 }, { name: "mobile-landscape", width: 667, height: 375 }, diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index fcfc1c2acb..63c353424d 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -986,7 +986,8 @@ test.describe("Clinical KB tools launcher", () => { for (const viewport of [ { name: "mobile", width: 390, height: 820 }, - { name: "tablet", width: 768, height: 1024 }, + // Tablet (≥640) shares the desktop-page composer path with desktop; keep one + // representative width above the phone breakpoint. { name: "desktop", width: 1280, height: 900 }, ] as const) { for (const route of [ diff --git a/tests/verify-phone-chrome.test.ts b/tests/verify-phone-chrome.test.ts index 40c2d80e2d..e73f7d16b5 100644 --- a/tests/verify-phone-chrome.test.ts +++ b/tests/verify-phone-chrome.test.ts @@ -141,4 +141,60 @@ describe("runPhoneChromeStages", () => { expect(exit).toHaveBeenCalledWith(1); expect(runCommand).toHaveBeenCalledTimes(2); }); + + it("reuses one Playwright build root across multiple browser stages and cleans it", () => { + const exit = vi.fn() as unknown as typeof process.exit; + const runCommand = vi.fn().mockReturnValue(0); + const cleanupBuildRoot = vi.fn(); + const stages = [ + { + id: "focused-browser", + label: "browser", + command: { executable: "node", args: ["scripts/run-playwright.mjs", "tests/ui-smoke.spec.ts"] }, + }, + { id: "full-ui", label: "full", command: { executable: "npm", args: ["run", "verify:ui"] } }, + ]; + + const code = runPhoneChromeStages(stages, { + runCommand, + exit, + log: () => undefined, + env: { NODE_ENV: "test" }, + cleanupBuildRoot, + }); + + expect(code).toBe(0); + expect(exit).not.toHaveBeenCalled(); + expect(runCommand).toHaveBeenCalledTimes(2); + const firstEnv = runCommand.mock.calls[0]?.[1]?.env as Record; + const secondEnv = runCommand.mock.calls[1]?.[1]?.env as Record; + expect(firstEnv.PLAYWRIGHT_KEEP_BUILD_ROOT).toBe("true"); + expect(firstEnv.PLAYWRIGHT_BUILD_ROOT_ID).toMatch(/^phone-chrome-/); + expect(secondEnv.PLAYWRIGHT_BUILD_ROOT_ID).toBe(firstEnv.PLAYWRIGHT_BUILD_ROOT_ID); + expect(cleanupBuildRoot).toHaveBeenCalledWith(firstEnv.PLAYWRIGHT_BUILD_ROOT_ID, expect.any(Object)); + }); + + it("does not invent a shared root for a single browser stage", () => { + const runCommand = vi.fn().mockReturnValue(0); + const cleanupBuildRoot = vi.fn(); + const stages = [ + { + id: "focused-browser", + label: "browser", + command: { executable: "node", args: ["scripts/run-playwright.mjs"] }, + }, + ]; + + runPhoneChromeStages(stages, { + runCommand, + exit: vi.fn() as unknown as typeof process.exit, + log: () => undefined, + env: { NODE_ENV: "test" }, + cleanupBuildRoot, + }); + + const passedEnv = runCommand.mock.calls[0]?.[1]?.env as Record; + expect(passedEnv.PLAYWRIGHT_KEEP_BUILD_ROOT).toBeUndefined(); + expect(cleanupBuildRoot).not.toHaveBeenCalled(); + }); }); diff --git a/tests/verify-pr-local.test.ts b/tests/verify-pr-local.test.ts index 7c056fa21c..0fbb77b74a 100644 --- a/tests/verify-pr-local.test.ts +++ b/tests/verify-pr-local.test.ts @@ -1,7 +1,9 @@ import { execFileSync, spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { DEV_SERVER_BUILD_REFUSED_EXIT_CODE } from "../scripts/guard-next-build.mjs"; +import { runPrLocalScripts, summarizePrLocalRun } from "../scripts/verify-pr-local.mjs"; const script = path.resolve("scripts/verify-pr-local.mjs"); @@ -61,4 +63,36 @@ describe("verify-pr-local CLI", () => { expect(result.status).toBe(1); expect(result.stderr).toContain("ALLOW_EXTENDED_PR_LOCAL=true"); }); + + it("treats a refused build as a failed selected step, not a green skip (#167)", () => { + const error = vi.fn(); + const runScript = vi + .fn() + .mockReturnValueOnce(0) + .mockReturnValueOnce(0) + .mockReturnValueOnce(DEV_SERVER_BUILD_REFUSED_EXIT_CODE); + const scripts = ["check:runtime", "format:changed", "build", "check:rag:fixtures"]; + + const exitCode = runPrLocalScripts(scripts, { runScript, log: () => undefined, error }); + + expect(exitCode).toBe(DEV_SERVER_BUILD_REFUSED_EXIT_CODE); + expect(runScript).toHaveBeenCalledTimes(3); + expect(error).toHaveBeenCalledOnce(); + const summary = String(error.mock.calls[0]?.[0] ?? ""); + expect(summary).toContain("failed: build"); + expect(summary).toContain("BUILD_REFUSED_DEV_SERVER"); + expect(summary).toContain("not reached: check:rag:fixtures"); + expect(summary).not.toMatch(/Skipping build/); + }); + + it("summarizes completed scripts when the whole plan passes", () => { + const summary = summarizePrLocalRun(["check:runtime", "build"], { + completed: ["check:runtime", "build"], + failedScript: null, + failedExitCode: 0, + }); + expect(summary).toContain("completed: check:runtime, build"); + expect(summary).toContain("failed: (none)"); + expect(summary).toContain("not reached: (none)"); + }); });