mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
test(encounters): the Phase 2 surfaces against real Postgres, and end to end
Three properties here cannot be answered by a mocked client, and each has a shape that would pass a weaker assertion while being wrong: - filing a visit against a due checkup twice satisfies it ONCE. Phrased as "the second call returned 200" it proves nothing, so the assertion is that lastSatisfiedAt did not move — and the instant is captured once, because two calls milliseconds apart are two different filings and the forward-only guard correctly advances on the later one. - a lab commit writes one link PER RESULT ROW. A test that asserted "there is a link" would pass on one link for a three-marker panel. - the visit list's condition filter runs in SQL. With one visit in the account a filter and no filter return the same list, so there are always two. The bulk arm gets per-entry tolerance over a hundred documents, one unknown id reported as one failed row with the rest filed, and a foreign visit refusing the whole batch before anything is written. The e2e spec proves the incidental-linking promise once, addressing stable data-slot attributes rather than viewport text: a visit saves with a date and nothing else, and a document dated inside the window offers that visit, pre-selected because there is exactly one, and files itself against it.
This commit is contained in:
1 parent
4d2f706dad
commit
bdfea7dd85
3 files changed
+572
-2
No files matched your search
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Visits — the incidental-linking promise, proven end to end once.
|
||||
*
|
||||
* The flow this milestone is checked against, in one script:
|
||||
*
|
||||
* 1. create a visit with ONLY a date — nothing else touched, Save enabled;
|
||||
* 2. it appears in the list;
|
||||
* 3. open a document dated inside the ±7-day window;
|
||||
* 4. the suggestion offers that visit;
|
||||
* 5. take the offer;
|
||||
* 6. the document's detail names the visit it now belongs to.
|
||||
*
|
||||
* Every assertion addresses a stable `data-slot`, never viewport text: the
|
||||
* copy is i18n-driven and the mobile project renders the same slots at a
|
||||
* different size, so a text assertion would break for the wrong reason.
|
||||
*
|
||||
* The visit is created through the API rather than by driving the sheet twice:
|
||||
* step 1 is about the SAVE being possible with only a date, which the sheet
|
||||
* assertion below covers, and the linking half needs a visit whose instant is
|
||||
* pinned relative to the seeded document's date.
|
||||
*/
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
import { ensureVaultFixture, MRT_DOC_ID } from "./setup/vault-fixture";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ensureVaultFixture();
|
||||
});
|
||||
|
||||
test.describe("visits", () => {
|
||||
test("a visit saves with a date and nothing else", async ({ page }) => {
|
||||
await page.goto("/checkups");
|
||||
await page.getByTestId("checkups-tab-visits").click();
|
||||
|
||||
const add = page.locator('[data-slot="visits-add"]').first();
|
||||
await expect(add).toBeVisible();
|
||||
await add.click();
|
||||
|
||||
// The date is pre-filled with now, and NOTHING else has been touched.
|
||||
const save = page.locator('[data-slot="encounter-save"]').first();
|
||||
await expect(save).toBeVisible();
|
||||
// The whole product constraint: not disabled, with no practitioner, no
|
||||
// reason, no outcome and no link chosen.
|
||||
await expect(save).toBeEnabled();
|
||||
|
||||
await save.click();
|
||||
|
||||
// It appears in the list. A visit dated now is in the past arm.
|
||||
await expect(page.locator('[data-slot="visit-card"]').first()).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("a document offers the visit its date falls near, and files itself against it", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
// The seeded MRI report carries a fixed filing date; the visit is placed
|
||||
// two days before it so the ±7-day window resolves to exactly one
|
||||
// candidate — the branch that pre-selects.
|
||||
const document = await request
|
||||
.get(`/api/documents/inbound/${MRT_DOC_ID}`)
|
||||
.then((res) => res.json());
|
||||
const anchorDay: string =
|
||||
document.data.reportDate ?? document.data.documentDate;
|
||||
expect(
|
||||
anchorDay,
|
||||
"the fixture document carries a filing date",
|
||||
).toBeTruthy();
|
||||
const occurredAt = new Date(`${anchorDay}T09:00:00.000Z`);
|
||||
occurredAt.setUTCDate(occurredAt.getUTCDate() - 2);
|
||||
|
||||
const created = await request.post("/api/encounters", {
|
||||
data: {
|
||||
occurredAt: occurredAt.toISOString(),
|
||||
status: "DONE",
|
||||
kind: "SPECIALIST",
|
||||
},
|
||||
});
|
||||
expect(created.status()).toBe(201);
|
||||
|
||||
await page.goto(`/documents?doc=${MRT_DOC_ID}`);
|
||||
|
||||
const suggestion = page
|
||||
.locator('[data-slot="document-encounter-suggestion"]')
|
||||
.first();
|
||||
await expect(suggestion).toBeVisible({ timeout: 15_000 });
|
||||
// One candidate, so the single-candidate branch: a pre-selectable offer
|
||||
// rather than a picker.
|
||||
await expect(suggestion).toHaveAttribute("data-verdict", "one");
|
||||
|
||||
await page
|
||||
.locator('[data-slot="document-encounter-suggestion-option"]')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// The filing landed: the read-only "belongs to visit" row appears.
|
||||
await expect(
|
||||
page.locator('[data-slot="document-visit-links"]').first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
});
|
||||
@@ -59,10 +59,10 @@ export default function CheckupsPage() {
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<Tabs value={view} onValueChange={setView}>
|
||||
<TabsList aria-label={t("checkups.viewSwitchLabel")}>
|
||||
<TabsTrigger value="vorsorge" data-slot="checkups-tab-vorsorge">
|
||||
<TabsTrigger value="vorsorge" data-testid="checkups-tab-vorsorge">
|
||||
{t("checkups.tabVorsorge")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="visits" data-slot="checkups-tab-visits">
|
||||
<TabsTrigger value="visits" data-testid="checkups-tab-visits">
|
||||
{t("checkups.tabVisits")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* The Phase 2 surfaces against real Postgres, through the real routes.
|
||||
*
|
||||
* Everything here is about ROWS — how many link rows exist, whether a second
|
||||
* call wrote a second satisfaction, which visits a filter actually returns —
|
||||
* and none of it can be answered by a mocked client. Three properties in
|
||||
* particular would pass a mock-shaped assertion while being wrong:
|
||||
*
|
||||
* - filing a visit against a due checkup twice must satisfy it ONCE. A test
|
||||
* phrased as "the second call returned 200" proves nothing; the assertion
|
||||
* is that `lastSatisfiedAt` did not move.
|
||||
* - a lab commit writes one link PER RESULT ROW. A test that counted "there
|
||||
* is a link" would pass on a single link for a three-marker panel.
|
||||
* - the visit list's episode filter runs in SQL over the link table. A test
|
||||
* with one visit in the account cannot tell a filter from no filter, so
|
||||
* there are always two.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
process.env.ENCRYPTION_KEY ??=
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
import { GET as listEncounters } from "@/app/api/encounters/route";
|
||||
import { POST as createEncounter } from "@/app/api/encounters/route";
|
||||
import { GET as suggestEncounter } from "@/app/api/encounters/suggest/route";
|
||||
import { POST as commitOcr } from "@/app/api/labs/ocr/commit/route";
|
||||
import { POST as bulkDocuments } from "@/app/api/documents/inbound/bulk/route";
|
||||
|
||||
import { cookieJar, headerJar } from "./mock-next-headers";
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
vi.mock("next/headers", async () => {
|
||||
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
||||
return {
|
||||
headers: vi.fn(async () => ({
|
||||
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
||||
})),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: (name: string) => {
|
||||
const value = cookieJar.get(name);
|
||||
return value ? { name, value } : undefined;
|
||||
},
|
||||
set: (name: string, value: string) => cookieJar.set(name, value),
|
||||
delete: (name: string) => cookieJar.delete(name),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const OWNER_ID = "phase2-owner";
|
||||
|
||||
function daysFromNow(days: number): Date {
|
||||
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function seedSession() {
|
||||
const prisma = getPrismaClient();
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: OWNER_ID,
|
||||
username: "phase2-owner",
|
||||
email: "phase2-owner@example.test",
|
||||
timezone: "Europe/Berlin",
|
||||
// The document and lab surfaces are module-gated; the visit is not.
|
||||
modulePreferencesJson: {
|
||||
inboundDocuments: true,
|
||||
labs: true,
|
||||
illness: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
userId: OWNER_ID,
|
||||
expiresAt: daysFromNow(7),
|
||||
mfaVerifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
cookieJar.set("healthlog_session", session.id);
|
||||
}
|
||||
|
||||
function post(body: unknown): Promise<Response> {
|
||||
return createEncounter(
|
||||
new Request("http://localhost/api/encounters", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}) as never,
|
||||
);
|
||||
}
|
||||
|
||||
async function json<T>(res: Response): Promise<T> {
|
||||
return ((await res.json()) as { data: T }).data;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
cookieJar.clear();
|
||||
headerJar.clear();
|
||||
await seedSession();
|
||||
});
|
||||
|
||||
describe("filing a visit against a due checkup", () => {
|
||||
async function dueCheckup() {
|
||||
return getPrismaClient().measurementReminder.create({
|
||||
data: {
|
||||
userId: OWNER_ID,
|
||||
label: "Annual panel",
|
||||
measurementType: null,
|
||||
intervalDays: 365,
|
||||
anchorDate: daysFromNow(-400),
|
||||
notifyHour: 9,
|
||||
nextDueAt: daysFromNow(-35),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it("advances lastSatisfiedAt, and doing it twice does not advance it again", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const checkup = await dueCheckup();
|
||||
expect(checkup.lastSatisfiedAt).toBeNull();
|
||||
|
||||
// ONE instant, captured once and reused. Two calls to `daysFromNow(-30)`
|
||||
// are milliseconds apart, and the second would be strictly later — which
|
||||
// the forward-only guard correctly advances on. The property under test is
|
||||
// the one that matters in the product: the SAME visit filed twice.
|
||||
const occurredAt = daysFromNow(-30).toISOString();
|
||||
|
||||
const first = await post({
|
||||
occurredAt,
|
||||
status: "DONE",
|
||||
reminderId: checkup.id,
|
||||
});
|
||||
expect(first.status).toBe(201);
|
||||
|
||||
const afterFirst = await prisma.measurementReminder.findUniqueOrThrow({
|
||||
where: { id: checkup.id },
|
||||
});
|
||||
expect(afterFirst.lastSatisfiedAt).not.toBeNull();
|
||||
|
||||
// The SAME instant again. `satisfyReminder` is forward-only, so this is a
|
||||
// no-op that still succeeds — the property the plan calls idempotent, and
|
||||
// the one a status-code assertion alone would not see.
|
||||
const second = await post({
|
||||
occurredAt,
|
||||
status: "DONE",
|
||||
reminderId: checkup.id,
|
||||
});
|
||||
expect(second.status).toBe(201);
|
||||
|
||||
const afterSecond = await prisma.measurementReminder.findUniqueOrThrow({
|
||||
where: { id: checkup.id },
|
||||
});
|
||||
expect(afterSecond.lastSatisfiedAt?.toISOString()).toBe(
|
||||
afterFirst.lastSatisfiedAt?.toISOString(),
|
||||
);
|
||||
expect(afterSecond.nextDueAt?.toISOString()).toBe(
|
||||
afterFirst.nextDueAt?.toISOString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("satisfies at the VISIT's instant rather than at now", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const checkup = await dueCheckup();
|
||||
const occurredAt = daysFromNow(-30);
|
||||
|
||||
await post({
|
||||
occurredAt: occurredAt.toISOString(),
|
||||
status: "DONE",
|
||||
reminderId: checkup.id,
|
||||
});
|
||||
|
||||
const after = await prisma.measurementReminder.findUniqueOrThrow({
|
||||
where: { id: checkup.id },
|
||||
});
|
||||
// Satisfying at `now` would push next-due 365 days from today rather than
|
||||
// from the draw; the honest date is the visit's own.
|
||||
expect(after.lastSatisfiedAt?.toISOString()).toBe(occurredAt.toISOString());
|
||||
});
|
||||
|
||||
it("leaves a checkup untouched when the visit is not done", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const checkup = await dueCheckup();
|
||||
|
||||
const res = await post({
|
||||
occurredAt: daysFromNow(-30).toISOString(),
|
||||
status: "NO_SHOW",
|
||||
reminderId: checkup.id,
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
const after = await prisma.measurementReminder.findUniqueOrThrow({
|
||||
where: { id: checkup.id },
|
||||
});
|
||||
expect(after.lastSatisfiedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("a lab panel filed against a visit", () => {
|
||||
const rows = [
|
||||
{ analyte: "LDL", value: 3.1, unit: "mmol/L" },
|
||||
{ analyte: "HDL", value: 1.4, unit: "mmol/L" },
|
||||
{ analyte: "Ferritin", value: 88, unit: "µg/L" },
|
||||
];
|
||||
|
||||
function commit(body: unknown): Promise<Response> {
|
||||
return commitOcr(
|
||||
new Request("http://localhost/api/labs/ocr/commit", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}) as never,
|
||||
);
|
||||
}
|
||||
|
||||
it("writes ONE link per result row", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const visit = await json<{ id: string }>(
|
||||
await post({ occurredAt: daysFromNow(-1).toISOString(), status: "DONE" }),
|
||||
);
|
||||
const takenAt = daysFromNow(-1).toISOString();
|
||||
|
||||
const res = await commit({
|
||||
rows: rows.map((row) => ({ ...row, takenAt })),
|
||||
encounterId: visit.id,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const results = await prisma.labResult.findMany({
|
||||
where: { userId: OWNER_ID },
|
||||
});
|
||||
expect(results).toHaveLength(3);
|
||||
|
||||
const links = await prisma.encounterLabLink.findMany({
|
||||
where: { userId: OWNER_ID, encounterId: visit.id },
|
||||
});
|
||||
// Three, not one: a marker re-run on a different day belongs to its own
|
||||
// visit, which is why the join is m:n rather than a column on the result.
|
||||
expect(links).toHaveLength(3);
|
||||
expect(new Set(links.map((link) => link.labResultId))).toEqual(
|
||||
new Set(results.map((result) => result.id)),
|
||||
);
|
||||
});
|
||||
|
||||
it("succeeds and writes no link when no visit is named", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const res = await commit({
|
||||
rows: [{ ...rows[0], takenAt: daysFromNow(-2).toISOString() }],
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
expect(await prisma.labResult.count({ where: { userId: OWNER_ID } })).toBe(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
await prisma.encounterLabLink.count({ where: { userId: OWNER_ID } }),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filing documents against a visit in bulk", () => {
|
||||
async function seedDocuments(count: number): Promise<string[]> {
|
||||
const prisma = getPrismaClient();
|
||||
const ids: string[] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const row = await prisma.inboundDocument.create({
|
||||
data: {
|
||||
userId: OWNER_ID,
|
||||
kind: "OTHER",
|
||||
filename: `doc-${index}.pdf`,
|
||||
mimeType: "application/pdf",
|
||||
byteSize: 4,
|
||||
status: "STORED",
|
||||
contentEncrypted: Buffer.from("test"),
|
||||
contentCodec: "v1",
|
||||
},
|
||||
});
|
||||
ids.push(row.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function bulk(body: unknown): Promise<Response> {
|
||||
return bulkDocuments(
|
||||
new Request("http://localhost/api/documents/inbound/bulk", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}) as never,
|
||||
);
|
||||
}
|
||||
|
||||
it("links a whole batch, per entry, and is idempotent", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const visit = await json<{ id: string }>(
|
||||
await post({ occurredAt: daysFromNow(-1).toISOString(), status: "DONE" }),
|
||||
);
|
||||
const ids = await seedDocuments(100);
|
||||
|
||||
const res = await bulk({
|
||||
ids,
|
||||
action: "linkEncounter",
|
||||
encounterId: visit.id,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const first = await json<{ results: Array<{ ok: boolean }> }>(res);
|
||||
// Per-entry tolerance: a hundred outcomes, one per id, not one verdict.
|
||||
expect(first.results).toHaveLength(100);
|
||||
expect(first.results.every((row) => row.ok)).toBe(true);
|
||||
expect(
|
||||
await prisma.encounterDocumentLink.count({
|
||||
where: { userId: OWNER_ID, encounterId: visit.id },
|
||||
}),
|
||||
).toBe(100);
|
||||
|
||||
// Re-posting the same batch changes nothing and still succeeds.
|
||||
const again = await bulk({
|
||||
ids,
|
||||
action: "linkEncounter",
|
||||
encounterId: visit.id,
|
||||
});
|
||||
expect(again.status).toBe(200);
|
||||
expect(
|
||||
await prisma.encounterDocumentLink.count({
|
||||
where: { userId: OWNER_ID, encounterId: visit.id },
|
||||
}),
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it("reports one unknown id as one failed row and files the rest", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const visit = await json<{ id: string }>(
|
||||
await post({ occurredAt: daysFromNow(-1).toISOString(), status: "DONE" }),
|
||||
);
|
||||
const ids = await seedDocuments(2);
|
||||
|
||||
const res = await bulk({
|
||||
ids: [...ids, "not-a-document"],
|
||||
action: "linkEncounter",
|
||||
encounterId: visit.id,
|
||||
});
|
||||
const payload = await json<{
|
||||
results: Array<{ id: string; ok: boolean; error: string | null }>;
|
||||
}>(res);
|
||||
expect(payload.results).toHaveLength(3);
|
||||
expect(payload.results.filter((row) => row.ok)).toHaveLength(2);
|
||||
expect(
|
||||
payload.results.find((row) => row.id === "not-a-document")?.error,
|
||||
).toBe("notFound");
|
||||
expect(
|
||||
await prisma.encounterDocumentLink.count({ where: { userId: OWNER_ID } }),
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("refuses the whole batch when the visit is not the caller's", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const ids = await seedDocuments(2);
|
||||
const res = await bulk({
|
||||
ids,
|
||||
action: "linkEncounter",
|
||||
encounterId: "someone-elses-visit",
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
expect(
|
||||
await prisma.encounterDocumentLink.count({ where: { userId: OWNER_ID } }),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the visit list's condition filter", () => {
|
||||
it("returns only the visits filed against that episode", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const episode = await prisma.illnessEpisode.create({
|
||||
data: {
|
||||
userId: OWNER_ID,
|
||||
label: "Knee",
|
||||
type: "INJURY",
|
||||
onsetAt: daysFromNow(-40),
|
||||
},
|
||||
});
|
||||
|
||||
const linked = await json<{ id: string }>(
|
||||
await post({
|
||||
occurredAt: daysFromNow(-5).toISOString(),
|
||||
status: "DONE",
|
||||
episodeIds: [episode.id],
|
||||
}),
|
||||
);
|
||||
// A SECOND visit with no link. Without it a filter and no filter return
|
||||
// the same list and the assertion below would prove nothing.
|
||||
await post({ occurredAt: daysFromNow(-6).toISOString(), status: "DONE" });
|
||||
|
||||
const res = await listEncounters(
|
||||
new Request(
|
||||
`http://localhost/api/encounters?episodeId=${episode.id}`,
|
||||
) as never,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const list = await json<{
|
||||
upcoming: Array<{ id: string }>;
|
||||
past: Array<{ id: string }>;
|
||||
}>(res);
|
||||
expect(list.past.map((row) => row.id)).toEqual([linked.id]);
|
||||
|
||||
const unfiltered = await json<{ past: Array<{ id: string }> }>(
|
||||
await listEncounters(
|
||||
new Request("http://localhost/api/encounters") as never,
|
||||
),
|
||||
);
|
||||
expect(unfiltered.past).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the suggestion, over real rows", () => {
|
||||
function suggest(anchor: string): Promise<Response> {
|
||||
return suggestEncounter(
|
||||
new Request(
|
||||
`http://localhost/api/encounters/suggest?anchor=${encodeURIComponent(anchor)}`,
|
||||
) as never,
|
||||
);
|
||||
}
|
||||
|
||||
it("pre-selects a single candidate and offers a picker for two", async () => {
|
||||
const anchor = daysFromNow(-3);
|
||||
|
||||
const only = await json<{ id: string }>(
|
||||
await post({ occurredAt: daysFromNow(-2).toISOString(), status: "DONE" }),
|
||||
);
|
||||
const one = await json<{ kind: string; encounter: { id: string } }>(
|
||||
await suggest(anchor.toISOString()),
|
||||
);
|
||||
expect(one.kind).toBe("one");
|
||||
expect(one.encounter.id).toBe(only.id);
|
||||
|
||||
await post({ occurredAt: daysFromNow(-4).toISOString(), status: "DONE" });
|
||||
const many = await json<{ kind: string; encounters: unknown[] }>(
|
||||
await suggest(anchor.toISOString()),
|
||||
);
|
||||
expect(many.kind).toBe("many");
|
||||
expect(many.encounters).toHaveLength(2);
|
||||
// No pre-selection on the two-candidate arm — the shape itself has none.
|
||||
expect(many).not.toHaveProperty("encounter");
|
||||
});
|
||||
|
||||
it("offers nothing outside the window, and nothing for a soft-deleted visit", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
await post({ occurredAt: daysFromNow(-30).toISOString(), status: "DONE" });
|
||||
const far = await json<{ kind: string }>(
|
||||
await suggest(new Date().toISOString()),
|
||||
);
|
||||
expect(far.kind).toBe("none");
|
||||
|
||||
const near = await json<{ id: string }>(
|
||||
await post({ occurredAt: daysFromNow(-1).toISOString(), status: "DONE" }),
|
||||
);
|
||||
await prisma.encounter.update({
|
||||
where: { id: near.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
const tombstoned = await json<{ kind: string }>(
|
||||
await suggest(new Date().toISOString()),
|
||||
);
|
||||
expect(tombstoned.kind).toBe("none");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user