Pin the async-truth contract over both document state machines

Two contract tests enumerate every branch of the summary job and the
index tree and assert each ends in a terminal state write: the summary
job's opt-out branch heals a phantom PENDING back to NONE, every
failure branch lands UNAVAILABLE or WITHHELD, success lands READY; the
index tree records an attempt outcome on every terminal branch,
success included. A third block pins the enqueue side: no PENDING is
claimed when the job's only branch would be the opt-out no-op, and
none before a job id exists. Honest scope stated in the test header:
these pin the two known machines, not a generic detector.

Refs #776, #788
This commit is contained in:
Marc-André Bombeck committed 2026-08-13 12:42:33 +02:00
1 parent 893db887e9
commit dceca62f8e
1 file changed
+415
@@ -0,0 +1,415 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
/**
* The async-truth contract, pinned per state machine (v1.37.15).
*
* > A state that claims work is running must have a writer for every terminal
* > branch; an enqueue must never claim a pending state its job cannot
* > terminally resolve.
*
* Two machines are pinned — the document summary job and the document index
* tree. Honest scope: these enumerate the branches of the two KNOWN machines;
* a generic phantom-pending detector for arbitrary future jobs is not
* provable and is not claimed. When a branch is added to either job, add it
* here — the enumerations below are the contract.
*
* (a) Every branch of `runDocumentSummaryJob` ends in a terminal
* `summaryState` write — the opt-out branch included (it heals a phantom
* PENDING back to NONE). The only silent exits are "row gone" and
* "summary already stored", whose states are terminal by precondition.
* (b) Every branch of the index tree behind `runDocumentIndex` ends in an
* attempt record (`lastIndexAttemptAt` + `lastIndexOutcome`) — success
* included (it clears the outcome). The only silent exit is not-found.
* (c) `enqueueDocumentSummary` never claims PENDING when the job's only
* branch would be the opt-out no-op, and never before a job id exists.
*/
vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() }));
vi.mock("@/lib/db", () => ({
prisma: {
inboundDocument: { findFirst: vi.fn(), updateMany: vi.fn() },
user: { findUnique: vi.fn() },
},
}));
vi.mock("@/lib/jobs/boss-instance", () => ({ getGlobalBoss: vi.fn() }));
vi.mock("@/lib/documents/document-settings", () => ({
documentAutoReadEnabled: vi.fn(),
}));
vi.mock("@/lib/documents/provider-order", () => ({
resolveDocumentVisionProvider: vi.fn(),
}));
vi.mock("@/lib/documents/ai-route-support", () => ({
loadOwnedDocument: vi.fn(),
prepareVisionInput: vi.fn(),
}));
vi.mock("@/lib/documents/describe", () => ({
runDocumentSummary: vi.fn(),
transcribeDocument: vi.fn(),
}));
vi.mock("@/lib/documents/store", () => ({
encryptDocumentSummary: vi.fn(() => new Uint8Array([9])),
decryptDocumentContent: vi.fn(() => Buffer.from([1, 2, 3])),
}));
vi.mock("@/lib/documents/content-index", () => ({
upsertContentIndex: vi.fn().mockResolvedValue({ tokenCount: 5 }),
}));
vi.mock("@/lib/documents/local-extract", () => ({
localExtractText: vi.fn(),
}));
vi.mock("@/lib/documents/auto-stage-labs", () => ({
maybeAutoStageLabFacts: vi.fn().mockResolvedValue({ staged: false }),
}));
vi.mock("@/lib/labs/ocr-upload", () => ({
detectOcrMimeType: vi.fn(() => "application/pdf"),
}));
vi.mock("@/lib/ai/consent-guard", () => {
class ConsentRequiredError extends Error {}
return {
ConsentRequiredError,
assertDocumentEgressConsent: vi.fn(),
isExternalDocumentEgress: (providerType: string) =>
providerType !== "local",
};
});
vi.mock("@/lib/ai/coach/budget", () => ({
buildDateKey: vi.fn(() => "2026-08-13"),
reserveBudget: vi.fn(),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 1000),
}));
vi.mock("@/lib/ai/ai-budgets", () => ({
AI_BUDGETS: {
documentSummary: { temperature: 0.3, maxTokens: 600 },
documentTranscribe: { temperature: 0, maxTokens: 4000 },
},
}));
import {
enqueueDocumentSummary,
runDocumentSummaryJob,
} from "../document-summary";
import { runDocumentIndex } from "../document-index";
import { prisma } from "@/lib/db";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import { documentAutoReadEnabled } from "@/lib/documents/document-settings";
import { resolveDocumentVisionProvider } from "@/lib/documents/provider-order";
import {
loadOwnedDocument,
prepareVisionInput,
} from "@/lib/documents/ai-route-support";
import {
runDocumentSummary,
transcribeDocument,
} from "@/lib/documents/describe";
import { localExtractText } from "@/lib/documents/local-extract";
import {
assertDocumentEgressConsent,
ConsentRequiredError,
} from "@/lib/ai/consent-guard";
import { reserveBudget } from "@/lib/ai/coach/budget";
const DOC = {
id: "doc-1",
kind: "OTHER",
contentEncrypted: new Uint8Array([1, 2, 3]),
contentCodec: "binary2",
mimeType: "application/pdf",
status: "STORED",
};
const PICK = {
chain: [{ providerType: "anthropic", instance: {} }],
pick: {
entry: { providerType: "anthropic", instance: {} },
providerType: "anthropic",
pdfSupported: true,
},
};
/** All updateMany data objects that carried a summaryState. */
function summaryStateWrites(): unknown[] {
return vi
.mocked(prisma.inboundDocument.updateMany)
.mock.calls.filter(
([arg]) => arg.data && "summaryState" in (arg.data as object),
)
.map(([arg]) => (arg.data as { summaryState: unknown }).summaryState);
}
/** All updateMany data objects that carried the index-attempt record. */
function indexAttemptWrites(): unknown[] {
return vi
.mocked(prisma.inboundDocument.updateMany)
.mock.calls.filter(
([arg]) => arg.data && "lastIndexAttemptAt" in (arg.data as object),
)
.map(
([arg]) => (arg.data as { lastIndexOutcome: unknown }).lastIndexOutcome,
);
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(prisma.inboundDocument.findFirst).mockResolvedValue({
id: "doc-1",
summaryEncrypted: null,
} as never);
vi.mocked(prisma.inboundDocument.updateMany).mockResolvedValue({
count: 1,
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
locale: "en",
} as never);
vi.mocked(documentAutoReadEnabled).mockResolvedValue(true);
vi.mocked(resolveDocumentVisionProvider).mockResolvedValue(PICK as never);
vi.mocked(assertDocumentEgressConsent).mockResolvedValue(undefined);
vi.mocked(loadOwnedDocument).mockResolvedValue(DOC as never);
vi.mocked(prepareVisionInput).mockResolvedValue({
ok: true,
images: [],
documents: [{ mediaType: "application/pdf", dataBase64: "AA==" }],
} as never);
vi.mocked(runDocumentSummary).mockResolvedValue({
summary: "A short descriptive summary.",
blocked: null,
} as never);
vi.mocked(transcribeDocument).mockResolvedValue({
text: "haemoglobin 14.2",
} as never);
vi.mocked(reserveBudget).mockResolvedValue({
allowed: true,
reserved: 1,
} as never);
vi.mocked(localExtractText).mockResolvedValue({
ok: true,
text: "glucose fasting values",
source: "local-pdf",
} as never);
});
const PAYLOAD = { userId: "user-1", documentId: "doc-1" };
describe("contract (a): every runDocumentSummaryJob branch writes a terminal state", () => {
const branches: Array<{
name: string;
setup: () => void;
state: string;
}> = [
{
name: "opt-out heals PENDING to NONE",
setup: () => {
vi.mocked(documentAutoReadEnabled).mockResolvedValue(false);
},
state: "NONE",
},
{
name: "no provider configured",
setup: () => {
vi.mocked(resolveDocumentVisionProvider).mockResolvedValue({
chain: [],
pick: null,
} as never);
},
state: "UNAVAILABLE",
},
{
name: "consent refused",
setup: () => {
vi.mocked(assertDocumentEgressConsent).mockRejectedValue(
new ConsentRequiredError("no consent"),
);
},
state: "UNAVAILABLE",
},
{
name: "document unreadable by the picked provider",
setup: () => {
vi.mocked(prepareVisionInput).mockResolvedValue({
ok: false,
reason: "fileType",
} as never);
},
state: "UNAVAILABLE",
},
{
name: "budget exhausted",
setup: () => {
vi.mocked(reserveBudget).mockResolvedValue({
allowed: false,
reserved: 0,
} as never);
},
state: "UNAVAILABLE",
},
{
name: "safety screen blocked the summary",
setup: () => {
vi.mocked(runDocumentSummary).mockResolvedValue({
summary: "",
blocked: "dose_directive",
} as never);
},
state: "WITHHELD",
},
{
name: "provider error",
setup: () => {
vi.mocked(runDocumentSummary).mockRejectedValue(new Error("boom"));
},
state: "UNAVAILABLE",
},
{
name: "success",
setup: () => {},
state: "READY",
},
];
for (const branch of branches) {
it(`${branch.name} → writes ${branch.state}`, async () => {
branch.setup();
await runDocumentSummaryJob(PAYLOAD);
expect(summaryStateWrites()).toContain(branch.state);
});
}
it("the only silent exits are row-gone and already-summarised (terminal by precondition)", async () => {
vi.mocked(prisma.inboundDocument.findFirst).mockResolvedValue(
null as never,
);
await runDocumentSummaryJob(PAYLOAD);
expect(summaryStateWrites()).toEqual([]);
vi.clearAllMocks();
vi.mocked(prisma.inboundDocument.findFirst).mockResolvedValue({
id: "doc-1",
summaryEncrypted: new Uint8Array([1]),
} as never);
await runDocumentSummaryJob(PAYLOAD);
expect(summaryStateWrites()).toEqual([]);
});
});
describe("contract (a): every runDocumentIndex branch records the attempt", () => {
const branches: Array<{
name: string;
setup: () => void;
outcome: string | null;
}> = [
{
name: "provider success",
setup: () => {},
outcome: null,
},
{
name: "local success (no provider)",
setup: () => {
vi.mocked(resolveDocumentVisionProvider).mockResolvedValue({
chain: [],
pick: null,
} as never);
},
outcome: null,
},
{
name: "scan with no text and no provider",
setup: () => {
vi.mocked(resolveDocumentVisionProvider).mockResolvedValue({
chain: [],
pick: null,
} as never);
vi.mocked(localExtractText).mockResolvedValue({
ok: false,
reason: "empty",
} as never);
},
outcome: "local-empty",
},
{
name: "provider error with no local text",
setup: () => {
vi.mocked(transcribeDocument).mockRejectedValue(new Error("boom"));
vi.mocked(localExtractText).mockResolvedValue({
ok: false,
reason: "empty",
} as never);
},
outcome: "provider-error",
},
{
name: "empty transcription with no local text",
setup: () => {
vi.mocked(transcribeDocument).mockResolvedValue({
text: " ",
} as never);
vi.mocked(localExtractText).mockResolvedValue({
ok: false,
reason: "empty",
} as never);
},
outcome: "empty-transcription",
},
{
name: "raster failure with no local text",
setup: () => {
vi.mocked(prepareVisionInput).mockResolvedValue({
ok: false,
reason: "rasterFailed",
} as never);
vi.mocked(localExtractText).mockResolvedValue({
ok: false,
reason: "empty",
} as never);
},
outcome: "raster-failed",
},
{
name: "decrypt failure",
setup: () => {
vi.mocked(prepareVisionInput).mockResolvedValue({
ok: false,
reason: "decryptFailed",
} as never);
},
outcome: "decrypt-error",
},
];
for (const branch of branches) {
it(`${branch.name} → records outcome ${JSON.stringify(branch.outcome)}`, async () => {
branch.setup();
await runDocumentIndex(PAYLOAD);
expect(indexAttemptWrites()).toContain(branch.outcome);
});
}
it("the only silent exit is not-found (no row to explain)", async () => {
vi.mocked(loadOwnedDocument).mockResolvedValue(null as never);
await runDocumentIndex(PAYLOAD);
expect(indexAttemptWrites()).toEqual([]);
});
});
describe("contract (b): enqueue never claims a pending state its job cannot resolve", () => {
it("enqueueDocumentSummary with the opt-in OFF claims nothing and mints no job", async () => {
vi.mocked(documentAutoReadEnabled).mockResolvedValue(false);
const send = vi.fn().mockResolvedValue("job-1");
vi.mocked(getGlobalBoss).mockReturnValue({ send } as never);
const result = await enqueueDocumentSummary("user-1", "doc-1");
expect(result).toEqual({ enqueued: false });
expect(send).not.toHaveBeenCalled();
expect(summaryStateWrites()).toEqual([]);
});
it("enqueueDocumentSummary claims PENDING only once a job id exists", async () => {
const send = vi.fn().mockResolvedValue(null);
vi.mocked(getGlobalBoss).mockReturnValue({ send } as never);
await enqueueDocumentSummary("user-1", "doc-1");
expect(summaryStateWrites()).toEqual([]);
send.mockResolvedValue("job-1");
await enqueueDocumentSummary("user-1", "doc-1");
expect(summaryStateWrites()).toContain("PENDING");
});
});