diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 3da1d205e..73fd6acb8 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -648,3 +648,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-20 | claude/clinical-kb-pwa-review-asi3wb (restarted; PR: #901 rerank regression closeout) | fd5dcd1582e1c7aa8bf92ff097575e0c68aa930d | #901 rerank regression: baseline re-eval + offline guard net + saturated-tie fix | Baseline eval-canary dispatch on remediated main b3ae061 (run 29731533081) PASSED — golden retrieval 36/36 + answer-quality green, confirming #913-#926 closed the live 4/36 regression (July 19 failing run tested the raw #901 state; fixes landed 1-8h after it, never re-evaled until now). Residual defect found by new offline repro and fixed: with byte-identical imputed fast-path primaries, selection's clamped keys tie exactly and the chunk-id fallback decides, which secondStageScore's position adjustment launders into releaseRankScore (buried the CIWA answer doc in repro); fix = contentRankScore carried on RetrievalCandidate as tie-break between clamped rerankScore and chunk id, never added to scores. Amended #901's own never-live-validated saturated-tie pin to content-rank-then-id. Guards: tests/rag-fast-path-ordering.test.ts (4 end-to-end eval-shape repros) + ranking-tuning gates (4 golden-mapped hard negatives at production weights + full-snapshot high-risk). Fixture aliases NOT changed (Step C not triggered — identity cases pass live). | Targeted 4-suite vitest 37/37; npm run test 2981 passed (sole fail = container-only pdf-extraction-budget artifact); verify:cheap green through all 18 pre-test checks incl. lint+typecheck; check:production-readiness PASS on substantive checks (2 FAILs = documented missing-secret class in this container); build + client-bundle scan + check:rag:fixtures PASS | | 2026-07-20 | claude/clinical-kb-pwa-review-asi3wb (restarted; PR: coverage tie-break follow-up) | 57ec880b306a8e2b31c5f20dacc47256fc93b4e2 | Post-merge live-eval finding on #982 + corrective fix: saturated-tie key rankScore → query-term coverage | Post-#982 golden dispatch (eval-canary run #50, 29735004222, main b9057f0 + deps) came back 35/36: the three verifiable July-19 failures (lithium-therapy-monitoring, clozapine-anc-threshold, patient-safety-plan-include) all PASS live, but alcohol-ciwa-threshold flipped pass→FAIL vs the same-morning pre-#982 run #49 (29731533081, 36/36) — failing top-3 ordered by descending rankScore (1.85/1.75/1.53, all finalScore-saturated, releaseRankScore 1.09/1.086/1.07), i.e. #982's tie-break let generic clinicalSignalBoost stacking outvote the ciwa/score/threshold-bearing chunk; #982 is the only retrieval-path delta in the window. Fix: contentRankScore → contentCoverageScore sourced from lexicalCoverageScore (query-term coverage, immune to boost stacking; ties still fall to chunk id); saturated-tie contract test re-pinned so coverage beats a HIGHER rankScore (discriminating — old key fails it); fast-path CIWA guard gains the run-#50 screening-chunk shape + content-term assertion. Live validation: tonight's 18:00 UTC scheduled canary (dispatch cap 2/2 spent ≈$2-4). Separately: ci.yml dispatch 4012 survived 30+ min of main churn under #979's per-run concurrency group (fix working); duplicate dispatch 4017 cancelled. | Targeted vitest 38/38; npm run test 3012 passed / 1 known container-only pdf-budget artifact; verify:cheap green to the same artifact; build + client-bundle scan + check:rag:fixtures PASS; check:production-readiness expected missing-secret FAILs only (no secrets in container) | | 2026-07-20 | claude/clinical-kb-pwa-review-asi3wb (restarted; PR: WebKit matrix diagnosis) | caa298972846ef52d472bfbff98c90fc26481ed8 | First completed release-browser-matrix in weeks (run 4012, main b9057f0, 38 min uncancelled under #979) — full triage of 28 failures + fix for the dominant cluster | Run 4012: 716 passed / 28 failed (25 webkit, 3 firefox) / 4 skipped. Dominant cluster (root-caused, FIXED here): all 8 ui-universal-search webkit failures share one signature — typeahead content never enters the DOM — because commandDropdownCanDisplay (added 42a3e3c 2026-07-17, AFTER the last completed matrix; never ran on WebKit until 4012) requires fine-pointer OR zero-touch; headless browsers fail the (hover:hover)+(pointer:fine) query (proven by the zero-touch escape existing for CI at all), Chromium/Firefox pass via maxTouchPoints===0, and Playwright's Linux WebKit build advertises phantom touch points — flunking both branches and disabling useUniversalSearch entirely. Fix: beforeEach addInitScript in ui-universal-search/ui-smoke/ui-tools specs stubbing Navigator.prototype.maxTouchPoints to the runner's true 0 (inert on Chromium/Firefox; product gate + tests/search-command-surface.test.ts pins untouched). Local chromium runs of ui-universal-search fail 16/20 IDENTICALLY on unmodified main (container artifact — hosted CI chromium green in 4012 is authoritative; verified by stash/run/pop baseline). Expected delta next matrix: ≥8 webkit failures clear; candidates ui-tools:1244 + several ui-smoke answer-flow cases (same surface). NOT yet root-caused (triaged remainder, hosted-matrix-only reproduction): webkit ui-stress overflow 330/409 (360px overflow at mobile), webkit ui-accessibility 195/252 (focus dismissal; forced-colors labels), webkit ui-formulation 132, webkit ui-smoke copy-table/retry/recovery/recent-searches/source-only/differential-context/viewer-hydration/document-questions, webkit ui-tools 2087 (goto interrupted by ?q= navigation), webkit ui-universal-search 183 strict-mode duplicate options (fallback-surface rendering; likely clears with the gate fix), firefox ui-smoke 946/2206/2932 (@critical document search fails on BOTH firefox and webkit — cross-browser, highest-priority remainder). | prettier PASS; eslint PASS; typecheck PASS; chromium stash-baseline no-delta (16 fail pre AND post — container artifact); webkit validation lands via tonight's 18:00 UTC scheduled matrix | +| 2026-07-20 | claude/clinical-kb-pwa-review-asi3wb (restarted; PR: matrix-remainder fixes) | ec070ccb20b9a067216e64e8399d9cd795027024 | Run-4012 remainder: 7 root-caused fixes (SW navigation hijack, WebKit focus/forced-colors/stale-style, Firefox tab order, CIWA alias, helper extraction) | Agent-diagnosed with probe evidence: (1) production-build PWA worker registers in EVERY matrix test (pwa-lifecycle.tsx:287 NODE_ENV gate), clients.claim()s the page and serves all navigations — Chromium probe proved SW-served reloads bypass page.route entirely (routeSawNav=[]); Playwright-Firefox wedges on its only two reloads (ui-smoke 2206/2932, both deterministic) → playwright.config.ts serviceWorkers:'block' + ui-pwa 'allow' opt-in (offline/CacheStorage journey verified passing locally under the opt-in). (2) Mode-menu dismissal relied solely on wrapper focusout; WebKit Tab navigation can move focus nowhere (links excluded) or wrap into the menu → keydown Tab-close in handleModeTriggerKeyDown (agent verified no test depends on old forward-Tab behavior). (3) WebKit has no forced-colors implementation → capability skip on the token-remap test. (4) WebKit stale :disabled computed style feeds axe a phantom 1.93 contrast for the re-enabled Previous button (blend arithmetic exact: 0.4×#475467+0.6×#fff=#b5bbc2) → toBeEnabled+opacity-1 pin before the scan, failure-at-pin = direct proof. (5) Firefox includes scrollable containers in tab order (sheet body scrollHeight 1125 vs 707 measured) → conditional step-over. (6) canary #50/#51 CIWA failure root cause: whitespace-delimited textContainsClinicalTerm can never match hyphenated 'CIWA-Ar' though the dosing-table region ranks top-5 → clinicalContentAliases ciwa:[ciwa, ciwa-ar] (plan-authorized fixture-alias route; content substance unchanged). (7) tests/helpers/zero-touch.ts shared helper replaces six #995 inline stubs (CodeRabbit follow-up). NOT claimed: ui-stress 409 overflow, ui-tools 2087 navigation race, webkit answer-flow subset — next matrix run (post-merge dispatch) measures these vs the 28-failure baseline; ui-smoke 2932 webkit-side is a distinct mock-data class deliberately deferred pending that run. | Targeted vitest 25/25 (eval-retrieval + search-command-surface); npm run test 3012 passed / 1 known container pdf-budget artifact; verify:cheap green to same artifact; build + client-bundle scan PASS; prettier/eslint/typecheck clean; chromium ui-pwa under new SW config: offline/CacheStorage privacy journey PASS, installability = known container in-incognito artifact; UI verification not run locally beyond that: chromium ui suites carry documented container artifacts — hosted ui-critical/ui-advisory + next matrix authoritative | diff --git a/playwright.config.ts b/playwright.config.ts index 4ba972e55..c6e637f9d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -48,6 +48,13 @@ export default defineConfig({ // reduced-motion a11y spec emulates a per-test mode, so suite-wide settings // remain stable across builds. contextOptions: { reducedMotion: "reduce" }, + // In production builds the PWA worker (public/sw.js) registers in every test, + // claims the page, and serves every subsequent navigation — bypassing route + // interception for navigations outright, and wedging Playwright-Firefox's + // reload path under an active route (the two ui-smoke reload hangs in matrix + // run 4012). Only ui-pwa.spec.ts is meant to exercise the worker; it opts + // back in with test.use({ serviceWorkers: "allow" }). + serviceWorkers: "block", }, projects: [ { diff --git a/scripts/eval-retrieval.ts b/scripts/eval-retrieval.ts index 30c7f7e5c..aeb4a7e1e 100644 --- a/scripts/eval-retrieval.ts +++ b/scripts/eval-retrieval.ts @@ -255,6 +255,10 @@ const clinicalDocumentAliases: Record = { const clinicalContentAliases: Record = { anc: ["anc", "absolute neutrophil count", "neutrophil", "neutrophils"], + // The scale's written name is the hyphenated token "CIWA-Ar"; textContainsClinicalTerm is + // whitespace-delimited, so the bare fixture term can never match it (canary runs #50/#51: + // the top-5 included the CIWA-Ar dosing-table region yet the content gate reported a miss). + ciwa: ["ciwa", "ciwa-ar"], fbc: ["fbc", "full blood count", "full blood", "wbc", "white blood cell", "white cell"], im: ["im", "intramuscular", "intramuscularly"], mg: ["mg", "milligram", "milligrams", "dose", "doses"], diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 02f34f26f..a17b51dbd 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -798,6 +798,14 @@ export function MasterSearchHeader({ } else if (event.key === "ArrowUp") { event.preventDefault(); openModeMenuWithFocus(selectedModeIndex - 1); + } else if (event.key === "Tab" && modeMenuOpen && !usesPhoneSearchLayout) { + // Desktop: Tab/Shift+Tab is leaving the trigger — close without trapping (APG + // menu-button pattern; arrow keys are the entry into the menu). WebKit's + // sequential focus navigation may never deliver a wrapper-escaping focusout + // (links are excluded from its Tab order, and backward navigation can wrap + // into the open menu), so keydown is the reliable dismiss signal; the wrapper + // onBlur remains the net for pointer and programmatic focus moves. + setModeMenuOpen(false); } } diff --git a/tests/helpers/zero-touch.ts b/tests/helpers/zero-touch.ts new file mode 100644 index 000000000..804564bd5 --- /dev/null +++ b/tests/helpers/zero-touch.ts @@ -0,0 +1,14 @@ +import type { Page } from "playwright/test"; + +/** + * Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI + * runner, tripping the fine-pointer/zero-touch gate on the search command surface + * (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. + * Report the runner's real capability so WebKit exercises the same desktop surfaces. + * Register as a beforeEach (or call inside one) BEFORE the page under test navigates. + */ +export async function stubZeroTouchPoints({ page }: { page: Page }) { + await page.addInitScript(() => { + Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); + }); +} diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index 9c642b719..9cd70a0b2 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -1,5 +1,6 @@ import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page, type TestInfo } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; const readySetupChecks = [ { id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." }, @@ -178,15 +179,7 @@ async function expectNoBlockingAxeViolations(page: Page, testInfo: TestInfo, opt expect(summary, "axe found critical/serious WCAG A/AA violations").toEqual([]); } -// Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI -// runner, tripping the fine-pointer/zero-touch gate on the search command surface -// (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. -// Report the runner's real capability so WebKit exercises the same desktop surfaces. -test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); -}); +test.beforeEach(stubZeroTouchPoints); test.describe("Clinical KB accessibility coverage", () => { test.describe.configure({ timeout: 60_000 }); @@ -259,7 +252,11 @@ test.describe("Clinical KB accessibility coverage", () => { await expectNoPageHorizontalOverflow(page); }); - test("solid-button label tokens stay legible with forced colors", async ({ page }) => { + test("solid-button label tokens stay legible with forced colors", async ({ browserName, page }) => { + test.skip( + browserName === "webkit", + "WebKit has no forced-colors implementation; Playwright's forcedColors emulation cannot engage the @media (forced-colors: active) token remap under test (the guarded glyph-backplate behavior is Chromium's).", + ); // Chromium paints a Canvas backplate behind every glyph run in forced-colors // mode: a label whose color resolves into the Canvas/ButtonFace family // disappears into its own backplate (axe cannot see this — it reads CSS, diff --git a/tests/ui-formulation.spec.ts b/tests/ui-formulation.spec.ts index d5f6d5871..fe37b5943 100644 --- a/tests/ui-formulation.spec.ts +++ b/tests/ui-formulation.spec.ts @@ -1,5 +1,6 @@ import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page, type TestInfo } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; const axeWcagTags = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]; const axeBlockingImpacts = new Set(["critical", "serious"]); @@ -50,13 +51,7 @@ async function expectNoBlockingAxeViolations(page: Page, testInfo: TestInfo) { test.beforeEach(async ({ page }) => { await blockExternalRequests(page); - // Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI - // runner, tripping the fine-pointer/zero-touch gate on the search command surface - // (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. - // Report the runner's real capability so WebKit exercises the same desktop surfaces. - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); + await stubZeroTouchPoints({ page }); }); test("searches patient language, opens a mechanism guide, and carries it into the builder", async ({ @@ -171,6 +166,14 @@ test("moves a selected mechanism through framework, quality review, and an edita await expect(draft).not.toHaveValue("Stale edited draft"); await expect(draft).toHaveValue(/Select mechanisms and add case evidence/); await expectNoHorizontalOverflow(page); + // WebKit can serve a stale :disabled computed style (opacity .4) for the step-nav + // Previous button after the select->draft transition re-enables it; axe folds that + // opacity into the glyph color and reports a phantom color-contrast violation for a + // state WCAG 1.4.3 exempts anyway (matrix run 4012). Poll the computed style so axe + // measures the live enabled state; a failure HERE is direct proof of the stale style. + const previousStep = page.getByRole("button", { name: "Previous", exact: true }); + await expect(previousStep).toBeEnabled(); + await expect(previousStep).toHaveCSS("opacity", "1"); await expectNoBlockingAxeViolations(page, testInfo); }); diff --git a/tests/ui-pwa.spec.ts b/tests/ui-pwa.spec.ts index c99e69e84..8bb66bfc7 100644 --- a/tests/ui-pwa.spec.ts +++ b/tests/ui-pwa.spec.ts @@ -118,6 +118,10 @@ function isAllowedPublicCachePath(pathname: string) { ); } +// The suite-wide Playwright config blocks service workers (they hijack navigations +// in production builds); this spec is the one place the worker itself is under test. +test.use({ serviceWorkers: "allow" }); + test.describe("Clinical KB PWA", () => { test.describe.configure({ timeout: 120_000 }); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 2950034c1..e6129dc40 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1,5 +1,6 @@ import type { Route } from "playwright-core"; import { expect, test, type Locator, type Page } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; import { readMobileComposerReservePx, scrollPrimarySurface } from "./playwright-scroll"; import { answerThreadStorageKey } from "../src/lib/answer-thread-storage"; import { documentSummaryQuestion } from "../src/lib/answer-contract"; @@ -873,15 +874,7 @@ async function openDailyActions(page: Page, triggerName: string | RegExp = /^Ope return menu; } -// Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI -// runner, tripping the fine-pointer/zero-touch gate on the search command surface -// (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. -// Report the runner's real capability so WebKit exercises the same desktop surfaces. -test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); -}); +test.beforeEach(stubZeroTouchPoints); test.describe("Clinical KB UI smoke coverage", () => { test.describe.configure({ timeout: 60000 }); @@ -987,6 +980,14 @@ test.describe("Clinical KB UI smoke coverage", () => { const restingButtonShadow = await newChat.evaluate((element) => getComputedStyle(element).boxShadow); await closeMenu.focus(); await page.keyboard.press("Tab"); + // Firefox includes scrollable containers in the tab order; the sheet body + // (overflow-y-auto) sits between Close and "New chat" in DOM order and + // genuinely overflows at this viewport. Step over it when focused. + const onScrollableBody = await page.evaluate(() => { + const element = document.activeElement; + return element instanceof HTMLElement && element.classList.contains("overflow-y-auto"); + }); + if (onScrollableBody) await page.keyboard.press("Tab"); await expect(newChat).toBeFocused(); const buttonFocus = await newChat.evaluate((element) => { const style = getComputedStyle(element); diff --git a/tests/ui-stress.spec.ts b/tests/ui-stress.spec.ts index b3ef03f7d..c81ca1ded 100644 --- a/tests/ui-stress.spec.ts +++ b/tests/ui-stress.spec.ts @@ -1,5 +1,6 @@ import type { Route } from "playwright-core"; import { expect, test, type Page } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; import { loadMedicationSnapshot } from "../src/lib/medication-snapshot"; const longTitle = @@ -321,15 +322,7 @@ async function openScopeControl(page: Page) { }).toPass({ timeout: 20_000 }); } -// Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI -// runner, tripping the fine-pointer/zero-touch gate on the search command surface -// (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. -// Report the runner's real capability so WebKit exercises the same desktop surfaces. -test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); -}); +test.beforeEach(stubZeroTouchPoints); test.describe("Clinical KB long-content stress coverage", () => { for (const viewport of [ diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 1d6e7765d..38edf578b 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Locator, type Page } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; import type { Route } from "playwright-core"; import { acuteConfusionPresentationWorkflow, differentialRecords } from "../src/lib/differentials"; import { demoAnswer, demoDocuments } from "../src/lib/demo-data"; @@ -350,15 +351,7 @@ async function expectVerticalSeparation(page: Page, upperSelector: string, lower expect((metrics?.lowerTop ?? 0) - (metrics?.upperBottom ?? 0)).toBeGreaterThanOrEqual(minimumGap); } -// Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI -// runner, tripping the fine-pointer/zero-touch gate on the search command surface -// (commandDropdownCanDisplay) that Chromium and Firefox pass via the zero-touch fallback. -// Report the runner's real capability so WebKit exercises the same desktop surfaces. -test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); -}); +test.beforeEach(stubZeroTouchPoints); test.describe("Clinical KB tools launcher", () => { test.describe.configure({ timeout: 60_000 }); diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts index 88b253b8e..7bb7e7d71 100644 --- a/tests/ui-universal-search.spec.ts +++ b/tests/ui-universal-search.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page, type Route } from "playwright/test"; +import { stubZeroTouchPoints } from "./helpers/zero-touch"; // Cross-entity universal typeahead in the command surface. The universal endpoint is // mocked so this spec exercises the UI contract (grouped sections, navigation, @@ -115,17 +116,7 @@ async function openComposer(page: Page, href = "/?mode=documents&focus=1") { return input; } -// Playwright's Linux WebKit build advertises phantom touch points on the touch-free CI -// runner. That trips the deliberate fine-pointer/zero-touch gate on the search command -// surface (commandDropdownCanDisplay: headless browsers fail the fine-pointer media query -// and rely on the zero-touch fallback), silently hiding the desktop dropdown that Chromium -// and Firefox render — every typeahead assertion then fails with "element not found". -// Report the runner's real capability; the product gate and its unit pins stay unchanged. -test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "maxTouchPoints", { configurable: true, get: () => 0 }); - }); -}); +test.beforeEach(stubZeroTouchPoints); test.describe("universal search typeahead", () => { test("shows grouped cross-entity results while typing", async ({ page }) => {