Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -837,5 +837,6 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0e77e7bc0842bef4ffc045c6dbea4626152490d1 | differentials diagnosis term links | implemented exact+alias termLinks chips on diagnosis+presentation pages; vitest 58/58; verify:pr-local green | vitest differential-diagnosis-links+detail+section-nav+route; verify:pr-local; ensure spot-check |
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0daa9e2f9fc84e879fd661da94203568309234a6 | PR #1768 Autopilot+Bugbot review-and-fix | Merged origin/main (DIRTY was ledger+detail-page staleness; merge-tree clean). Fixed SEGMENT_SPLIT to spaced-slash only so Delirium / medical psychosis links while alcohol/benzo, DVT/PE, food/fluid stay intact. Dispositioned: Copilot termLinks ??{} + Fragment key already fixed; CodeRabbit clean-keys moot (visibleSectionItems already cleans); CodeRabbit bare-slash split rejected (clinical harm). No Bugbot findings. Threads cleared on push. Merge left to user. | vitest differential-diagnosis-links+detail+route 49/49; verify:cheap exit 0 (543 files, 5828 passed/4 skipped); verify:pr-local exit 0 (lint/typecheck/test/build/rag-fixtures); merge-tree clean vs origin/main; no provider gates |
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | f784e81bcc0b53ef76b3da07a8e81f96d9bf0c71 | pr-1768 unblock | merged origin/main onto ba590f9; merge-tree clean; DIRTY mergeability cleared; push tip follows amend with this ledger | merge-tree clean; threads resolved; auto-merge was armed |
| 2026-08-09 | claude/document-viewer-phase-3-bj5k5v | 156db63f1b60f09791e426b043ea90d427b789ab | post-#1772 test simplification: replace the viewer perf source-text grep with behavioural coverage; de-literalise rail window and keyboard label assertions | PR #1777 opened. Self-review of #1772's own tests against an excessive-strictness challenge. Finding: the client-performance-boundaries grep for resolveLiveCanvasWindow / resolveRenderAheadPages / liveCanvasLimit / requestIdleCallback was not merely brittle, it was INEFFECTIVE - replacing the budget call with a hardcoded 3 leaves every identifier in the file, so it stayed green while the viewer retained three full-zoom canvases (measured both ways). Replaced by a DOM case that binds the budget (VIEWER_MAX_ZOOM at dpr 3 gives ~16.8M backing px against the 24M budget, window collapses to 1) and fails on exactly that substitution. Also exported RAIL_IMAGE_WINDOW so the rail test derives its counts (verified by tuning 6->8: all 7 still pass), and relaxed the keyboard aria-label assertions from exact prose to the key names. Pre-existing greps for disableAutoFetch / canvas.width = 0 / pageToCleanup left alone deliberately - two are now redundant but they are another author's guard. | verify:pr-local (1 pre-existing root-only failure: pr-handoff-stop #291; 5872 passed), build OK 80s + client bundle secret check, eval:rag:offline 36 golden cases / 574 tests, lint + typecheck clean. Sabotage-verified in both directions. Browser gates unrunnable here (#279) - unchanged by this diff. |
| 2026-08-09 | claude/planning-build-intelligence-9ot0nm | 3df3cb3993f73cda4dbbc4ac7549f84b3c6ea7ed | Node 24.15 engine floor: engines.node, preinstall hook, check:runtime, session-start provisioning, codex-cloud assertion | Authored and handed off as PR #1771; closes #285; operationalRisk true, clinicalRisk/ragRanking false | test 5800 passed/1 pre-existing root-uid failure (pr-handoff-stop, confirmed on stashed clean tree); lint 0; typecheck 0; prettier --check . pass; check:runtime pass; check:codex-cloud pass; check:outstanding-issues pass; preinstall boundary proof 24.13/24.14.9 reject, 24.15/24.19 accept, 25.0.0 reject; contract test mutation-checked red |
| 2026-08-09 | pull/1771 | 466ec4216272c31c5f754db213dbdc529583b167 | PR 1771 runtime floor enforcement | P2: Cloud and Desktop setup paths remain major-only; do not merge until range-aware | static review; check:runtime PASS; check:codex-cloud PASS; ledger PASS; outstanding issues PASS; focused Vitest blocked by active Playwright lease |
2 changes: 1 addition & 1 deletion src/components/document-viewer/source-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ export function DocumentImage({
* Six covers the great majority of indexed documents outright, so most rails
* never mount a sentinel at all.
*/
const RAIL_IMAGE_WINDOW = 6;
export const RAIL_IMAGE_WINDOW = 6;

/**
* Lookahead for the rail's own signed-URL requests, against `SignedImage`'s
Expand Down
31 changes: 14 additions & 17 deletions tests/client-performance-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,20 @@ describe("fixture-free client performance boundaries", () => {
expect(pdfViewer).not.toContain("renderedPageRef.current?.cleanup()");
});

it("bounds retained page canvases by a document-wide budget, not only the per-canvas one", () => {
const pdfViewer = source("src/components/document-viewer/pdf-canvas-viewer.tsx");

// `resolveCanvasRasterPlan` bounds ONE canvas against WebKit's ~2^24 ceiling.
// Once several pages are live at once, N individually legal canvases can still
// exhaust device memory, so the render window must come from the document-wide
// budget rather than a hardcoded page count. The DOM test cannot catch a
// regression here: its fixture canvases are small enough that the budget never
// binds, so a viewer that dropped it would still window correctly there.
expect(pdfViewer).toContain("resolveLiveCanvasWindow");
expect(pdfViewer).toContain("resolveRenderAheadPages");
expect(pdfViewer).toMatch(/liveCanvasLimit/);

// Render-ahead must stay deferred. Rendering neighbours eagerly re-introduces
// exactly the background fetching `disableAutoFetch` above exists to prevent.
expect(pdfViewer).toContain("requestIdleCallback");
});
// The document-wide canvas budget and the deferred render-ahead are covered by
// `tests/document-viewer-page-virtualization.dom.test.tsx`, behaviourally.
//
// They were briefly asserted here instead, by checking the viewer source
// contained `resolveLiveCanvasWindow` / `resolveRenderAheadPages` /
// `liveCanvasLimit`. That guard was measured against the regression it named
// and did not catch it: replacing the budget call with a hardcoded `3` leaves
// every one of those identifiers in the file, so the grep stayed green while
// the viewer retained three full-zoom canvases. The DOM test now drives the
// budget until it binds (a page at maximum zoom on a dpr-3 display costs the
// whole per-canvas ceiling) and fails on exactly that substitution.
//
// Worth generalising when adding a contract here: a source-text assertion can
// only prove a name is present, never that it is load-bearing.

it("revalidates cached document download URLs on every viewer action", () => {
const viewer = source("src/components/DocumentViewer.tsx");
Expand Down
57 changes: 36 additions & 21 deletions tests/document-rail-image-window.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ vi.mock("@/lib/supabase/client", () => {
return { useAuthSession: () => ({ authorizationHeader, markSessionExpired, session: null }) };
});

import { DocumentImageList } from "@/components/document-viewer/source-panels";
import { DocumentImageList, RAIL_IMAGE_WINDOW } from "@/components/document-viewer/source-panels";
import type { ImageRow } from "@/components/document-viewer/types";

/**
Expand Down Expand Up @@ -93,56 +93,69 @@ function reachSentinel() {

const renderedRows = () => screen.queryAllByTestId("document-image").length;

/**
* A list long enough that the window always applies, expressed against the
* window rather than as a literal.
*
* Every count below is derived from `RAIL_IMAGE_WINDOW`. Tuning the window from
* 6 to 8 is a product judgement about how much of a figure rail is worth
* mounting up front — it is not a regression, and it should not turn five
* assertions red.
*/
const LONG_LIST = RAIL_IMAGE_WINDOW * 6 + 4;

describe("document rail figure windowing", () => {
it("mounts only the first window of rows for a long figure list", () => {
render(<DocumentImageList images={images(40)} revealLabel="Tables and diagrams" />);
render(<DocumentImageList images={images(LONG_LIST)} revealLabel="Tables and diagrams" />);

expect(renderedRows()).toBe(6);
expect(renderedRows()).toBe(RAIL_IMAGE_WINDOW);
expect(screen.getByTestId("document-image-reveal")).toBeTruthy();
});

it("mounts a short list whole, with no sentinel and no reveal control", () => {
// The overwhelming majority of indexed documents are this case, so the
// window must not add chrome to them.
render(<DocumentImageList images={images(4)} revealLabel="Tables and diagrams" />);
const shortList = RAIL_IMAGE_WINDOW - 2;
render(<DocumentImageList images={images(shortList)} revealLabel="Tables and diagrams" />);

expect(renderedRows()).toBe(4);
expect(renderedRows()).toBe(shortList);
expect(screen.queryByTestId("document-image-reveal")).toBeNull();
});

it("grows by one window each time the sentinel is reached", () => {
render(<DocumentImageList images={images(40)} revealLabel="Tables and diagrams" />);
render(<DocumentImageList images={images(LONG_LIST)} revealLabel="Tables and diagrams" />);

act(() => reachSentinel());
expect(renderedRows()).toBe(12);
expect(renderedRows()).toBe(RAIL_IMAGE_WINDOW * 2);

act(() => reachSentinel());
expect(renderedRows()).toBe(18);
expect(renderedRows()).toBe(RAIL_IMAGE_WINDOW * 3);
});

it("reveals the whole list from its control, for readers who never scroll it", () => {
// Scroll-driven growth is unreachable by keyboard and by a screen reader
// walking the rail, so the control is the real affordance rather than a
// no-IntersectionObserver fallback.
render(<DocumentImageList images={images(40)} revealLabel="Tables and diagrams" />);
render(<DocumentImageList images={images(LONG_LIST)} revealLabel="Tables and diagrams" />);

const reveal = screen.getByRole("button", { name: /show the remaining 34/i });
const reveal = screen.getByRole("button", { name: /show the remaining \d+/i });
fireEvent.click(reveal);

expect(renderedRows()).toBe(40);
expect(renderedRows()).toBe(LONG_LIST);
expect(screen.queryByTestId("document-image-reveal")).toBeNull();
});

it("clamps the window when the list shrinks underneath an expanded reader", () => {
// Navigating to another document, or a reindex, can replace the array while
// the reader has already expanded past the new length.
const { rerender } = render(<DocumentImageList images={images(40)} revealLabel="Tables and diagrams" />);
fireEvent.click(screen.getByRole("button", { name: /show the remaining 34/i }));
expect(renderedRows()).toBe(40);
const { rerender } = render(<DocumentImageList images={images(LONG_LIST)} revealLabel="Tables and diagrams" />);
fireEvent.click(screen.getByRole("button", { name: /show the remaining \d+/i }));
expect(renderedRows()).toBe(LONG_LIST);

rerender(<DocumentImageList images={images(3)} revealLabel="Tables and diagrams" />);
const shrunk = RAIL_IMAGE_WINDOW - 3;
rerender(<DocumentImageList images={images(shrunk)} revealLabel="Tables and diagrams" />);

expect(renderedRows()).toBe(3);
expect(renderedRows()).toBe(shrunk);
expect(screen.queryByTestId("document-image-reveal")).toBeNull();
});

Expand All @@ -155,14 +168,16 @@ describe("document rail figure windowing", () => {

it("resets the window when remounted for another document's long list", () => {
const { rerender } = render(
<DocumentImageList key="doc-a:clinical" images={images(40)} revealLabel="Tables and diagrams" />,
<DocumentImageList key="doc-a:clinical" images={images(LONG_LIST)} revealLabel="Tables and diagrams" />,
);
fireEvent.click(screen.getByRole("button", { name: /show the remaining 34/i }));
expect(renderedRows()).toBe(40);
fireEvent.click(screen.getByRole("button", { name: /show the remaining \d+/i }));
expect(renderedRows()).toBe(LONG_LIST);

rerender(<DocumentImageList key="doc-b:clinical" images={images(40, 100)} revealLabel="Tables and diagrams" />);
rerender(
<DocumentImageList key="doc-b:clinical" images={images(LONG_LIST, 100)} revealLabel="Tables and diagrams" />,
);

expect(renderedRows()).toBe(6);
expect(renderedRows()).toBe(RAIL_IMAGE_WINDOW);
expect(screen.getByTestId("document-image-reveal")).toBeTruthy();
});
});
12 changes: 8 additions & 4 deletions tests/document-viewer-keyboard.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,13 @@ describe("PDF reader keyboard bindings", () => {
const { holder } = await renderReader();

const label = holder.getAttribute("aria-label") ?? "";
expect(label).toMatch(/Page Up and Page Down/);
expect(label).toMatch(/Home and End/);
expect(label).toMatch(/F fits the width/);
expect(label).toMatch(/R rotates/);

// A binding a screen-reader user is never told about is a binding they do
// not have, so the contract is that every key is named. The phrasing around
// the keys is copy and may be rewritten freely — assert the keys, not the
// sentence, or the next wording pass turns this red for no defect.
for (const key of ["Page Up", "Page Down", "Home", "End", "F", "R"]) {
expect(label, `the holder's accessible name does not mention ${key}`).toMatch(new RegExp(`\\b${key}\\b`));
}
});
});
29 changes: 29 additions & 0 deletions tests/document-viewer-page-virtualization.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { VIEWER_MAX_ZOOM } from "@/components/document-viewer/viewer-zoom";

/**
* Page virtualization behaviour for the PDF reader, in a real React tree.
*
Expand Down Expand Up @@ -257,6 +259,33 @@ describe("PDF reader page virtualization", () => {
expect(getPageCalls).not.toContain(6);
});

it("collapses render-ahead to the reader's page alone when one canvas costs the whole budget", async () => {
// The other half of the curve the test above measures.
//
// `resolveCanvasRasterPlan` bounds ONE canvas against WebKit's ~2^24 ceiling
// and says nothing about how many exist, so the document-wide budget is what
// stops three individually-legal canvases exhausting device memory. At this
// page size, zoom and density a single canvas costs ~16.8M backing pixels
// against a 24M budget, so `resolveLiveCanvasWindow` returns 1 and
// render-ahead is vetoed by memory rather than by the idle gate — which is
// why this asserts AFTER `flushIdle()`, not before.
vi.stubGlobal("devicePixelRatio", 3);

await renderViewer({ initialPage: 4, fitWidth: false, zoom: VIEWER_MAX_ZOOM });

// `renderZoom` is a debounced mirror of `zoom`; the raster runs at the
// default zoom until it settles. Reading before that measures a cheap canvas
// and proves nothing about the budget.
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
});
await flushIdle();

await waitFor(() => expect(renderedPages()).toEqual([4]));
expect(getPageCalls).not.toContain(3);
expect(getPageCalls).not.toContain(5);
});

it("releases the backing store of a page that leaves the render window", async () => {
await renderViewer();
const canvas = screen.getByLabelText("Synthetic monitoring protocol page 1") as HTMLCanvasElement;
Expand Down
Loading