diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index 24c98c53a..855a160a0 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -183,6 +183,7 @@ const inboundDocument = { factCount: 0, pendingCount: 0, conditionLinks: [], + encounterLinks: [], servingClass: "attachment", hasContentIndex: true, contentIndexSource: "vision", diff --git a/e2e/setup/vault-fixture.ts b/e2e/setup/vault-fixture.ts index dc9314e84..79d2e41a1 100644 --- a/e2e/setup/vault-fixture.ts +++ b/e2e/setup/vault-fixture.ts @@ -328,6 +328,33 @@ async function upsertDocs( } } +/** + * Remove the seeded e2e user's visits and their document links. + * + * The incidental-linking spec asserts a GLOBAL verdict — "exactly one visit + * falls in this document's ±7-day window" — so it must own the window it counts + * against. A visit created by an earlier run, a Playwright retry, or a second + * project sharing the account would leave two rows in the window and turn the + * single-candidate branch into the picker branch. Clearing first makes the + * count the test creates the only count it reads. `encounter_document_links` + * goes first for the FK; both are user-scoped. + */ +export async function resetEncounters(): Promise { + const url = process.env.DATABASE_URL; + if (!url) throw new Error("[vault-fixture] DATABASE_URL is not set"); + const pool = new pg.Pool({ connectionString: url }); + try { + const userId = await getUserId(pool); + await pool.query( + "DELETE FROM encounter_document_links WHERE user_id = $1", + [userId], + ); + await pool.query("DELETE FROM encounters WHERE user_id = $1", [userId]); + } finally { + await pool.end(); + } +} + /** * Enable the vault module for the seeded e2e user and plant the doctor-flow * corpus. Safe to call from every spec's beforeAll — every statement diff --git a/e2e/visits.spec.ts b/e2e/visits.spec.ts index 76daf12c0..de58676dd 100644 --- a/e2e/visits.spec.ts +++ b/e2e/visits.spec.ts @@ -19,15 +19,37 @@ * assertion below covers, and the linking half needs a visit whose instant is * pinned relative to the seeded document's date. */ +import { STORAGE_STATE_PATH } from "./setup/global-setup"; import { expect, test } from "./setup/test"; -import { ensureVaultFixture, MRT_DOC_ID } from "./setup/vault-fixture"; +import { + ensureVaultFixture, + MRT_DOC_ID, + resetEncounters, +} from "./setup/vault-fixture"; test.beforeEach(async () => { + // Own the window the linking case counts against: clear the account's visits + // before each test so a retry or a prior run cannot leave a second candidate + // in the ±7-day window and flip the single-candidate verdict to the picker. + await resetEncounters(); await ensureVaultFixture(); }); +// Serial, and single-project below: the two tests share one account, and the +// reset above would race a sibling running in parallel — the linking test would +// clear the visit the save test is still asserting. In order, in one worker, +// each test owns the account for its span. +test.describe.configure({ mode: "serial" }); + test.describe("visits", () => { + // The seeded account owns the visits and the document this flow drives, and + // both the `page` and the `request` fixture read them under its session. + // Without this the whole file runs anonymous: `/checkups` redirects to login + // and every `/api/*` read answers 401, which is a test-setup gap, not a + // product one. + test.use({ storageState: STORAGE_STATE_PATH }); + test("a visit saves with a date and nothing else", async ({ page }) => { await page.goto("/checkups"); await page.getByTestId("checkups-tab-visits").click(); diff --git a/playwright.config.ts b/playwright.config.ts index 069501a15..421731c48 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -136,6 +136,13 @@ export default defineConfig({ // desktop project instead, where the shell below `md` renders from the // same width-driven rules. Its file comment carries the reasoning. "chrome-header-seam-banners.spec.ts", + // The incidental-linking journey creates a visit and files a document + // against it, then asserts a GLOBAL verdict — one candidate in the + // document's ±7-day window. Two projects mutating the one shared + // account in parallel would leave two visits in that window and flip + // the single-candidate branch to the picker. It proves a flow through + // stable data-slots, not a mobile layout, so it runs in one project. + "visits.spec.ts", // Runs only in the service-worker project. "v137-record-session-fence-offline.spec.ts", ], diff --git a/src/components/documents/document-encounter-suggestion.tsx b/src/components/documents/document-encounter-suggestion.tsx index d0c2ea3fc..65affd67e 100644 --- a/src/components/documents/document-encounter-suggestion.tsx +++ b/src/components/documents/document-encounter-suggestion.tsx @@ -12,6 +12,14 @@ * of the two; `documentDate` is the person's own filing date and is the * fallback. * + * Both DTO fields are date-only (`YYYY-MM-DD`), but the suggest endpoint takes + * a full instant — the same shape the two lab moments already hand it. So the + * date is lifted to noon UTC here, exactly as the OCR review does: the rule is + * a ±7-day window, so the time of day never changes the verdict, and noon keeps + * a viewer's local midnight from rounding the day either way. Passing the bare + * date instead is what made this suggestion silently never appear — the API + * answered 422 and the field rendered nothing. + * * It renders nothing once the document already names a visit that this session * did not choose: an offer to file something already filed is noise. */ @@ -31,9 +39,10 @@ export function DocumentEncounterSuggestion({ onChange: (encounterId: string | null) => void; }) { if (alreadyLinked) return null; + const instant = anchor ? `${anchor}T12:00:00.000Z` : null; return (