test(intake): rules, PDF text, C2 idempotency, enrich-vs-candidates, pantry hand-off, Slack taps

Unit: merchant pattern, header validation, reconciliation tolerance, verbatim
line items, one-call message shape, card action ids; unpdf over the two real
Uber Eats exports (Coles $85.74 / 15 lines, Burgertory $22.40 / Michael +
Joel). Integration: fetch stubbed at the LiteLLM and pantry boundaries so the
PDF path, persistence, match and writes run for real against personal_test.
intake-live.test.ts is opt-in (INTAKE_LIVE_TEST=1) and was run green against
cloud-budget on both PDFs.
This commit is contained in:
2026-08-21 12:10:06 +10:00
parent c7ea516c34
commit 32a47bf8ee
5 changed files with 774 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";
import { extractPdfText, liteLLMExtractor } from "../../lib/intake-extract";
import { reconcileItems, toLineItems, validateHeader } from "../../lib/intake-rules";
/**
* Opt-in: the real model over the real Burgertory export.
*
* INTAKE_LIVE_TEST=1 LITELLM_BASE_URL=http://localhost:4200 LITELLM_KEY=... npm test
*
* Skipped otherwise — CI has no LiteLLM, and a unit run must not depend on one.
* What it proves is the thing the mock cannot: that `cloud-budget` keeps the
* names verbatim ("Michael", "Joel", not "Burger") under the order-item prompt.
*/
const live = process.env.INTAKE_LIVE_TEST === "1" && !!process.env.LITELLM_BASE_URL && !!process.env.LITELLM_KEY;
describe.skipIf(!live)("live extraction via LiteLLM (opt-in)", () => {
it("Burgertory PDF: header exact, two items named verbatim, reconciles", async () => {
const text = await extractPdfText(readFileSync(resolve(__dirname, "../fixtures/intake/ubereats-burgertory-2026-08-19.pdf")));
const extracted = await liteLLMExtractor()([{ kind: "text", text, name: "Receipt_19Aug2026.pdf" }]);
const header = validateHeader(extracted);
expect(header.ok && header.header).toEqual({ merchant: "Burgertory (Werribee)", order_date: "2026-08-19", total: 22.4 });
const items = toLineItems(extracted.items);
expect(items.map((i) => i.description)).toEqual(["Michael", "Joel"]);
expect(reconcileItems(extracted).ok).toBe(true);
}, 120_000);
it("Coles PDF: header exact, 15 items", async () => {
const text = await extractPdfText(readFileSync(resolve(__dirname, "../fixtures/intake/ubereats-coles-2026-08-18.pdf")));
const extracted = await liteLLMExtractor()([{ kind: "text", text, name: "Receipt_18Aug2026.pdf" }]);
const header = validateHeader(extracted);
expect(header.ok && header.header).toEqual({ merchant: "Coles (Wyndham Vale)", order_date: "2026-08-18", total: 85.74 });
expect(toLineItems(extracted.items)).toHaveLength(15);
expect(reconcileItems(extracted).ok).toBe(true);
}, 120_000);
});