fix(documents): keep an incomplete extraction on the document

A long document read in parts can end with a part cut at the provider's
output limit, a part never read, or more facts than one review takes.
That was told once, as a notice when the run finished; a review opened
later looked complete.

InboundDocument.extractionIncomplete (migration 0483) is written with
the facts by both stagings, the extract route or run and lab
auto-staging. The document detail publishes it and the review shows the
same note whenever it is opened. The disaster-recovery backup carries
it; an older file restores it as false.

Refs #1257
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 01:01:13 +02:00
1 parent 5cfaf4869d
commit 958d230b9f
15 files changed
+233 -44

No files matched your search

@@ -854,3 +854,73 @@ describe("fact extraction in the background", () => {
expect(await getPrismaClient().documentAiRun.count()).toBe(0);
});
});
async function documentDetail(documentId: string) {
const { GET } = await import("@/app/api/documents/inbound/[id]/route");
const { NextRequest } = await import("next/server");
const res = await GET(
new NextRequest(
`http://localhost/api/documents/inbound/${documentId}`,
) as never,
{ params: Promise.resolve({ id: documentId }) } as never,
);
return {
status: res.status,
body: (await res.json()) as Envelope<Record<string, unknown>>,
};
}
/** The extraction mock's default answer, cut short. */
async function answerIncompleteOnce() {
const actual = await vi.mocked(runInboundExtraction).getMockImplementation()!(
{} as never,
);
vi.mocked(runInboundExtraction).mockResolvedValueOnce({
...actual,
incomplete: true,
});
}
describe("an extraction that did not read the whole document", () => {
it("keeps saying so on the document until a whole read replaces it", async () => {
const user = await makeUser();
await grantConsent(user.id);
const document = await indexedDocument(user.id);
await signIn(user.id);
await answerIncompleteOnce();
const cut = await postStoredExtract(document.id, false);
expect(cut.status).toBe(200);
expect(cut.body.data).toMatchObject({ extractionIncomplete: true });
// Opened later, the review still says it is not the whole document.
const later = await documentDetail(document.id);
expect(later.status).toBe(200);
expect(later.body.data).toMatchObject({ extractionIncomplete: true });
const whole = await postStoredExtract(document.id, false);
expect(whole.status).toBe(200);
expect(whole.body.data).toMatchObject({ extractionIncomplete: false });
expect((await documentDetail(document.id)).body.data).toMatchObject({
extractionIncomplete: false,
});
});
it("records it from a background run as well", async () => {
const user = await makeUser();
await grantConsent(user.id);
const document = await indexedDocument(user.id);
await signIn(user.id);
await answerIncompleteOnce();
const queued = await postStoredExtract(document.id, true);
expect(queued.status).toBe(202);
const { runId } = queued.body.data as { runId: string };
expect(await work(runId)).toBe("succeeded");
expect((await poll(runId)).body.data).toMatchObject({
result: { incomplete: true },
});
expect((await documentDetail(document.id)).body.data).toMatchObject({
extractionIncomplete: true,
});
});
});