From b4f3b95bd0114fd3ec136ff16acfff650fda3456 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:38:38 +0800 Subject: [PATCH 1/2] fix(differentials): wire selected compare to workflow --- .../clinical-dashboard/differentials-home.tsx | 28 ++++++------------- src/lib/differentials-navigation.ts | 20 +++++++++++++ tests/differentials-navigation.test.ts | 20 +++++++++++++ 3 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 src/lib/differentials-navigation.ts create mode 100644 tests/differentials-navigation.test.ts diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index dcc9caa09..3042f07ba 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -30,6 +30,7 @@ import { useDifferentialSearch } from "@/components/clinical-dashboard/use-diffe import { useResultSort } from "@/components/use-result-sort"; import { cn } from "@/components/ui-primitives"; import { appModeHomeHref } from "@/lib/app-modes"; +import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation"; import { differentialsMobileCompareAddonSlotId } from "@/lib/mode-home-composer"; import { composeDifferentialSearchResults, @@ -111,17 +112,6 @@ const candidateIconBySlug: Array<[string, LucideIcon]> = [ ["delirium", BrainCircuit], ]; -function routeWithQuery(path: string, query: string, selectedIds?: Set) { - const params = new URLSearchParams(); - const trimmedQuery = query.trim(); - if (trimmedQuery) params.set("q", trimmedQuery); - if (selectedIds && selectedIds.size > 0) { - params.set("ids", Array.from(selectedIds).join(",")); - } - const suffix = params.toString(); - return suffix ? `${path}?${suffix}` : path; -} - /** * Mobile/tablet floating compare action. Portals into the search composer's * addon slot so it stays anchored beside the active result controls, but @@ -162,7 +152,7 @@ function DifferentialsMobileCompareBar({
{hasSelection ? ( @@ -599,7 +589,7 @@ function SafetyCard({ safety, query }: { safety: string; query: string }) {

{safety}

View presentation guide @@ -929,14 +919,14 @@ function SearchResultsView({

Browse presentations Browse diagnoses @@ -1097,7 +1087,7 @@ function SearchResultsView({
View all catalogue matches ({results.length}) @@ -1106,7 +1096,7 @@ function SearchResultsView({ {selectedCount > 0 ? ( @@ -1205,12 +1195,12 @@ export function DifferentialsHome({ function handleAction(action: DifferentialAction) { if (action.target === "presentations") { if (onOpenPresentations) onOpenPresentations(action.query); - else router.push(routeWithQuery("/differentials/presentations", action.query)); + else router.push(differentialRouteWithQuery("/differentials/presentations", action.query)); return; } if (action.target === "diagnoses") { if (onOpenDiagnoses) onOpenDiagnoses(action.query); - else router.push(routeWithQuery("/differentials/diagnoses", action.query)); + else router.push(differentialRouteWithQuery("/differentials/diagnoses", action.query)); return; } runSearch(action.query); diff --git a/src/lib/differentials-navigation.ts b/src/lib/differentials-navigation.ts new file mode 100644 index 000000000..cbd465a18 --- /dev/null +++ b/src/lib/differentials-navigation.ts @@ -0,0 +1,20 @@ +import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials"; + +export function differentialRouteWithQuery(path: string, query: string, selectedIds?: Iterable) { + const params = new URLSearchParams(); + const trimmedQuery = query.trim(); + if (trimmedQuery) params.set("q", trimmedQuery); + const ids = selectedIds ? Array.from(selectedIds, (id) => id.trim()).filter(Boolean) : []; + if (ids.length > 0) params.set("ids", ids.join(",")); + const suffix = params.toString(); + return suffix ? `${path}?${suffix}` : path; +} + +export function differentialSelectedCompareHref(query: string, selectedIds: Iterable) { + const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds); + return differentialRouteWithQuery( + `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`, + query, + selection?.diagnosisIds, + ); +} diff --git a/tests/differentials-navigation.test.ts b/tests/differentials-navigation.test.ts new file mode 100644 index 000000000..42e2dee0f --- /dev/null +++ b/tests/differentials-navigation.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation"; + +describe("differentials navigation", () => { + it("builds same-origin relative query routes", () => { + expect(differentialRouteWithQuery("/differentials/diagnoses", " acute confusion ")).toBe( + "/differentials/diagnoses?q=acute+confusion", + ); + }); + + it("wires selected comparison directly to a presentation page instead of the redirect endpoint", () => { + const href = differentialSelectedCompareHref("Pain", new Set(["anorexia-nervosa", "bulimia-nervosa-binge-purge-pattern"])); + + expect(href).toMatch(/^\/differentials\/presentations\/[^?]+\?/); + expect(href).toContain("q=Pain"); + expect(href).toContain("ids=anorexia-nervosa%2Cbulimia-nervosa-binge-purge-pattern"); + expect(href).not.toContain("0.0.0.0"); + }); +}); From 78ceaa9a79e9b154eafe5b0ecf4a9f08f8105a55 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:46:35 +0000 Subject: [PATCH 2/2] fix(differentials): keep compare href helpers snapshot-free Resolve presentation workflow on the server redirect with a relative Location header so Compare selected keeps IDs without pulling the differentials snapshot into the client bundle. Co-authored-by: BigSimmo --- src/app/differentials/presentations/route.ts | 26 +++++++++----- src/lib/differentials-navigation.ts | 18 ++++++---- .../audit-navigation-auth-regressions.test.ts | 2 +- tests/differentials-navigation.test.ts | 36 ++++++++++++++++--- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/app/differentials/presentations/route.ts b/src/app/differentials/presentations/route.ts index 001912f0f..282b0555c 100644 --- a/src/app/differentials/presentations/route.ts +++ b/src/app/differentials/presentations/route.ts @@ -2,20 +2,30 @@ import { type NextRequest, NextResponse } from "next/server"; import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials"; -export function GET(request: NextRequest) { +function presentationsRedirectLocation(request: NextRequest) { 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()) .filter(Boolean); const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds); - const destination = new URL( - `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`, - request.url, - ); - if (query) destination.searchParams.set("q", query); - if (selection?.diagnosisIds.length) destination.searchParams.set("ids", selection.diagnosisIds.join(",")); - return NextResponse.redirect(destination); + const params = new URLSearchParams(); + if (query) params.set("q", query); + if (selection?.diagnosisIds.length) params.set("ids", selection.diagnosisIds.join(",")); + const pathname = `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`; + const suffix = params.toString(); + // Relative Location so redirects stay same-origin in the browser even when + // the server request URL uses a bind address like 0.0.0.0. + return suffix ? `${pathname}?${suffix}` : pathname; +} + +export function GET(request: NextRequest) { + return new NextResponse(null, { + status: 307, + headers: { + Location: presentationsRedirectLocation(request), + }, + }); } export const HEAD = GET; diff --git a/src/lib/differentials-navigation.ts b/src/lib/differentials-navigation.ts index cbd465a18..592bc99e7 100644 --- a/src/lib/differentials-navigation.ts +++ b/src/lib/differentials-navigation.ts @@ -1,4 +1,8 @@ -import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials"; +/** + * Client-safe differentials href helpers. + * Keep this module free of `@/lib/differentials` / snapshot imports so the + * clinical-dashboard client bundle stays fixture-free. + */ export function differentialRouteWithQuery(path: string, query: string, selectedIds?: Iterable) { const params = new URLSearchParams(); @@ -10,11 +14,11 @@ export function differentialRouteWithQuery(path: string, query: string, selected return suffix ? `${path}?${suffix}` : path; } +/** + * Compare-selected CTA href. Resolves the presentation workflow on the server + * via `/differentials/presentations` (see presentations/route.ts) so the client + * never loads the differentials snapshot just to build a link. + */ export function differentialSelectedCompareHref(query: string, selectedIds: Iterable) { - const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds); - return differentialRouteWithQuery( - `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`, - query, - selection?.diagnosisIds, - ); + return differentialRouteWithQuery("/differentials/presentations", query, selectedIds); } diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 9e2bbb840..813cc4f59 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -40,7 +40,7 @@ describe("audit navigation and auth regressions", () => { ); expect(presentations.status).toBe(307); expect(presentations.headers.get("location")).toBe( - "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium", + "/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium", ); const medications = redirectMedications(new NextRequest("https://clinical-kb.test/medications?ignored=1")); diff --git a/tests/differentials-navigation.test.ts b/tests/differentials-navigation.test.ts index 42e2dee0f..4dfa3642c 100644 --- a/tests/differentials-navigation.test.ts +++ b/tests/differentials-navigation.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; +import { NextRequest } from "next/server"; +import { GET as redirectPresentations } from "@/app/differentials/presentations/route"; import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation"; describe("differentials navigation", () => { @@ -9,12 +11,36 @@ describe("differentials navigation", () => { ); }); - it("wires selected comparison directly to a presentation page instead of the redirect endpoint", () => { - const href = differentialSelectedCompareHref("Pain", new Set(["anorexia-nervosa", "bulimia-nervosa-binge-purge-pattern"])); + it("keeps compare-selected hrefs client-safe and ID-preserving without resolving the workflow locally", () => { + const href = differentialSelectedCompareHref( + "Pain", + new Set(["anorexia-nervosa", "bulimia-nervosa-binge-purge-pattern"]), + ); - expect(href).toMatch(/^\/differentials\/presentations\/[^?]+\?/); - expect(href).toContain("q=Pain"); - expect(href).toContain("ids=anorexia-nervosa%2Cbulimia-nervosa-binge-purge-pattern"); + expect(href).toBe("/differentials/presentations?q=Pain&ids=anorexia-nervosa%2Cbulimia-nervosa-binge-purge-pattern"); expect(href).not.toContain("0.0.0.0"); + expect(href).not.toMatch(/^https?:\/\//); + }); + + it("does not import the differentials snapshot module from the client-safe navigation helpers", async () => { + const { readFileSync } = await import("node:fs"); + const source = readFileSync(new URL("../src/lib/differentials-navigation.ts", import.meta.url), "utf8"); + expect(source).not.toMatch(/from\s+["']@\/lib\/differentials["']/); + expect(source).not.toMatch(/differentials-snapshot|loadDifferentialSnapshot/); + }); + + it("redirects compare selection with a relative Location even when the request host is a bind address", () => { + const response = redirectPresentations( + new NextRequest( + "http://0.0.0.0:4461/differentials/presentations?q=Pain&ids=anorexia-nervosa,bulimia-nervosa-binge-purge-pattern", + ), + ); + + expect(response.status).toBe(307); + const location = response.headers.get("location"); + expect(location).toMatch(/^\/differentials\/presentations\/[^/?]+/); + expect(location).toContain("q=Pain"); + expect(location).toContain("ids="); + expect(location).not.toContain("0.0.0.0"); }); });