Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
/**
|
|
* Sprint B: scanner speed — client prepare, vision page pick, reasoning
|
|
* retry policy, provider timeout constant, PDF raster target.
|
|
*/
|
|
|
|
import { describe, test, expect } from "./runner";
|
|
import {
|
|
AI_IMAGE_LONG_EDGE_PX,
|
|
MAX_VISION_PAGES,
|
|
selectVisionPages,
|
|
} from "../../src/lib/image/processor";
|
|
import { prepareUploadFile, CLIENT_UPLOAD_SKIP_BELOW_BYTES } from "../../src/lib/image/prepareUpload";
|
|
import {
|
|
PROVIDER_TIMEOUT_MS,
|
|
isRateLimitError,
|
|
shouldRetryWithReasoning,
|
|
} from "../../src/lib/ai/extractor";
|
|
import { PENDING_VALIDATION, ReceiptData } from "../../src/lib/schema/receipt";
|
|
|
|
function receipt(mathValid: boolean): ReceiptData {
|
|
return {
|
|
merchant: { name: "REWE", address: null, taxId: null, confidence: 0.97 },
|
|
date: { isoDate: "2026-08-12", time: null, confidence: 0.95 },
|
|
documentType: "KASSENBON",
|
|
receiptNumber: null,
|
|
currency: "EUR",
|
|
totalAmount: { value: 11.9, confidence: 0.98 },
|
|
netAmount: 10.0,
|
|
taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }],
|
|
lineItems: [],
|
|
suggestedCategory: "Sonstiges",
|
|
validation: { ...PENDING_VALIDATION, isMathValid: mathValid },
|
|
};
|
|
}
|
|
|
|
describe("Sprint B — Vision-Seiten", () => {
|
|
test("kurze Dokumente behalten alle Seiten", () => {
|
|
expect(selectVisionPages([1, 2, 3])).toEqual([1, 2, 3]);
|
|
expect(selectVisionPages([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
});
|
|
|
|
test("lange Dokumente: erste 6 + letzte 2, ohne Duplikate", () => {
|
|
const pages = Array.from({ length: 20 }, (_, i) => i + 1);
|
|
expect(selectVisionPages(pages)).toEqual([1, 2, 3, 4, 5, 6, 19, 20]);
|
|
expect(selectVisionPages(pages).length).toBeLessThanOrEqual(MAX_VISION_PAGES);
|
|
});
|
|
|
|
test("7 Seiten überlappen sich nicht doppelt", () => {
|
|
expect(selectVisionPages([1, 2, 3, 4, 5, 6, 7])).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
});
|
|
});
|
|
|
|
describe("Sprint B — Reasoning on-demand", () => {
|
|
test("Math-Fehler löst Reasoning-Retry aus", () => {
|
|
expect(shouldRetryWithReasoning(receipt(false))).toBe(true);
|
|
});
|
|
|
|
test("valide Math braucht kein Reasoning", () => {
|
|
expect(shouldRetryWithReasoning(receipt(true))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Sprint B — Rate-Limit & Timeout", () => {
|
|
test("429 wird als retryable erkannt", () => {
|
|
expect(isRateLimitError({ status: 429, message: "Too Many Requests" })).toBe(true);
|
|
expect(isRateLimitError(new Error("HTTP 429 rate_limit"))).toBe(true);
|
|
expect(isRateLimitError(new Error("timeout"))).toBe(false);
|
|
});
|
|
|
|
test("Provider-Timeout ist 12s", () => {
|
|
expect(PROVIDER_TIMEOUT_MS).toBe(12_000);
|
|
});
|
|
});
|
|
|
|
describe("Sprint B — Bildpipeline", () => {
|
|
test("PDF-Raster und AI-JPEG teilen die 1536px-Kante", () => {
|
|
expect(AI_IMAGE_LONG_EDGE_PX).toBe(1536);
|
|
});
|
|
});
|
|
|
|
describe("Sprint B — Client-Prepare", () => {
|
|
test("PDF bleibt unverändert", async () => {
|
|
const pdf = new File([new Uint8Array([0x25, 0x50, 0x44, 0x46])], "invoice.pdf", {
|
|
type: "application/pdf",
|
|
});
|
|
const out = await prepareUploadFile(pdf);
|
|
expect(out).toBe(pdf);
|
|
});
|
|
|
|
test("kleine JPEGs unter dem Skip-Limit bleiben unverändert", async () => {
|
|
const bytes = new Uint8Array(Math.min(1024, CLIENT_UPLOAD_SKIP_BELOW_BYTES));
|
|
const file = new File([bytes], "tiny.jpg", { type: "image/jpeg" });
|
|
Object.defineProperty(file, "size", { value: 12_000 });
|
|
const out = await prepareUploadFile(file);
|
|
expect(out).toBe(file);
|
|
});
|
|
});
|