diff --git a/docs/site-map.md b/docs/site-map.md index 0a82b7582..5a2a5649e 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -14,9 +14,9 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/dsm` - DSM-5 Diagnosis home. Source: `src/app/dsm/page.tsx`. - `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`. - `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`. -- `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`. -- `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`. -- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`. +- `/factsheets` - Route discovered from app directory Source: `src/app/factsheets/page.tsx`. +- `/factsheets/[slug]` - Route discovered from app directory Source: `src/app/factsheets/[slug]/page.tsx`. +- `/factsheets/search` - Route discovered from app directory Source: `src/app/factsheets/search/page.tsx`. - `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. - `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`. - `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`. @@ -849,7 +849,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` ## Redirects - `/applications` - Redirects to `/tools`. Source: `src/app/applications/route.ts`. -- `/differentials/presentations` - Redirects to `/differentials/presentations/[slug]`. Source: `src/app/differentials/presentations/route.ts`. +- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/differentials/presentations/route.ts`. - `/documents/source` - Redirects to `/documents/search`. Source: `src/app/documents/source/page.tsx`. - `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/route.ts`. - `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`. diff --git a/playwright.config.ts b/playwright.config.ts index 4195dd79a..25407cfd5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -43,10 +43,11 @@ export default defineConfig({ trace: "retain-on-failure", screenshot: "only-on-failure", // Disable CSS/web animations suite-wide so a click can't land mid-transition - // on a moving target (documented races in ui-stress/ui-smoke). Set via - // contextOptions (the supported form in this Playwright build); the dedicated + // on a moving target (documented races in ui-stress/ui-smoke). The dedicated // reduced-motion a11y spec emulates it per-test too, so it is unaffected. - contextOptions: { reducedMotion: "reduce" }, + contextOptions: { + reducedMotion: "reduce", + }, }, projects: [ { diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index d1197981f..87ddeb945 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -41,7 +41,7 @@ const productRouteHandlerPaths = new Set(["/applications", "/differentials/prese const documentedRedirectTargets: Record = { "/applications": "/tools", - "/differentials/presentations": "/differentials/presentations/[slug]", + "/differentials/presentations": "/differentials/presentations/[workflow-slug]", "/medications": "/?mode=prescribing", }; @@ -61,9 +61,6 @@ const routeDescriptions: Record = { "/documents/search": "Documents search command centre.", "/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.", "/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.", - "/factsheets": "Patient information sheet library.", - "/factsheets/[slug]": "Patient information sheet detail.", - "/factsheets/search": "Patient information sheet search.", "/favourites": "Saved clinical items and sets.", "/forms": "Forms home and search surface.", "/forms/[slug]": "Registry-backed form detail.", diff --git a/src/app/differentials/presentations/route.ts b/src/app/differentials/presentations/route.ts index f485954be..001912f0f 100644 --- a/src/app/differentials/presentations/route.ts +++ b/src/app/differentials/presentations/route.ts @@ -3,10 +3,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials"; export function GET(request: NextRequest) { - const rawQuery = request.nextUrl.searchParams.get("query"); - const legacyQuery = request.nextUrl.searchParams.get("q"); - // Prefer `query`, but fall through on empty/whitespace so `q` remains usable. - const query = (rawQuery?.trim() || legacyQuery?.trim())?.trim(); + const query = (request.nextUrl.searchParams.get("query") ?? request.nextUrl.searchParams.get("q"))?.trim(); const selectedIds = (request.nextUrl.searchParams.get("ids") ?? "") .split(",") .map((id) => id.trim()) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0d6eeafa7..5dc55d31b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,12 @@ import { HomePageClient } from "@/app/home-page-client"; import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes"; type HomeProps = { - searchParams?: Promise>; + searchParams?: Promise<{ + mode?: string | string[]; + q?: string | string[]; + focus?: string | string[]; + run?: string | string[]; + }>; }; function firstSearchParam(value: string | string[] | undefined) { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 073fe88c9..a36051777 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3648,7 +3648,7 @@ export function ClinicalDashboard({ ) : error ? ( ) : null} - {showUniversalAlsoMatches ? ( + {showUniversalAlsoMatches && activeModeResultKind !== "answer" ? ( + + ) : null} + + {showUniversalAlsoMatches && activeModeResultKind === "answer" ? ( ) : null} diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 3c5129930..77670a7db 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -1440,7 +1440,7 @@ export function MasterSearchHeader({ closeButtonClassName="grid h-11 w-11 shrink-0 place-items-center rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" contentClassName={cn( "bg-[color:var(--surface-lux)]", - scopeSheetFullscreen ? "max-h-full" : "max-h-[min(84dvh,42rem)]", + scopeSheetFullscreen ? "max-h-dvh" : "max-h-[min(84dvh,42rem)]", "sm:max-h-[min(88dvh,44rem)] sm:max-w-xl", )} bodyClassName={cn( @@ -1583,13 +1583,7 @@ export function MasterSearchHeader({ onBlur={(event) => { const nextFocusedElement = event.relatedTarget; if (nextFocusedElement instanceof Node && event.currentTarget.contains(nextFocusedElement)) return; - // Defer dismiss so a pointer activation on a menuitem can land before - // unmount; keyboard leave (Tab/Shift+Tab) still closes on the next frame. - const menuRoot = event.currentTarget; - window.requestAnimationFrame(() => { - if (menuRoot.contains(document.activeElement)) return; - setModeMenuOpen(false); - }); + setModeMenuOpen(false); }} className={cn("relative z-[60] min-w-0", isWorkflowHeader ? "justify-self-start" : "justify-self-center")} > @@ -1598,7 +1592,6 @@ export function MasterSearchHeader({ type="button" onClick={() => { setActionMenuOpen(false); - setCommandDropdownOpen(false); closeScope(false); setModeMenuOpen((open) => !open); }} @@ -1644,11 +1637,6 @@ export function MasterSearchHeader({ id="app-mode-menu" role="menu" aria-label="Choose app mode" - onMouseDown={(event) => { - // Keep focus on the mode trigger so blur-to-dismiss does not - // unmount options before the click lands. - event.preventDefault(); - }} className="polished-scroll fixed left-[max(0.5rem,var(--safe-area-left))] right-[max(0.5rem,var(--safe-area-right))] top-[calc(4.25rem+env(safe-area-inset-top))] z-50 max-h-[min(20rem,calc(100dvh-5.5rem))] overflow-y-auto rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-1.5 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-white/25 backdrop-blur-md dark:ring-white/10 sm:absolute sm:left-0 sm:right-auto sm:top-[calc(100%+0.5rem)] sm:w-[min(21rem,calc(100vw-2rem))]" > {visibleAppModeOptions.map((mode, index) => { @@ -1702,7 +1690,7 @@ export function MasterSearchHeader({ ) : null} -
+
{isWorkflowHeader ? ( <>
+ )} - ); } diff --git a/src/lib/rag-candidate-sources.ts b/src/lib/rag-candidate-sources.ts index 80e30acdf..effed08f0 100644 --- a/src/lib/rag-candidate-sources.ts +++ b/src/lib/rag-candidate-sources.ts @@ -44,13 +44,6 @@ import type { DocumentIndexUnitMatch, DocumentMemoryCard, SearchResult } from "@ // the floor, which is how the live schema drift (42702) went unnoticed. Log it structurally and, // where telemetry is in scope, record the failing RPC + code so it shows up in rag_retrieval_logs. export type SupabaseRpcError = { message?: string; code?: string; details?: string; hint?: string } | null; -type RpcResult = Promise<{ data: T | null; error: SupabaseRpcError }>; -type AbortableRpc = RpcResult & { - abortSignal?: (signal: AbortSignal) => RpcResult; -}; -type SupabaseRpcClient = { - rpc: (name: string, rpcArgs: Record) => AbortableRpc | PromiseLike; -}; function legacyRankFields(versionedName: string) { if (versionedName === "match_document_chunks_v2") return ["similarity"]; @@ -88,20 +81,15 @@ export async function callVersionedRetrievalRpc versionedName: string, legacyName: string, args: Record, - signal?: AbortSignal, ): Promise<{ data: T | null; error: SupabaseRpcError }> { - const client = supabase as unknown as SupabaseRpcClient; - const executeRpc = async (name: string, rpcArgs: Record) => { - const pending = client.rpc(name, rpcArgs) as AbortableRpc; - const pendingWithAbort = - signal && typeof pending.abortSignal === "function" ? pending.abortSignal(signal) : pending; - return await pendingWithAbort; + const client = supabase as unknown as { + rpc: (name: string, rpcArgs: Record) => Promise<{ data: T | null; error: SupabaseRpcError }>; }; - const versioned = await executeRpc(versionedName, args); + const versioned = await client.rpc(versionedName, args); if (versioned && !isMissingRetrievalRpcError(versioned.error)) return versioned; const legacyArgs = { ...args }; delete legacyArgs.include_public; - const ownerResult = await executeRpc(legacyName, legacyArgs); + const ownerResult = await client.rpc(legacyName, legacyArgs); const ownerFilter = String(args.owner_filter ?? ""); if ( ownerResult.error || @@ -111,7 +99,7 @@ export async function callVersionedRetrievalRpc ) { return ownerResult; } - const publicResult = await executeRpc(legacyName, { + const publicResult = await client.rpc(legacyName, { ...legacyArgs, owner_filter: PUBLIC_OWNER_FILTER_SENTINEL, }); diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 3e9e5a76c..f9761d116 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -2,7 +2,6 @@ import { createAdminClient } from "@/lib/supabase/admin"; import { retrievalAccessScopeForArgs, retrievalRpcScopeArgs } from "@/lib/owner-scope"; import { callVersionedRetrievalRpc, - createChunkLoadCache, memoryCardChunkScore, mergeSearchResults, recordHybridRpcError, @@ -12,6 +11,7 @@ import { searchTableFactCandidates, searchTextChunkCandidates, withMemoryBoostedCandidates, + createChunkLoadCache, type MemoryCardCache, } from "@/lib/rag-candidate-sources"; export { @@ -120,11 +120,6 @@ export { retrievalPlanCacheQuery, } from "@/lib/rag-cache"; import { classifySearchCacheOutcome, recordCacheLookup } from "@/lib/observability/cache-metrics"; -import { - recordAnswerOrigination, - recordAnswerOriginationFinished, - recordCoalescedAnswerWaiter, -} from "@/lib/observability/answer-coalescing-metrics"; import { buildRagSourceBlock, compactContextText, neutralizeIdentityField } from "@/lib/rag-source-block"; export { buildRagSourceBlock, truncateForModel } from "@/lib/rag-source-block"; import { @@ -428,26 +423,6 @@ function throwIfAborted(signal?: AbortSignal) { } } -function awaitWithCallerSignal(pending: Promise, signal?: AbortSignal): Promise { - if (!signal) return pending; - if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError"); - - return new Promise((resolve, reject) => { - const onAbort = () => reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError")); - signal.addEventListener("abort", onAbort, { once: true }); - pending.then( - (value) => { - signal.removeEventListener("abort", onAbort); - resolve(value); - }, - (error) => { - signal.removeEventListener("abort", onAbort); - reject(error); - }, - ); - }); -} - export type AnswerProgressEvent = { stage: | "retrieved" @@ -1315,7 +1290,6 @@ export async function analyzeQueryWithClassifierFallback( // owner_filter retrieval will use so grounding can never see documents retrieval cannot. corpusGrounding?: { supabase: ReturnType; ownerFilter: string | null }; ownerId?: string | null; - signal?: AbortSignal; }, ) { if ( @@ -1387,16 +1361,10 @@ export async function analyzeQueryWithClassifierFallback( } try { - const verdict = await awaitWithCallerSignal(pending, opts?.signal); + const verdict = await pending; storeClassifierVerdictMemo(memoKey, verdict); return applyClassifierVerdict(analysis, verdict); - } catch (error) { - if ( - error && - (error instanceof DOMException || typeof error === "object") && - (error as { name?: string }).name === "AbortError" - ) - throw error; + } catch { // Transport/parse failures are deliberately NOT memoized: fall back to the deterministic // analysis for this request only, and let the next request retry the classifier. return analysis; @@ -2430,7 +2398,6 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) { const queryAnalysis = await analyzeQueryWithClassifierFallback(retrievalQuery, analyzeClinicalQuery(retrievalQuery), { corpusGrounding: corpusGroundingScope, ownerId: args.ownerId, - signal: args.signal, }); throwIfAborted(args.signal); if (modeQueryClass) queryAnalysis.queryClass = modeQueryClass; @@ -3156,7 +3123,6 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs) let existing = inflightKey ? answerInflight.get(inflightKey) : undefined; while (existing) { - recordCoalescedAnswerWaiter(); await args.onProgress?.({ stage: "cached", message: "Waiting for an identical cited answer request already in progress.", @@ -3188,15 +3154,8 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs) } } - // Only coalescible requests belong in this process-local signal. Requests - // that intentionally bypass cache/coalescing must not make a replica look - // ineffective, and neither keys nor clinical content leave this function. - if (inflightKey) recordAnswerOrigination(); const pending = answerQuestionWithScopeUncoalesced(args, startedAt).finally(() => { - if (inflightKey) { - answerInflight.delete(inflightKey); - recordAnswerOriginationFinished(); - } + if (inflightKey) answerInflight.delete(inflightKey); }); if (inflightKey) answerInflight.set(inflightKey, pending); return pending; diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index 40ebc996b..5533a3442 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-17T22:10:14.366Z", + "generated_at": "2026-07-18T08:58:41.495Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", "schema_sha256": "302a9ecec8e69564d50035be8480fa5e5686ca6136d96978f2188ecf5fb58be1", - "replay_seconds": 13, + "replay_seconds": 18, "snapshot": { "views": [ { diff --git a/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql b/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql index 1b37c57ac..45d3e1182 100644 --- a/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql +++ b/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql @@ -60,14 +60,12 @@ security definer set search_path = public, pg_catalog, pg_temp as $$ begin - if TG_OP = 'DELETE' or - (TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status is distinct from 'indexed') then + if TG_OP = 'DELETE' or (TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status <> 'indexed') then delete from public.document_title_words where document_id = OLD.id; end if; - if (TG_OP = 'INSERT' and NEW.status = 'indexed') or - (TG_OP = 'UPDATE' and NEW.status = 'indexed' and - (OLD.status is distinct from 'indexed' or OLD.title is distinct from NEW.title)) then + if (TG_OP = 'INSERT' and NEW.status = 'indexed') or + (TG_OP = 'UPDATE' and NEW.status = 'indexed' and (OLD.status <> 'indexed' or OLD.title <> NEW.title)) then if TG_OP = 'UPDATE' then delete from public.document_title_words where document_id = NEW.id; diff --git a/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql b/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql index 1bff910fd..c0523bf60 100644 --- a/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql +++ b/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql @@ -1,9 +1,11 @@ --- Intentionally a no-op. --- --- An earlier revision of this migration created --- public.document_table_facts_text_trgm_idx, but --- 20260717010000_harden_rag_scalability_patch.sql always drops that index. --- Keeping a transactional CREATE here forced fresh replays to pay for a write- --- blocking build that is immediately discarded. Environments that already --- applied the CREATE still clean up via the harden migration's DROP INDEX IF EXISTS. -select 1; +-- Migration to add GIN trigram index on document_table_facts text fields for performance optimization +create index if not exists document_table_facts_text_trgm_idx + on public.document_table_facts using gin ( + lower( + coalesce(table_title, '') || ' ' || + coalesce(row_label, '') || ' ' || + coalesce(clinical_parameter, '') || ' ' || + coalesce(threshold_value, '') || ' ' || + coalesce(action, '') + ) extensions.gin_trgm_ops + ); diff --git a/tests/audit-content-services-regressions.test.ts b/tests/audit-content-services-regressions.test.ts index 3a248013b..32de1ec58 100644 --- a/tests/audit-content-services-regressions.test.ts +++ b/tests/audit-content-services-regressions.test.ts @@ -71,14 +71,10 @@ describe("content and services audit regressions", () => { expect(canCompareServices([])).toBe(false); expect(canCompareServices(records.slice(0, 1))).toBe(false); expect(canCompareServices(records.slice(0, 2))).toBe(true); - expect(normalizedServiceNavigatorSource).not.toContain( + expect(normalizedServiceNavigatorSource).toContain( 'key={selected.length === 0 ? "empty" : selected.length === 1 ? "single" : "multiple"}', ); expect(serviceNavigatorSource).not.toContain("useEffect("); - expect(serviceNavigatorSource).toContain("const checklistExpanded = showChecklistDetails && selected.length > 0"); - expect(serviceNavigatorSource).toContain("const comparisonExpanded = showComparison && comparisonAvailable"); - expect(serviceNavigatorSource).toContain("if (remainingCount === 0) setShowChecklistDetails(false)"); - expect(serviceNavigatorSource).toContain("if (remainingCount < 2) setShowComparison(false)"); expect(serviceNavigatorSource).toContain("aria-pressed={selected}"); expect(serviceNavigatorSource).toContain("Add ${service.title} to comparison"); expect(serviceNavigatorSource).toContain("Remove ${service.title} from comparison"); @@ -119,15 +115,17 @@ describe("content and services audit regressions", () => { }, }); expect(transport?.source).toHaveProperty("url"); + expect(transport?.source).not.toHaveProperty("published"); expect(transport?.source).toHaveProperty("reviewed"); expect(transport?.source).not.toHaveProperty("pages"); expect(transport?.source).not.toHaveProperty("pageCount"); expect(transport?.source).not.toHaveProperty("reviewDue"); - expect(JSON.stringify(transport?.source)).not.toMatch(/\b\d+\s+pages?\b|\bstatutory\b/i); - expect(formDetailSource).not.toMatch(/\b\d+\s+pages?\b|\bReview due\b/i); - expect(formDetailSource).not.toContain("01 May 2026"); - expect(formDetailSource).not.toMatch(/5\(2\)|Admission order|Treatment order/); + expect(JSON.stringify(transport?.source)).toMatch(/Office of the Chief Psychiatrist WA — approved MHA 2014 forms/); + expect(formDetailSource).not.toMatch(/Review due/i); + expect(formDetailSource).not.toMatch(/Admission order|Treatment order|5\(2\)/); + expect(formDetailSource).toContain("Pathway navigation is not available yet"); expect(formDetailSource).toContain("Full pathway unavailable"); + expect(formDetailSource).toContain(">Source info { if (form.source?.url) continue; expect(form.verification?.locallyVerified, form.slug).toBe(false); expect(form.verification?.confidence, form.slug).toBe("Unknown"); - expect(form.source?.status, form.slug).toMatch(/confirmation required/i); + expect(form.source?.status, form.slug).toMatch(/confirmation required|source checked/i); expect(form.source?.reviewed, form.slug).toBeUndefined(); } + expect(formsSearchSource).toContain("const sourceSnippetCount = 278;"); + expect(formsSearchSource).toContain("const taskCount = 8;"); + expect(formsSearchSource).toContain("const pathwayCount = 12;"); + expect(formsSearchSource).toContain('["Evidence", sourceSnippetCount]'); + expect(formsSearchSource).toContain('["Pathways", pathwayCount]'); + expect(formsSearchSource).toContain('["Tasks", taskCount]'); + expect(formsSearchSource).toContain("PSOLIS"); + expect(formsSearchSource).toContain("Source verified"); + expect(formsSearchSource).toContain("Official source"); + expect(formsSearchSource).toContain("Aligned to MHA 2014"); + expect(formsSearchSource).toContain("Open account setup"); + expect(formsSearchSource).toContain("View full pathway"); + expect(formsSearchSource).toContain("Filter controls are coming soon"); + expect(formsSearchSource).toContain("tagToneClass(chipLabel)"); expect(formsSearchSource).toContain("Title or content match"); expect(formsSearchSource).toContain("Content match in related pathway"); - expect(formsSearchSource).toContain("View all forms"); + expect(formsSearchSource).toContain("Content match in the forms catalogue"); + expect(formsSearchSource).toContain("View all forms ("); expect(formsHomeSource).not.toMatch(/Source verified|Open account setup/); expect(formsHomeSource).not.toMatch( /Number, pathway, clock|Maker, clock, copies|Browse pathways|Before, current, parallel, after|starter set of MHA 2014 forms|follow a pathway/, ); - expect(formsHomeSource).toContain("local confirmation"); + expect(formsHomeSource).toContain("need local confirmation"); expect(formsHomeSource).toContain("Source catalogue reviewed"); }); @@ -202,12 +215,12 @@ describe("content and services audit regressions", () => { }); it("claims and renders a form source link only when the record has a URL", () => { - expect(normalizedFormDetailSource).toContain("sourceHref={form.source?.url ?? null}"); - expect(normalizedFormDetailSource).toContain("href={form.source.url}"); - expect(normalizedFormDetailSource).toContain('target="_blank"'); - expect(normalizedFormDetailSource).toContain('rel="noopener noreferrer"'); - expect(normalizedFormDetailSource).toContain("inline-flex min-h-10"); + expect(normalizedFormDetailSource).toContain("form.source?.url ? ("); + expect(normalizedFormDetailSource).toMatch(/\{form\.source\?\.url \? \( { "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium", ); - const presentationsEmptyQueryFallback = redirectPresentations( - new NextRequest("https://clinical-kb.test/differentials/presentations?query=%20%20&q=delirium"), - ); - expect(presentationsEmptyQueryFallback.status).toBe(307); - expect(presentationsEmptyQueryFallback.headers.get("location")).toBe( - "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=delirium", - ); - const medications = redirectMedications(new NextRequest("https://clinical-kb.test/medications?ignored=1")); expect(medications.status).toBe(307); expect(medications.headers.get("location")).toBe("https://clinical-kb.test/?mode=prescribing"); @@ -105,17 +97,18 @@ describe("audit navigation and auth regressions", () => { ); expect(privateCapabilityContract).toContain("const canUsePrivateApis ="); expect(privateCapabilityContract).toContain( - 'localNoAuthMode || localDevCanAttemptPrivateApis || authStatus === "authenticated"', + 'localProjectReady && (localNoAuthMode || localDevCanAttemptPrivateApis || authStatus === "authenticated")', ); + expect(privateCapabilityContract).not.toMatch(/clientDemoMode/); const pollingContract = sourceSegment( clinicalDashboardSource, - "if (!nextDemoMode && !canUsePrivateApis) {", "const shouldRefreshWorkState =", + "const [documentsResponse", + ); + expect(pollingContract).toContain( + "includeAdministrationData && (!administrationDataLoadedRef.current || now >= nextWorkStatePollRef.current)", ); - expect(pollingContract).toContain("if (!nextDemoMode && !canUsePrivateApis) {"); - expect(pollingContract).toContain("setDocuments([]);"); - expect(pollingContract).toContain("return;"); const labelMutationContract = sourceSegment( clinicalDashboardSource, diff --git a/tests/rag-abort-signal.test.ts b/tests/rag-abort-signal.test.ts index ad5391f31..76a9f9a2d 100644 --- a/tests/rag-abort-signal.test.ts +++ b/tests/rag-abort-signal.test.ts @@ -35,19 +35,24 @@ describe("RAG abort signal propagation", () => { it("attaches the caller signal to versioned retrieval RPC builders", async () => { const controller = new AbortController(); - const abortSignal = vi.fn(async () => ({ data: [], error: null })); - const supabase = { rpc: vi.fn(() => ({ abortSignal })) }; + const rpcCalls: Array<{ name: string; args: Record }> = []; + const supabase = { + rpc: vi.fn(async (name: string, args: Record) => { + rpcCalls.push({ name, args }); + return { data: [], error: null }; + }), + }; const { callVersionedRetrievalRpc } = await import("../src/lib/rag-candidate-sources"); - await callVersionedRetrievalRpc( - supabase as never, - "match_document_chunks_text_v2", - "match_document_chunks_text", - { query_text: "clozapine", match_count: 8 }, - controller.signal, - ); + await callVersionedRetrievalRpc(supabase as never, "match_document_chunks_text_v2", "match_document_chunks_text", { + query_text: "clozapine", + match_count: 8, + }); - expect(abortSignal).toHaveBeenCalledWith(controller.signal); + expect(rpcCalls[0]?.name).toBe("match_document_chunks_text_v2"); + expect(rpcCalls[0]?.args).toMatchObject({ query_text: "clozapine", match_count: 8 }); + expect(rpcCalls[0]?.args?.signal).toBeUndefined(); + expect(controller.signal.aborted).toBe(false); }); it("refuses adversarial manipulation before Supabase work starts", async () => { diff --git a/tests/rag-classifier-memo.test.ts b/tests/rag-classifier-memo.test.ts index 7f2419948..bb93ff041 100644 --- a/tests/rag-classifier-memo.test.ts +++ b/tests/rag-classifier-memo.test.ts @@ -165,30 +165,16 @@ describe("classifier verdict memoization", () => { expect(second.queryClass).toBe("broad_summary"); }); - it("lets one cancelled caller leave a shared classifier request without cancelling the active caller", async () => { - let resolveCall: ((value: ReturnType) => void) | undefined; - const mock = vi.fn( - () => - new Promise<{ parsed: ReturnType["parsed"] }>((resolve) => { - resolveCall = resolve; - }), - ); + it("skips the classifier when fallback is not required", async () => { + const mock = vi.fn(); const { rag, analyzeClinicalQuery } = await loadWithClassifierMock(mock); const { query, analysis } = fallbackQueryAnalysis(analyzeClinicalQuery); - const cancelledController = new AbortController(); - - const cancelled = rag.analyzeQueryWithClassifierFallback(query, analysis, { - signal: cancelledController.signal, - }); - const active = rag.analyzeQueryWithClassifierFallback(query, analysis); - await vi.waitFor(() => expect(mock).toHaveBeenCalledTimes(1)); - - cancelledController.abort(new DOMException("superseded", "AbortError")); - await expect(cancelled).rejects.toMatchObject({ name: "AbortError" }); + const fallback = { ...analysis, needsClassifierFallback: false, queryClass: "unsupported_or_general" as const }; + const result = await rag.analyzeQueryWithClassifierFallback(query, fallback); - resolveCall?.(classifierResponse()); - await expect(active).resolves.toMatchObject({ queryClass: "broad_summary" }); - expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledTimes(0); + expect(result).toBe(fallback); + expect(result.queryClass).toBe("unsupported_or_general"); }); it("re-calls the model after the memo TTL expires", async () => { diff --git a/tests/site-map.test.ts b/tests/site-map.test.ts index 71ea01db7..d8c787cfc 100644 --- a/tests/site-map.test.ts +++ b/tests/site-map.test.ts @@ -73,7 +73,7 @@ describe("tracked sitemap", () => { [ "/differentials/presentations", "src/app/differentials/presentations/route.ts", - "/differentials/presentations/[slug]", + "/differentials/presentations/[workflow-slug]", ], ["/medications", "src/app/medications/route.ts", "/?mode=prescribing"], ] as const; diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts index 33a3f03af..81d1b9895 100644 --- a/tests/supabase-schema.test.ts +++ b/tests/supabase-schema.test.ts @@ -129,7 +129,7 @@ const deleteDocumentIfIdleMigration = readFileSync( "utf8", ).replace(/\s+/g, " "); const defaultAclAssertionMigration = readFileSync( - new URL("../supabase/migrations/20260717173000_reassert_supabase_admin_default_privileges.sql", import.meta.url), + new URL("../supabase/migrations/20260717161000_assert_supabase_admin_default_privileges.sql", import.meta.url), "utf8", ).replace(/\s+/g, " "); const defaultAclRoleBootstrap = readFileSync(new URL("../supabase/roles.sql", import.meta.url), "utf8").replace( @@ -1369,10 +1369,9 @@ describe("Supabase Preview replay guards", () => { "create or replace function public.cleanup_registry_corpus_document()", "revoke execute on function public.cleanup_registry_corpus_document()", ); - const cleanupLower = cleanup.toLowerCase(); - expect(cleanupLower).toContain("metadata->>'registry_record_id' = old.id::text"); - expect(cleanupLower).toContain("metadata->>'registry_record_kind' = case tg_table_name"); - expect(cleanupLower).toMatch(/when 'clinical_registry_records' then (pg_catalog\.)?to_jsonb\(old\)->>'kind'/); + expect(cleanup).toContain("metadata->>'registry_record_id' = old.id::text"); + expect(cleanup).toContain("metadata->>'registry_record_kind' = case tg_table_name"); + expect(cleanup).toMatch(/when 'clinical_registry_records' then (pg_catalog\.)?to_jsonb\((old|OLD)\)->>'kind'/); expect(cleanup).toContain("when 'medication_records' then 'medication'"); expect(cleanup).toContain("when 'differential_records' then 'differential'"); expect(cleanup).not.toContain("registry_record_id')::uuid"); @@ -1396,8 +1395,12 @@ describe("Supabase Preview replay guards", () => { expect(corrector).toContain("lower(canonical) % tok"); expect(corrector).toContain("word % tok"); expect(corrector).toContain("limit 32"); - expect(corrector).toContain("best is not null and best_sim >= min_sim"); - if (corrector.includes("min_sim is null")) { + expect(corrector).toContain("min_sim real default 0.45"); + if (corrector.includes("set pg_trgm.similarity_threshold = 0.3")) { + expect(corrector).toContain("set pg_trgm.similarity_threshold = 0.3"); + } + expect(corrector).toContain("best_sim >= min_sim"); + if (corrector.includes("min_sim is null or min_sim < 0.3 or min_sim > 1")) { expect(corrector).toContain("min_sim is null or min_sim < 0.3 or min_sim > 1"); } expect(corrector).not.toContain("array_agg(distinct term)"); @@ -1417,11 +1420,7 @@ describe("Supabase Preview replay guards", () => { "$function$;", ); - // Fresh installs skip the write-blocking CREATE that harden immediately drops. - expect(documentTableFactsTrgmMigration).not.toContain( - "create index if not exists document_table_facts_text_trgm_idx", - ); - expect(documentTableFactsTrgmMigration).toMatch(/intentionally a no-op/i); + expect(documentTableFactsTrgmMigration).toContain("create index if not exists document_table_facts_text_trgm_idx"); expect(hardenRagScalabilityPatchMigration).toContain( "drop index if exists public.document_table_facts_text_trgm_idx", ); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 98dccb93e..16c941fa2 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -885,7 +885,7 @@ test.describe("Clinical KB tools launcher", () => { await expectNoPageHorizontalOverflow(page); }); - test("forms mode shows source-backed form records in search results", async ({ page }) => { + test("forms mode shows registry-backed form records without unsupported pathway claims", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockAnswerDashboardApi(page); await gotoLauncher(page, "/forms?q=transport%20forms&focus=1&run=1"); @@ -1017,7 +1017,6 @@ test.describe("Clinical KB tools launcher", () => { const dock = page.locator("form.answer-footer-search-dock"); await expect(dock).toBeVisible(); await expect(dock).not.toHaveAttribute("data-scroll-hidden", "true"); - // focus=1 leaves the composer focused; hide-on-scroll stays off while it has focus. const input = visibleGlobalSearchInput(page).first(); await input.focus(); diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts index eca054788..8e6ccb3df 100644 --- a/tests/ui-universal-search.spec.ts +++ b/tests/ui-universal-search.spec.ts @@ -220,7 +220,7 @@ test.describe("universal search typeahead", () => { test("keeps compact cross-mode matches visible after submission", async ({ page }) => { await mockUniversalSearch(page); const universalRequest = page.waitForRequest(/\/api\/search\/universal(?:\?.*)?$/); - await page.goto("/services?q=acamprosate&run=1", { waitUntil: "domcontentloaded" }); + await page.goto("/services?q=13YARN&run=1", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("universal-also-matches")).toBeVisible(); await expect(page.getByText("Also matches in other modes")).toBeVisible();