mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
/**
|
|
* #1311 — a stored PDF on a server whose CPU cannot run the page renderer
|
|
* (closed here with `NATIVE_CANVAS=off`), for a provider that only takes
|
|
* images, through the real routes against a real Postgres.
|
|
*
|
|
* What only this file can prove: the route, the provider pick, the stored
|
|
* (encrypted) original, the real reader child and the content-index write
|
|
* meet as the unit tests assume. A text-layer PDF is summarised from its text
|
|
* and indexed as a local text read; a scan answers the honest error and
|
|
* records why on the document.
|
|
*/
|
|
import { Buffer } from "node:buffer";
|
|
|
|
import { jsPDF } from "jspdf";
|
|
import {
|
|
afterAll,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from "vitest";
|
|
|
|
process.env.ENCRYPTION_KEY ??=
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
import { encrypt } from "@/lib/crypto";
|
|
|
|
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/jobs/boss-instance", () => ({ getGlobalBoss: () => null }));
|
|
vi.mock("@/lib/documents/describe", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("@/lib/documents/describe")>()),
|
|
runDocumentSummary: vi.fn(async () => ({
|
|
summary: "A blood count from October.",
|
|
blocked: null,
|
|
})),
|
|
}));
|
|
vi.mock("@/lib/documents/auto-stage-labs", () => ({
|
|
maybeAutoStageLabFacts: vi.fn(async () => ({
|
|
staged: false,
|
|
reason: "not-lab",
|
|
})),
|
|
}));
|
|
|
|
import { runDocumentSummary } from "@/lib/documents/describe";
|
|
import { __resetNativeCanvasSupportForTests } from "@/lib/documents/native-canvas-support";
|
|
|
|
const LAB_LINE = "Haemoglobin 14.2 g/dl 13.5 - 17.5";
|
|
const PNG =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
|
|
function textLayerPdf(): Buffer {
|
|
const doc = new jsPDF();
|
|
doc.text(LAB_LINE, 10, 15);
|
|
doc.text("Leukocytes 6.1 /nl 4.0 - 10.0", 10, 23);
|
|
return Buffer.from(doc.output("arraybuffer"));
|
|
}
|
|
|
|
function scannedPdf(): Buffer {
|
|
const doc = new jsPDF();
|
|
doc.addImage(`data:image/png;base64,${PNG}`, "PNG", 10, 10, 180, 250);
|
|
return Buffer.from(doc.output("arraybuffer"));
|
|
}
|
|
|
|
let counter = 0;
|
|
|
|
async function signedInUser() {
|
|
const suffix = `u-${counter++}`;
|
|
const user = await getPrismaClient().user.create({
|
|
data: {
|
|
username: `pdf-${suffix}`,
|
|
email: `pdf-${suffix}@example.test`,
|
|
role: "USER",
|
|
timezone: "UTC",
|
|
locale: "en",
|
|
onboardingCompletedAt: new Date(),
|
|
aiProvider: "OPENAI",
|
|
aiModel: "gpt-4o",
|
|
aiOpenaiKeyEncrypted: encrypt("sk-test-openai-0001"),
|
|
modulePreferencesJson: { inboundDocuments: true, labs: true },
|
|
},
|
|
});
|
|
await getPrismaClient().consentReceipt.create({
|
|
data: {
|
|
userId: user.id,
|
|
kind: "ai_extraction",
|
|
artefact: "test",
|
|
signedAt: new Date(),
|
|
},
|
|
});
|
|
const session = await getPrismaClient().session.create({
|
|
data: { userId: user.id, expiresAt: new Date(Date.now() + 600_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
return user;
|
|
}
|
|
|
|
async function storePdf(userId: string, bytes: Buffer) {
|
|
const { encryptDocumentContent } = await import("@/lib/documents/store");
|
|
const { content, codec } = encryptDocumentContent(bytes);
|
|
return getPrismaClient().inboundDocument.create({
|
|
data: {
|
|
userId,
|
|
kind: "OTHER",
|
|
filename: "report.pdf",
|
|
mimeType: "application/pdf",
|
|
byteSize: bytes.byteLength,
|
|
status: "STORED",
|
|
contentEncrypted: content,
|
|
contentCodec: codec,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function post(documentId: string, path: "summary" | "index") {
|
|
const route =
|
|
path === "summary"
|
|
? await import("@/app/api/documents/inbound/[id]/summary/route")
|
|
: await import("@/app/api/documents/inbound/[id]/index/route");
|
|
const { NextRequest } = await import("next/server");
|
|
const res = await route.POST(
|
|
new NextRequest(
|
|
`http://localhost/api/documents/inbound/${documentId}/${path}`,
|
|
{ method: "POST" },
|
|
) as never,
|
|
{ params: Promise.resolve({ id: documentId }) } as never,
|
|
);
|
|
return {
|
|
status: res.status,
|
|
body: (await res.json()) as {
|
|
data: unknown;
|
|
error: string | null;
|
|
meta?: Record<string, unknown>;
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeAll(() => {
|
|
process.env.NATIVE_CANVAS = "off";
|
|
__resetNativeCanvasSupportForTests();
|
|
});
|
|
afterAll(() => {
|
|
delete process.env.NATIVE_CANVAS;
|
|
__resetNativeCanvasSupportForTests();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
headerJar.clear();
|
|
vi.mocked(runDocumentSummary).mockClear();
|
|
});
|
|
|
|
describe("a PDF on a server that cannot render PDF pages", () => {
|
|
it("summarises a text-layer PDF from its text", async () => {
|
|
const user = await signedInUser();
|
|
const document = await storePdf(user.id, textLayerPdf());
|
|
|
|
const res = await post(document.id, "summary");
|
|
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
|
expect(runDocumentSummary).toHaveBeenCalledOnce();
|
|
const input = vi.mocked(runDocumentSummary).mock.calls[0][0];
|
|
expect(input.ocrText).toContain(LAB_LINE);
|
|
expect(input.images).toBeUndefined();
|
|
expect(input.documents).toBeUndefined();
|
|
}, 60_000);
|
|
|
|
it("indexes a text-layer PDF from Read with AI as a local text read", async () => {
|
|
const user = await signedInUser();
|
|
const document = await storePdf(user.id, textLayerPdf());
|
|
|
|
const res = await post(document.id, "index");
|
|
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
|
const index = await getPrismaClient().documentContentIndex.findUnique({
|
|
where: { documentId: document.id },
|
|
select: { source: true, providerType: true },
|
|
});
|
|
expect(index).toEqual({ source: "local-pdf", providerType: null });
|
|
}, 60_000);
|
|
|
|
it("answers a scan with pdfRendererUnavailable and records why", async () => {
|
|
const user = await signedInUser();
|
|
const document = await storePdf(user.id, scannedPdf());
|
|
|
|
const summary = await post(document.id, "summary");
|
|
expect(summary.status).toBe(422);
|
|
expect(summary.body.meta?.errorCode).toBe(
|
|
"documents.inbound.pdfRendererUnavailable",
|
|
);
|
|
expect(runDocumentSummary).not.toHaveBeenCalled();
|
|
|
|
const index = await post(document.id, "index");
|
|
expect(index.status).toBe(422);
|
|
expect(index.body.meta?.errorCode).toBe(
|
|
"documents.inbound.pdfRendererUnavailable",
|
|
);
|
|
const row = await getPrismaClient().inboundDocument.findUniqueOrThrow({
|
|
where: { id: document.id },
|
|
select: { lastIndexOutcome: true },
|
|
});
|
|
expect(row.lastIndexOutcome).toBe("pdf-needs-anthropic");
|
|
}, 60_000);
|
|
});
|