mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
test(mcp): search and fetch over clinical records against real Postgres
This commit is contained in:
1 parent
5d80b194ac
commit
1dbab4a912
1 file changed
+137
@@ -10,6 +10,10 @@
|
||||
* trailing windows use and filters to the requested span — exercised end to
|
||||
* end through `compareMetric` (which seeds real DAY rollups via the measure
|
||||
* write core).
|
||||
* - v1.39.3: `search` finds visits, conditions and documents through the
|
||||
* real ciphertext and the real blind content index, `fetch` hydrates each
|
||||
* id it returned with its links, one account never sees another's
|
||||
* records, and a switched-off module's records drop out.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
@@ -19,6 +23,8 @@ process.env.ENCRYPTION_KEY ??=
|
||||
import { logMcpBloodPressure, logMcpMeasurement } from "@/lib/mcp/writes";
|
||||
import { compareMetric } from "@/lib/mcp/rich-reads";
|
||||
import { MCP_TOOLS } from "@/lib/mcp/tools";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { upsertContentIndex } from "@/lib/documents/content-index";
|
||||
import type { McpAuthContext } from "@/lib/mcp/auth";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
@@ -266,3 +272,134 @@ describe("compareMetric — explicit {from,to} range over real rollups", () => {
|
||||
expect(res.delta?.mean).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search + fetch over clinical records — real DB (v1.39.3)", () => {
|
||||
async function seed(suffix: string) {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await makeUser(suffix);
|
||||
const practitioner = await prisma.practitioner.create({
|
||||
data: { userId: user.id, name: "Dr. Weber", specialty: "Orthopädie" },
|
||||
});
|
||||
const visit = await prisma.encounter.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
occurredAt: new Date("2023-05-02T08:00:00Z"),
|
||||
kind: "PROCEDURE",
|
||||
status: "DONE",
|
||||
practitionerId: practitioner.id,
|
||||
bodySiteEncrypted: encryptToBytes("Knie"),
|
||||
laterality: "LEFT",
|
||||
reasonEncrypted: encryptToBytes("Arthroskopie"),
|
||||
},
|
||||
});
|
||||
const condition = await prisma.illnessEpisode.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
label: "Meniskusriss",
|
||||
type: "INJURY",
|
||||
onsetAt: new Date("2023-04-01T00:00:00Z"),
|
||||
bodySiteEncrypted: encryptToBytes("Knie"),
|
||||
laterality: "LEFT",
|
||||
},
|
||||
});
|
||||
const document = await prisma.inboundDocument.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
kind: "DISCHARGE_LETTER",
|
||||
title: "Brief Klinikum",
|
||||
mimeType: "application/pdf",
|
||||
byteSize: 3,
|
||||
contentEncrypted: encryptToBytes("pdf"),
|
||||
},
|
||||
});
|
||||
await upsertContentIndex({
|
||||
userId: user.id,
|
||||
documentId: document.id,
|
||||
text: "Befund nach Arthroskopie: Innenmeniskus teilreseziert.",
|
||||
source: "local-pdf",
|
||||
});
|
||||
await prisma.encounterConditionLink.create({
|
||||
data: { userId: user.id, encounterId: visit.id, episodeId: condition.id },
|
||||
});
|
||||
await prisma.encounterDocumentLink.create({
|
||||
data: { userId: user.id, encounterId: visit.id, documentId: document.id },
|
||||
});
|
||||
return { user, visit, condition, document };
|
||||
}
|
||||
|
||||
async function search(userId: string, query: string) {
|
||||
return (await readTool("search").run(readCtx(userId), { query })) as {
|
||||
results: Array<{ id: string; title: string; url: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
it("finds the left knee operation and round-trips it through fetch", async () => {
|
||||
const mine = await seed("rec-a");
|
||||
const theirs = await seed("rec-b");
|
||||
|
||||
const hit = await search(mine.user.id, "my left knee operation");
|
||||
expect(hit.results[0]?.id).toBe(`visit:${mine.visit.id}`);
|
||||
const ids = hit.results.map((r) => r.id);
|
||||
expect(ids).toContain(`condition:${mine.condition.id}`);
|
||||
// Nothing of the other account, which holds an identical record.
|
||||
expect(ids.some((id) => id.endsWith(theirs.visit.id))).toBe(false);
|
||||
expect(ids.some((id) => id.endsWith(theirs.condition.id))).toBe(false);
|
||||
|
||||
const visit = (await readTool("fetch").run(readCtx(mine.user.id), {
|
||||
id: `visit:${mine.visit.id}`,
|
||||
})) as { text: string; metadata: Record<string, unknown> };
|
||||
expect(visit.metadata.present).toBe(true);
|
||||
expect(visit.metadata.bodySite).toBe(
|
||||
"<<<USER_TEXT_START>>>Knie<<<USER_TEXT_END>>>",
|
||||
);
|
||||
const linkIds = (visit.metadata.links as Array<{ id: string }>).map(
|
||||
(l) => l.id,
|
||||
);
|
||||
expect(linkIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
`condition:${mine.condition.id}`,
|
||||
`document:${mine.document.id}`,
|
||||
]),
|
||||
);
|
||||
|
||||
// The other account cannot fetch it by id.
|
||||
const foreign = (await readTool("fetch").run(readCtx(theirs.user.id), {
|
||||
id: `visit:${mine.visit.id}`,
|
||||
})) as { title: string };
|
||||
expect(foreign.title).toBe("Not found");
|
||||
});
|
||||
|
||||
it("finds a document by a word of its indexed text and returns an excerpt", async () => {
|
||||
const mine = await seed("rec-doc");
|
||||
const byText = await search(mine.user.id, "Innenmeniskus");
|
||||
expect(byText.results.map((r) => r.id)).toEqual([
|
||||
`document:${mine.document.id}`,
|
||||
]);
|
||||
const byKind = await search(mine.user.id, "the discharge letter");
|
||||
expect(byKind.results[0]?.id).toBe(`document:${mine.document.id}`);
|
||||
|
||||
const doc = (await readTool("fetch").run(readCtx(mine.user.id), {
|
||||
id: `document:${mine.document.id}`,
|
||||
})) as { url: string; metadata: Record<string, unknown> };
|
||||
expect(doc.metadata.indexed).toBe(true);
|
||||
expect(doc.metadata.excerpt).toContain("Innenmeniskus");
|
||||
expect(doc.url).toContain(`/documents?doc=${mine.document.id}`);
|
||||
});
|
||||
|
||||
it("drops a switched-off module's records from search and fetch", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const mine = await seed("rec-gate");
|
||||
await prisma.user.update({
|
||||
where: { id: mine.user.id },
|
||||
data: { modulePreferencesJson: { illness: false } },
|
||||
});
|
||||
const ids = (await search(mine.user.id, "knie")).results.map((r) => r.id);
|
||||
expect(ids).toContain(`visit:${mine.visit.id}`);
|
||||
expect(ids.some((id) => id.startsWith("condition:"))).toBe(false);
|
||||
|
||||
const condition = (await readTool("fetch").run(readCtx(mine.user.id), {
|
||||
id: `condition:${mine.condition.id}`,
|
||||
})) as { metadata: Record<string, unknown> };
|
||||
expect(condition.metadata.reason).toBe("module_disabled");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user