diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index daedab10..da033824 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -44,7 +44,6 @@ Durable, cross-session memory of everything still outstanding for this repo: ope | #012 | P3 | rec | Slim the lazy cross-mode differentials chunk | `cross-mode-differentials.ts` is dynamically imported (correctly code-split **out** of the initial/dashboard bundle — verified), but it pulls the full ~860 KB differentials snapshot (~125 KB gzip lazy chunk) just to build a tiny `{slug,title,clinicalHinge}` + presentations + aliases catalog. A precomputed lightweight index (generator + drift check, like the `specifiers-content` split / medications `fields=index`) would cut that lazy chunk ~5–10×. Not a bundle leak — an M-effort slim. | `src/lib/cross-mode-differentials.ts`; `src/components/clinical-dashboard/cross-mode-links.tsx:150`; session 2026-07-21 (build:analyze) | 2026-07-21 | | #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | `build:analyze`: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), `/formulation` ships `formulation-content.json` (~52 KB, client-side local search — needs index/full split or a search endpoint, architectural). All route-scoped (not initial bundle). Also `*-mockups.tsx` (~100 KB across chunks) build though `/mockups` 404s in prod — exclude from the prod artifact. | session 2026-07-21 (build:analyze) | 2026-07-21 | | #014 | P3 | rec | Realize the `next/image` win on signed previews | `next.config` `images` (AVIF + `*.supabase.co` `remotePatterns` pinned to the project host, from #1024) is currently inert — signed document/image previews still render as raw ``. Route them through `next/image` to actually get AVIF + lazy optimization. | #1024; `src/components/clinical-dashboard/signed-image.tsx`; session 2026-07-21 | 2026-07-21 | -| #015 | P3 | task | Content-first fallback regression tests | CodeRabbit nitpick on #1024: add focused tests for the medication + registry (services/forms) content-first transitions — fallback content during loading, live record swap-in, error/empty states, and that **no authoritative verification badge shows before live governance reconciles** (the neutralized fixture flag). | #1024 CodeRabbit review; `src/components/clinical-dashboard/medication-record-page.tsx`; `src/components/registry-record-loader.tsx` | 2026-07-21 | | #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `ƒ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them). | session 2026-07-21 (build route table + design audit) | 2026-07-21 | | #017 | P3 | task | Field Web-Vitals baseline via live Lighthouse | In-sandbox runtime vitals were blocked (prod server hard-requires Supabase secrets; dev-mode CLS measured excellent at 0.00–0.04, content-first pages 0.000). Run Lighthouse against `psychiatry.tools` for real LCP/INP/CLS to prioritize #012–#016 by measured impact rather than reasoning. | session 2026-07-21 (measurement pass) | 2026-07-21 | | #018 | P2 | task | Monitoring retrieval-depth trio misses schedules | quality-lithium-monitoring-range, quality-antipsychotic-metabolic-monitoring, quality-adhd-medication-monitoring still fail targeting ("no schedule/interval") after the #1040 gate parity — their live chunk sets apparently surface no admissible schedule sentence. Diagnose via read-only chunk queries + run #61 dump artifact (30d retention) to split corpus-content vs retrieval-depth vs fact-extraction; fix shape TBD after diagnosis. A retrieval-side fix is a protected surface (canary pair, fresh ~$2-4 authorization). | run #61 targeting misses; branch-review-ledger 2026-07-21 | 2026-07-21 | @@ -63,3 +62,4 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #003 | task | Staging tenancy release evidence outstanding | Ran GitHub Action and validated isolation | 2026-07-21 | | #002 | task | Process-ownership fix not yet isolated on `main` | Fixed process isolation using child.pid termination | 2026-07-21 | | #008 | rec | Dead href builders in `document-flow-routes.ts` | Not dead code (false positive): `documentReaderHref`/`documentEvidenceHref` are live via the mock wrappers in `src/components/document-search-mockups.tsx` + `src/components/master-document-flow-mockups.tsx` (rendered under `src/app/mockups/document-search/`) and covered by `tests/document-flow-routes.test.ts`; removing breaks the build. Only the production non-mock hrefs are unlinked from prod UI — a wiring gap, not dead code. | 2026-07-22 | +| #015 | task | Content-first fallback regression tests | Added `tests/registry-record-loader.dom.test.tsx` (8) + `tests/medication-record-page.dom.test.tsx` (6) covering content-first fallback paint, live swap-in, spinner/skeleton, error + not-found/unauthorized states, and the invariant that no authoritative verification badge shows before live governance reconciles (registry fixture-flag neutralization + medication governance-drop-on-error). | 2026-07-22 | diff --git a/tests/medication-record-page.dom.test.tsx b/tests/medication-record-page.dom.test.tsx new file mode 100644 index 00000000..1e19bf91 --- /dev/null +++ b/tests/medication-record-page.dom.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { MedicationRecordPage } from "@/components/clinical-dashboard/medication-record-page"; +import type { MedicationRecord } from "@/lib/medications"; + +// Controllable data-hook mock so each test drives one content-first state. +const { useMedicationDetail } = vi.hoisted(() => ({ useMedicationDetail: vi.fn() })); +vi.mock("@/components/clinical-dashboard/use-medication-catalog", () => ({ useMedicationDetail })); +// The two heavy sidebar panels carry their own data concerns; stub them so these +// tests isolate the page's content-first + governance-reconciliation logic. +vi.mock("@/components/clinical-dashboard/patient-profile-panel", () => ({ PatientProfilePanel: () => null })); +vi.mock("@/components/clinical-dashboard/medication-considerations", () => ({ MedicationConsiderations: () => null })); + +function mockDetail(state: { data: unknown; loading: boolean; error: string | null }) { + useMedicationDetail.mockReturnValue(state); +} + +// Minimal record with no `src` "...checked" text, so the "Reviewed" identity +// badge depends purely on governance — which lets us assert the governance-drop +// invariant cleanly (isReviewed() otherwise falls back to source-review text). +const fallbackDrug: MedicationRecord = { + slug: "test-med", + name: "Fallback Drug", + class: "Test class", + subclass: "", + category: "", + accent: "#0f766e", + tag: "", + schedule: "", + stats: [], + sections: [], + quick: [], +}; +const liveDrug: MedicationRecord = { ...fallbackDrug, name: "Live Drug" }; + +describe("MedicationRecordPage content-first states", () => { + it("renders the SSR fallback record immediately during loading, not the skeleton", () => { + mockDetail({ data: null, loading: true, error: null }); + render(); + expect(screen.getByRole("heading", { name: "Fallback Drug" })).toBeInTheDocument(); + expect(screen.queryByText(/Loading medication reference/i)).not.toBeInTheDocument(); + }); + + it("shows the skeleton when loading with no fallback record", () => { + mockDetail({ data: null, loading: true, error: null }); + render(); + expect(screen.getByText(/Loading medication reference/i)).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Fallback Drug" })).not.toBeInTheDocument(); + }); + + it("live swap-in prefers the live record over the SSR fallback", () => { + mockDetail({ data: { record: liveDrug, governance: null }, loading: false, error: null }); + render(); + expect(screen.getByRole("heading", { name: "Live Drug" })).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Fallback Drug" })).not.toBeInTheDocument(); + }); + + it("renders the error panel when nothing renderable exists", () => { + mockDetail({ data: null, loading: false, error: "Network unavailable" }); + render(); + expect(screen.getByText("Network unavailable")).toBeInTheDocument(); + }); + + it("drops fixture governance on error so a fixture 'Reviewed' badge does not persist as authoritative", () => { + mockDetail({ data: null, loading: false, error: "boom" }); + render( + , + ); + // Content-first still paints the record... + expect(screen.getByRole("heading", { name: "Fallback Drug" })).toBeInTheDocument(); + // ...but the fixture's approved-governance "Reviewed" badge must not survive + // the error, because the authoritative status is now unknown. + expect(screen.queryByText("Reviewed")).not.toBeInTheDocument(); + }); + + it("keeps the SSR fallback governance badge while the fetch is still in flight", () => { + mockDetail({ data: null, loading: true, error: null }); + render( + , + ); + // Contrast to the error case: while loading (no error) the SSR-provided + // governance is trusted, so the "Reviewed" badge shows. + expect(screen.getByText("Reviewed")).toBeInTheDocument(); + }); +}); diff --git a/tests/registry-record-loader.dom.test.tsx b/tests/registry-record-loader.dom.test.tsx new file mode 100644 index 00000000..11b045bc --- /dev/null +++ b/tests/registry-record-loader.dom.test.tsx @@ -0,0 +1,141 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { RegistryRecordLoader } from "@/components/registry-record-loader"; +import type { ServiceRecord } from "@/lib/services"; + +// Controllable hook mock: each test drives one state so we can exercise the full +// content-first state machine (loading fallback paint, live swap-in, empty +// states) without a real fetch. The sibling registry-retry.dom.test.tsx covers +// the error → Retry affordance; this file covers everything else. +const { useRegistryRecord } = vi.hoisted(() => ({ useRegistryRecord: vi.fn() })); +vi.mock("@/lib/use-registry-records", () => ({ useRegistryRecord })); + +function mockHook(state: Record) { + useRegistryRecord.mockReturnValue({ + status: "loading", + record: null, + linkedDocuments: [], + demoMode: false, + governance: null, + refetch: vi.fn(), + ...state, + }); +} + +const verified: ServiceRecord = { + slug: "cmhs", + title: "Community Mental Health Service", + verification: { locallyVerified: true }, +}; +const unverified: ServiceRecord = { + slug: "cmhs", + title: "Community Mental Health Service", + verification: { locallyVerified: false }, +}; + +// The render-prop receives the reconciled record, so we assert exactly which +// verification flag the loader hands downstream (the badge input) per state. +const body = (record: ServiceRecord) => ( +
+ {record.title} + {String(record.verification?.locallyVerified)} +
+); + +describe("RegistryRecordLoader content-first states", () => { + it("paints the public fallback record immediately during loading instead of a spinner", () => { + mockHook({ status: "loading", record: null }); + render( + + {body} + , + ); + expect(screen.getByTestId("title")).toHaveTextContent("Community Mental Health Service"); + expect(screen.queryByText(/Loading service record/i)).not.toBeInTheDocument(); + }); + + it("neutralizes a fixture 'locally verified' flag on the provisional paint (no authoritative badge before governance reconciles)", () => { + mockHook({ status: "loading", record: null }); + render( + + {body} + , + ); + // The fixture is verified, but the loading paint must not assert it before + // live governance reconciles — the flag is neutralized to false so a stale + // "verified" badge cannot flash in. + expect(screen.getByTestId("verified")).toHaveTextContent("false"); + }); + + it("shows a spinner (not content) when loading with no fallback record", () => { + mockHook({ status: "loading", record: null }); + const { container } = render( + + {body} + , + ); + expect(screen.getByText(/Loading service record/i)).toBeInTheDocument(); + expect(container.querySelector('[aria-busy="true"]')).not.toBeNull(); + expect(screen.queryByTestId("title")).not.toBeInTheDocument(); + }); + + it("live swap-in reconciles the verified flag ON when governance is approved", () => { + mockHook({ + status: "ready", + record: unverified, + governance: { sourceStatus: "current", validationStatus: "approved" }, + }); + render( + + {body} + , + ); + expect(screen.getByTestId("verified")).toHaveTextContent("true"); + }); + + it("live swap-in downgrades a stale fixture flag when governance is unverified", () => { + mockHook({ + status: "ready", + record: verified, + governance: { sourceStatus: "current", validationStatus: "unverified" }, + }); + render( + + {body} + , + ); + expect(screen.getByTestId("verified")).toHaveTextContent("false"); + }); + + it("passes the record through unchanged when governance is absent", () => { + mockHook({ status: "ready", record: verified, governance: null }); + render( + + {body} + , + ); + expect(screen.getByTestId("verified")).toHaveTextContent("true"); + }); + + it("renders a not-found empty state, not the record body", () => { + mockHook({ status: "not_found", record: null }); + render( + + {body} + , + ); + expect(screen.getByText(/No form record found/i)).toBeInTheDocument(); + expect(screen.queryByTestId("title")).not.toBeInTheDocument(); + }); + + it("renders the session-expired panel when unauthorized", () => { + mockHook({ status: "unauthorized", record: null }); + render( + + {body} + , + ); + expect(screen.getByText("Session expired")).toBeInTheDocument(); + }); +});