diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index ec877ab9fa..5cc5472fa4 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1175,3 +1175,9 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `a1ca6a016490e4d4b564edd3d553d87fed3071df` | Protected-main RAG reliability, clinical-governance and release review | APPROVE. Independent retrieval, governance and fallback reviews found and fixed three merge blockers: global chunk-query alias overreach was narrowed to the measured clozapine blood-count action shape; legacy private source reviews remain on the deployed v1 RPC while unapplied-v2 paths fail explicitly; and source-backed review fallback is now a zero-tolerance blocking metric with reconciled evidence. Final rereviews found no P0-P2. PR #1288 was superseded without force-push after GitGuardian correctly rejected a token-shaped fake fixture; the clean replacement tree is byte-identical to the reviewed tree and both secret scanners pass. The additive BMJ attestation migration remains unapplied and BMJ stays unverified pending qualified human action. | Exact application tree `verify:pr-local` PASS: format, zero-warning lint, typecheck, 403 files and 4,101 tests passed with 2 skipped, production build/client-secret scan, and 36 offline RAG fixtures. Live 36-case canary PASS with document/content recall 1.0, zero failed cases and zero per-case document/content RR regressions; three cache-bypassed affected-path answer probes PASS with zero provider requests and zero generation cost. Earlier coverage PASS: 399 files, 4,062 passed and 2 skipped, RAG 86.83% statements and 90.79% lines. Hosted build, static, unit coverage, migration replay, Supabase Preview, Production UI, policy, Semgrep, Gitleaks and GitGuardian passed on the implementation tree; final evidence-only head requires the normal hosted rerun. | | 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `d49af8acb42ccedbfe6c8b3f03d30769ba906ec7` | Bugbot review (exact head) | APPROVE. No high-confidence P0–P2. Application `src/`/`supabase/`/`tests/`/`scripts/` trees are unchanged vs prior APPROVE tip `a1ca6a01`; tip delta is docs evidence + clean merge from `main`. No unresolved `cursor[bot]` Bugbot threads. Residual risk: unapplied BMJ attestation migration (`#022`) correctly fails closed with `503 source_review_v2_unavailable` until qualified hosted apply. | Focused high-risk Vitest 358 + 1502 passed; merge-tree CLEAN vs `origin/main`; no provider-backed checks; no PR comment mutations. | | 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `ab6ca036937bff1acaefbda8a5581d6d75f489b3` | Final current-main sync review | APPROVE pending fresh exact-head required checks. Merged current `origin/main` without conflict after its already-reviewed document-search and focus-path changes; no protected RAG, evaluation, migration, or RAG fixture surface changed from the live-canary application tree, and no P0-P2 finding remains. | `git merge-tree --write-tree` CLEAN before sync; branch-ledger guard and `git diff --check` PASS; prior exact application-tree `verify:pr-local` and live 36-case canary remain applicable; fresh hosted checks required. | +| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `82c17f76` | Mode-switch prefetch review + merge restore | Prefetch mode homes on menu open; later reconciled with main per-option prefetch. Ledger restored append-only from main after mojibake rewrite. | Focused nav tests; check:branch-review-ledger; no provider checks. | +| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `bc5b51c2` | CodeRabbit duplicate-ledger disposition | DISPOSITIONED / not actionable as a PR product delete. Near-duplicates already exist on origin/main as non-identical historical records; check:branch-review-ledger passes on main. Removing main-owned history from a feature PR would violate append-only. | substring counts on origin/main; ledger guard PASS; no provider checks. | +| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `191707ee` | CodeRabbit malformed-ledger-row disposition | DISPOSITIONED / not actionable on this PR. MD056 cell-count warnings on ~700-722 are identical on origin/main. Fix belongs in dedicated main ledger hygiene if desired. | cell-count scan main vs tip; check:branch-review-ledger PASS; no provider checks. | +| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `f4b55751` | CodeRabbit behavioral prefetch-test disposition | RESOLVED. Behavioral DOM coverage for menu-open prefetch paths (later adapted to per-option model). | focused vitest PASS; no provider checks. | +| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `81005d18` | Codex mojibake-ledger disposition | RESOLVED. Historical rows restored byte-for-byte from origin/main; append-only thereafter. | exact prefix check; check:branch-review-ledger PASS; no provider checks. | +| 2026-07-28 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `01469840` | CI/merge conflict closeout + Bugbot triage | RESOLVED merge conflict with origin/main (7ae4eb49 per-option prefetchModeHome). Dropped superseded bulk prefetchModeHomes; menu-open warms only highlighted option; focus/pointer scanning retained. CodeRabbit duplicate/malformed ledger threads dispositioned (main-owned). No open cursor[bot] Bugbot product defects. | merge-tree clean; focused vitest 12/12; ledger restored from main + appends; no provider checks. | diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index d493f7afa9..373a4482af 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -852,9 +852,22 @@ export function MasterSearchHeader({ setScopeSheetOpen(false); } + // Mode options are buttons (not Links), so Next cannot discover destinations. + // Prefetch only the mode the user is about to choose — the highlighted option + // on open, then whichever option receives focus/pointer while scanning. + function prefetchModeHome(modeId: AppModeId) { + if (modeId === searchMode) return; + const href = appModeHomeHref(modeId); + if (prefetchedModeHrefsRef.current.has(href)) return; + prefetchedModeHrefsRef.current.add(href); + router.prefetch(href); + } + function openModeMenuWithFocus(index: number) { closeModeSurfaces(); const nextIndex = (index + visibleAppModeOptions.length) % visibleAppModeOptions.length; + const highlighted = visibleAppModeOptions[nextIndex]; + if (highlighted) prefetchModeHome(highlighted.id); const phoneLayout = currentUsesPhoneSearchLayout(); setUsesPhoneSearchLayout(phoneLayout); setModeMenuFocusIndex(nextIndex); @@ -872,6 +885,8 @@ export function MasterSearchHeader({ setModeMenuOpen(false); return; } + const highlighted = visibleAppModeOptions[selectedModeIndex]; + if (highlighted) prefetchModeHome(highlighted.id); setUsesPhoneSearchLayout(currentUsesPhoneSearchLayout()); setModeMenuFocusIndex(selectedModeIndex); setModeMenuOpen(true); @@ -923,14 +938,6 @@ export function MasterSearchHeader({ } } - function prefetchModeHome(modeId: AppModeId) { - if (modeId === searchMode) return; - const href = appModeHomeHref(modeId); - if (prefetchedModeHrefsRef.current.has(href)) return; - prefetchedModeHrefsRef.current.add(href); - router.prefetch(href); - } - function renderModeMenuOptions() { return visibleAppModeOptions.map((mode, index) => { const Icon = appModeIcons[mode.id]; diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 1ef55e78f8..bfc15b223c 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -122,12 +122,29 @@ describe("audit navigation and auth regressions", () => { "function renderModeMenuOptions()", "const restoreActionMenuFocusRef", ); + const openModeMenuWithFocus = sourceSegment( + masterSearchHeaderSource, + "function openModeMenuWithFocus(", + "function toggleModeMenu(", + ); + const toggleModeMenu = sourceSegment( + masterSearchHeaderSource, + "function toggleModeMenu(", + "function handleModeTriggerKeyDown(", + ); expect(masterSearchHeaderSource).toContain("function prefetchModeHome(modeId: AppModeId)"); expect(masterSearchHeaderSource).toContain("router.prefetch(href)"); expect(modeOptions).toContain("onFocus={() => prefetchModeHome(mode.id)}"); expect(modeOptions).toContain("onPointerEnter={() => prefetchModeHome(mode.id)}"); + // Menu-open paths warm only the highlighted option — never every visible home. + expect(openModeMenuWithFocus).toContain("prefetchModeHome(highlighted.id)"); + expect(toggleModeMenu).toContain("prefetchModeHome(highlighted.id)"); + expect(masterSearchHeaderSource).not.toContain("function prefetchModeHomes("); expect(masterSearchHeaderSource).not.toContain("visibleAppModeOptions.forEach((mode) => router.prefetch"); + expect(masterSearchHeaderSource).not.toContain( + "new Set(visibleAppModeOptions.map((mode) => appModeHomeHref(mode.id)))", + ); }); it("defers cross-mode search on narrow screens until expansion except for completed answers", () => { diff --git a/tests/mode-menu-prefetch.dom.test.tsx b/tests/mode-menu-prefetch.dom.test.tsx new file mode 100644 index 0000000000..d84c8c7241 --- /dev/null +++ b/tests/mode-menu-prefetch.dom.test.tsx @@ -0,0 +1,115 @@ +/** @vitest-environment jsdom */ + +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header"; +import { appModeHomeHref, visibleAppModeDefinitionsForSession } from "@/lib/app-modes"; + +const router = vi.hoisted(() => ({ + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => router, +})); + +vi.mock("@/lib/supabase/client", () => ({ + useAuthSession: () => ({ + status: "signed_out", + session: null, + isConfigured: true, + error: null, + signInWithEmail: vi.fn(), + signOut: vi.fn(), + }), +})); + +vi.mock("@/components/clinical-dashboard/use-saved-registry-favourites", () => ({ + useSavedRegistryFavourites: () => [], +})); + +vi.mock("@/components/clinical-dashboard/search-command-context", () => ({ + useSearchCommand: () => null, +})); + +vi.mock("@/components/clinical-dashboard/universal-search-also-matches", () => ({ + UniversalSearchAlsoMatches: () => null, +})); + +function headerProps() { + return { + demoMode: false, + documents: [], + query: "", + searchMode: "answer" as const, + loading: false, + selectedDocumentIds: [] as string[], + queryMode: "auto" as const, + scopeFilters: {}, + realDataReady: true, + canAccessFavourites: false, + onQueryChange: () => undefined, + onSearchModeChange: vi.fn(), + onAsk: () => undefined, + onClearQuery: () => undefined, + onClearScope: () => undefined, + onQueryModeChange: () => undefined, + onScopeFiltersChange: () => undefined, + onToggleScope: () => undefined, + queryModeOptions: [{ value: "auto" as const, label: "Auto" }], + }; +} + +function guestModeHomes() { + return visibleAppModeDefinitionsForSession({ authenticated: false, demoMode: false }); +} + +describe("mode menu home prefetch", () => { + beforeEach(() => { + router.push.mockReset(); + router.replace.mockReset(); + router.prefetch.mockReset(); + }); + + it("prefetches a mode home when the user points at that option", async () => { + const user = userEvent.setup(); + const documents = guestModeHomes().find((mode) => mode.id === "documents"); + expect(documents).toBeTruthy(); + const documentsHref = appModeHomeHref("documents"); + + render(); + await user.click(screen.getByRole("button", { name: /Mode Answer/i })); + const menu = await screen.findByRole("menu", { name: "Choose app mode" }); + + // Opening on the current mode is a no-op; scanning another option warms it. + expect(router.prefetch).not.toHaveBeenCalledWith(documentsHref); + await user.hover(within(menu).getByRole("menuitemradio", { name: /Documents/i })); + expect(router.prefetch).toHaveBeenCalledWith(documentsHref); + const prefetched = new Set(router.prefetch.mock.calls.map(([href]) => href as string)); + expect(prefetched.has(documentsHref)).toBe(true); + expect(prefetched.size).toBeLessThan(guestModeHomes().length); + }); + + it("prefetches the highlighted mode when openModeMenuWithFocus targets another home", async () => { + const user = userEvent.setup(); + const modes = guestModeHomes(); + const answerIndex = modes.findIndex((mode) => mode.id === "answer"); + expect(answerIndex).toBeGreaterThanOrEqual(0); + const previous = modes[(answerIndex - 1 + modes.length) % modes.length]; + expect(previous.id).not.toBe("answer"); + const previousHref = appModeHomeHref(previous.id); + + render(); + const trigger = screen.getByRole("button", { name: /Mode Answer/i }); + trigger.focus(); + await user.keyboard("{ArrowUp}"); + await screen.findByRole("menu", { name: "Choose app mode" }); + + expect(router.prefetch).toHaveBeenCalledWith(previousHref); + expect(new Set(router.prefetch.mock.calls.map(([href]) => href)).size).toBe(1); + }); +});