Files
HealthLog/tests/integration/document-thumbnail-backfill.test.ts
Marc-André Bombeck 3e08fd0e68 fix(documents): stop queueing a thumbnail that cannot be rendered on every boot
A document whose preview render fails for good (a PDF whose first page
renders blank) was queued by the boot-time backfill, refused, and queued
again at the next boot, without end.

The thumbnail job now keeps the failure with the document
(document_thumbnail_failures, migration 0374): the reason, the attempts
since the last success and when the newest failed. The backfill, both the
boot discovery and the per-account pass, leaves a document out after one
deterministic refusal (raster-failed, empty-render, pixel-cap) or three of
any other kind, until the document changes. A direct enqueue still renders
it, and a preview that is made clears the record.
2026-10-04 15:56:56 +02:00

144 lines
5.3 KiB
TypeScript

/**
* A document whose preview cannot be rendered is not queued again on every
* boot.
*
* What this pins. The boot-time backfill queued every thumbnailable document
* without a thumbnail, and a PDF whose first page renders blank never gets
* one: it was queued, rendered, refused, and queued again at the next boot,
* forever. The failure is now kept with the document, the backfill leaves a
* document that failed for good alone, and a document that changed after the
* failure is tried once more. A preview that does get made clears it.
*
* Both ends against the real schema: the job writes the failure, the boot
* discovery and the per-account pass read it. Only the renderer and the queue
* are stand-ins, so the outcome of a render can be chosen.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const generateThumbnail = vi.hoisted(() => vi.fn());
const sends = vi.hoisted(() => [] as Array<{ queue: string; data: unknown }>);
vi.mock("@/lib/documents/thumbnail", () => ({ generateThumbnail }));
vi.mock("@/lib/documents/native-canvas-support", () => ({
nativeCanvasSupported: () => true,
}));
vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: () => ({
send: async (queue: string, data: unknown) => {
sends.push({ queue, data });
return `job-${sends.length}`;
},
}),
}));
import { encryptDocumentContent } from "@/lib/documents/store";
import { runDocumentThumbnail } from "@/lib/jobs/document-thumbnail";
import {
enqueueBootTimeThumbnailBackfill,
runThumbnailBackfillForUser,
} from "@/lib/jobs/document-thumbnail-backfill";
import { getPrismaClient, truncateAllTables } from "./setup";
const prisma = getPrismaClient();
const USER_ID = "thumbnail-backfill-owner";
async function seedPdf(id: string) {
const { content, codec } = encryptDocumentContent(
Buffer.from("%PDF-1.7 a page that renders blank"),
);
await prisma.inboundDocument.create({
data: {
id,
userId: USER_ID,
mimeType: "application/pdf",
byteSize: 32,
contentEncrypted: content,
contentCodec: codec,
title: id,
},
});
}
function queuedDocuments(): string[] {
return sends
.filter((s) => s.queue === "document-thumbnail")
.map((s) => (s.data as { documentId: string }).documentId);
}
beforeEach(async () => {
await truncateAllTables(prisma);
sends.length = 0;
generateThumbnail.mockReset();
await prisma.user.create({
data: { id: USER_ID, username: "thumbnail-backfill-owner" },
});
});
describe("thumbnail backfill and a render that fails for good", () => {
it("leaves a document whose render failed for good out of every later backfill", async () => {
await seedPdf("doc-blank");
await seedPdf("doc-fine");
generateThumbnail.mockResolvedValue({ ok: false, reason: "raster-failed" });
await runDocumentThumbnail({ userId: USER_ID, documentId: "doc-blank" });
sends.length = 0;
await runThumbnailBackfillForUser(USER_ID);
expect(queuedDocuments()).toEqual(["doc-fine"]);
// The boot discovery still finds the account, for the other document,
// and stops finding it once that one has its preview.
sends.length = 0;
expect((await enqueueBootTimeThumbnailBackfill()).enqueued).toBe(1);
generateThumbnail.mockResolvedValue({
ok: true,
thumbnail: { jpeg: Buffer.from([0xff, 0xd8, 0xff]), width: 2, height: 3 },
});
await runDocumentThumbnail({ userId: USER_ID, documentId: "doc-fine" });
sends.length = 0;
expect((await enqueueBootTimeThumbnailBackfill()).enqueued).toBe(0);
expect(await runThumbnailBackfillForUser(USER_ID)).toEqual({ enqueued: 0 });
});
it("tries a failed document again once it has changed, and forgets the failure when a preview is made", async () => {
await seedPdf("doc-blank");
generateThumbnail.mockResolvedValue({ ok: false, reason: "raster-failed" });
await runDocumentThumbnail({ userId: USER_ID, documentId: "doc-blank" });
sends.length = 0;
expect(await runThumbnailBackfillForUser(USER_ID)).toEqual({ enqueued: 0 });
// A later edit is a change: the document is tried once more.
await new Promise((resolve) => setTimeout(resolve, 5));
await prisma.inboundDocument.update({
where: { id: "doc-blank" },
data: { title: "renamed" },
});
expect(await runThumbnailBackfillForUser(USER_ID)).toEqual({ enqueued: 1 });
sends.length = 0;
expect((await enqueueBootTimeThumbnailBackfill()).enqueued).toBe(1);
generateThumbnail.mockResolvedValue({
ok: true,
thumbnail: { jpeg: Buffer.from([0xff, 0xd8, 0xff]), width: 2, height: 3 },
});
await runDocumentThumbnail({ userId: USER_ID, documentId: "doc-blank" });
expect(
await prisma.documentThumbnailFailure.count({
where: { documentId: "doc-blank" },
}),
).toBe(0);
});
it("gives a render that failed by a throw three tries, not one", async () => {
await seedPdf("doc-flaky");
generateThumbnail.mockResolvedValue({ ok: false, reason: "error" });
for (let attempt = 1; attempt <= 3; attempt++) {
sends.length = 0;
expect(await runThumbnailBackfillForUser(USER_ID)).toEqual({
enqueued: 1,
});
await runDocumentThumbnail({ userId: USER_ID, documentId: "doc-flaky" });
}
expect(await runThumbnailBackfillForUser(USER_ID)).toEqual({ enqueued: 0 });
});
});