diff --git a/tests/integration/mcp-record-links.test.ts b/tests/integration/mcp-record-links.test.ts index ec4a29cad..82af33fb8 100644 --- a/tests/integration/mcp-record-links.test.ts +++ b/tests/integration/mcp-record-links.test.ts @@ -74,12 +74,17 @@ async function makeDocument( }); } -async function makeLab(userId: string, analyte: string, takenAt: string) { +async function makeLab( + userId: string, + analyte: string, + takenAt: string, + value = 120, +) { return getPrismaClient().labResult.create({ data: { userId, analyte, - value: 120, + value, unit: "mg/dL", takenAt: new Date(takenAt), }, @@ -206,8 +211,9 @@ describe("get_labs lists each reading's links", () => { }, ]); expect(JSON.stringify(answer)).not.toContain("paperless.example.test"); - // No raw lab result id is left on the wire. - expect(JSON.stringify(answer)).not.toContain(r.ldl.id); + // Each reading carries its own fetch id, and no bare lab result id is left. + expect((ldl as Reading & { id?: string }).id).toBe(`labresult:${r.ldl.id}`); + expect(JSON.stringify(answer).split(r.ldl.id)).toHaveLength(2); }); it("lists the links of earlier values and of a trajectory, [] when none", async () => { @@ -331,8 +337,8 @@ describe("fetch document lists the labs and medications filed against it", () => const r = await seedRecord(user.id); const doc = await fetchId(user.id, `document:${r.report.id}`); expect(doc.metadata.links?.map((l) => l.id)).toEqual([ - "lab:LDL", - "lab:HDL", + `labresult:${r.ldl.id}`, + `labresult:${r.hdl.id}`, `med:${r.med.id}`, ]); expect(doc.text).toContain("Linked records: lab"); @@ -347,6 +353,130 @@ describe("fetch document lists the labs and medications filed against it", () => }); }); +describe("a link to a lab result lands on the reading that record holds", () => { + /** + * Two reports a year apart with an LDL reading each, plus a second report + * dated the same day as the later one with its own LDL reading. `lab:LDL` + * answers with the latest; a link from a report must not. + */ + async function seedDraws(userId: string) { + const db = getPrismaClient(); + const earlier = await makeDocument(userId, "Lipid panel 2025"); + const later = await makeDocument(userId, "Lipid panel 2026"); + const sameDay = await makeDocument(userId, "Second lab, same day"); + const visit = await db.encounter.create({ + data: { + userId, + occurredAt: new Date("2025-03-01T09:00:00Z"), + kind: "ROUTINE", + }, + }); + const early = await makeLab(userId, "LDL", "2025-03-01T09:00:00Z", 160); + const late = await makeLab(userId, "LDL", "2026-03-01T09:00:00Z", 110); + const twin = await makeLab(userId, "LDL", "2026-03-01T09:00:00Z", 115); + for (const [lab, doc] of [ + [early, earlier], + [late, later], + [twin, sameDay], + ] as const) { + await linkTargets(db, { + userId, + sourceKind: "labResult", + sourceId: lab.id, + targetKind: "document", + targetIds: [doc.id], + }); + } + await linkTargets(db, { + userId, + sourceKind: "encounter", + sourceId: visit.id, + targetKind: "labResult", + targetIds: [early.id], + }); + return { earlier, later, sameDay, visit, early, late, twin }; + } + + const linkIds = (answer: FetchAnswer) => + (answer.metadata.links ?? []).map((l) => l.id); + + it("from a report, a visit and get_labs, each id fetches that reading", async () => { + const user = await makeUser(); + const d = await seedDraws(user.id); + + const fromEarlier = await fetchId(user.id, `document:${d.earlier.id}`); + expect(linkIds(fromEarlier)).toEqual([`labresult:${d.early.id}`]); + const reading = await fetchId(user.id, `labresult:${d.early.id}`); + expect(reading.text).toContain("LDL: 160 mg/dL"); + expect(reading.text).toContain("measured 2025-03-01"); + expect(reading.metadata.value).toBe(160); + expect(linkIds(reading)).toEqual([ + `document:${d.earlier.id}`, + `visit:${d.visit.id}`, + ]); + expect(linkIds(reading)).not.toContain(`document:${d.later.id}`); + + // The visit the earlier draw is filed against links the same reading. + const visit = await fetchId(user.id, `visit:${d.visit.id}`); + expect(linkIds(visit)).toContain(`labresult:${d.early.id}`); + + // Two reports on one day: each lands on its own reading. + for (const [doc, lab, value] of [ + [d.later, d.late, 110], + [d.sameDay, d.twin, 115], + ] as const) { + const fromDoc = await fetchId(user.id, `document:${doc.id}`); + expect(linkIds(fromDoc)).toEqual([`labresult:${lab.id}`]); + const own = await fetchId(user.id, `labresult:${lab.id}`); + expect(own.metadata.value).toBe(value); + expect(linkIds(own)).toEqual([`document:${doc.id}`]); + } + + // The analyte view is unchanged: the latest reading, named by its id. + const latest = await fetchId(user.id, "lab:LDL"); + expect(latest.metadata.readingId).toMatch(/^labresult:/); + expect([`labresult:${d.late.id}`, `labresult:${d.twin.id}`]).toContain( + latest.metadata.readingId, + ); + + // Every reading get_labs lists carries an id that fetch takes back. + const trajectory = (await tool("get_labs").run(ctx(user.id), { + analyte: "LDL", + history: true, + })) as { readings: Array<{ id: string; value: number }> }; + expect(trajectory.readings).toHaveLength(3); + for (const r of trajectory.readings) { + const back = await fetchId(user.id, r.id); + expect(back.metadata.value).toBe(r.value); + } + }); + + it("does not resolve another account's, a deleted or a switched-off reading", async () => { + const me = await makeUser(); + const other = await makeUser(); + const mine = await seedDraws(me.id); + const theirs = await seedDraws(other.id); + + const foreign = await fetchId(me.id, `labresult:${theirs.early.id}`); + expect(foreign.title).toBe("Not found"); + expect(JSON.stringify(foreign)).not.toContain(theirs.earlier.id); + + await getPrismaClient().labResult.update({ + where: { id: mine.early.id }, + data: { deletedAt: new Date() }, + }); + const deleted = await fetchId(me.id, `labresult:${mine.early.id}`); + expect(deleted.title).toBe("Not found"); + const report = await fetchId(me.id, `document:${mine.earlier.id}`); + expect(linkIds(report)).toEqual([]); + + const off = await makeUser({ labs: false }); + const offDraws = await seedDraws(off.id); + const gated = await fetchId(off.id, `labresult:${offDraws.late.id}`); + expect(gated.metadata.reason).toBe("module_disabled"); + }); +}); + describe("list rows carry the id fetch takes", () => { it("on visits, the medication schedule and conditions", async () => { const user = await makeUser();