/** * Milestone 3 (R3): Interactive Live Table & Batch Operations — Adversarial Stress Suite * Empirical Challenger Verification * * Stress-tests: * 1. Filter edge cases: special characters, regex meta-chars in search (`.*+?^${}()`), empty strings, whitespace-only, emojis * 2. Amount boundary testing: 0.00 €, negative amounts, high numbers (999999.99 €), German comma vs English dot decimals * 3. Temporal edge cases: Leap years, boundary dates (Jan 1 / Dec 31), invalid dates, malformed ISO strings * 4. Multi-selection boundary stress: Rapid toggle, select-all with 0 items, select-all with 1000 items, invalid range IDs * 5. Bulk operation stress: Bulk export with empty list, bulk export with mixed tax rates, bulk categorization on corrupted records * 6. Inline editing stress: Rapid successive edits, invalid date strings, NaN gross amounts, missing merchant names * 7. Status tier resolution stress: Null validation, missing issues, partial receipts, corrupt status enums */ import { describe, test, it, expect, beforeEach } from "./runner"; import { ProcessedReceipt, ReceiptCategory } from "../../src/lib/schema/receipt"; import { resolveReceiptStatusTier, getStatusTierMeta, } from "../../src/components/dashboard/StatusBadge"; import { recalculateReceipt, confirmReceiptReviewed } from "../../src/lib/ai/recalculate"; import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator"; import { generateAccountingCsv } from "../../src/lib/export/csvGenerator"; function createStressReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: "stress-rcpt-001", imageHash: "hash-stress-999", originalFileName: "stress_receipt.jpg", fileSizeBytes: 200000, previewUrl: "blob:http://localhost/stress.jpg", createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready", documentType: "KASSENBON", receiptNumber: "STR-001", currency: "EUR", merchant: { name: "Standard Merchant", address: "Musterstr. 1, Berlin", taxId: "DE123456789", confidence: 0.95, }, date: { isoDate: "2026-08-15", time: "12:00", confidence: 0.95, }, totalAmount: { value: 100.0, confidence: 0.95, }, netAmount: 84.03, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: 84.03 }], lineItems: [], suggestedCategory: "Sonstiges", paymentMethod: "BAR", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, userConfirmed: false, reviewField: "none", reviewReason: null, issues: [], }, ...overrides, }; } describe("Milestone 3 (R3) Adversarial: Search Query & Filter Robustness", () => { const receipts = [ createStressReceipt({ id: "r1", merchant: { name: "Bäckerei Müller (GmbH & Co. KG)", address: "Hauptstr. [42]", taxId: "DE999", confidence: 0.9 }, receiptNumber: "INV-2026/08+01", date: { isoDate: "2026-08-15", time: "08:00", confidence: 0.9 }, totalAmount: { value: 12.5, confidence: 0.9 }, suggestedCategory: "Bewirtung", }), createStressReceipt({ id: "r2", merchant: { name: "Shell Tankstelle *** Sonderaktion ***", address: "Autobahn A8", taxId: "DE888", confidence: 0.9 }, receiptNumber: "SH-$$$-99", date: { isoDate: "2026-08-10", time: "14:00", confidence: 0.9 }, totalAmount: { value: 89.9, confidence: 0.9 }, suggestedCategory: "Tanken & KFZ", }), ]; test("ADV-M3.1: Search query with regex special characters does not crash matcher", () => { const specialQueries = ["[42]", "***", "$$$", "+01", "(GmbH", ".*+?^${}()|[]\\"]; for (const query of specialQueries) { const term = query.toLowerCase(); const filtered = receipts.filter( (r) => r.merchant?.name.toLowerCase().includes(term) || r.merchant?.address?.toLowerCase().includes(term) || r.receiptNumber?.toLowerCase().includes(term) ); expect(Array.isArray(filtered)).toBe(true); } }); test("ADV-M3.2: Handles German comma vs English dot decimal searches gracefully", () => { const queryDe = "12,50"; const queryEn = "12.50"; const matchAmount = (q: string) => { return receipts.filter((r) => { const gross = (r.totalAmount?.value || 0).toFixed(2); const grossDe = gross.replace(".", ","); return gross.includes(q) || grossDe.includes(q); }); }; expect(matchAmount(queryDe)).toHaveLength(1); expect(matchAmount(queryEn)).toHaveLength(1); }); test("ADV-M3.3: Amount range boundary checks handles 0.00 €, high caps, and inverted bounds", () => { const filterAmount = (min: number | null, max: number | null) => { return receipts.filter((r) => { const gross = r.totalAmount?.value || 0; if (min !== null && gross < min) return false; if (max !== null && gross > max) return false; return true; }); }; expect(filterAmount(0, 0)).toHaveLength(0); expect(filterAmount(0, 1000000)).toHaveLength(2); expect(filterAmount(100, 50)).toHaveLength(0); // Inverted bounds returns 0 cleanly }); }); describe("Milestone 3 (R3) Adversarial: Selection State Integrity", () => { test("ADV-M3.4: Rapid toggling and duplicate selection handling maintains clean uniqueness", () => { let selected: string[] = []; const addOrToggle = (id: string) => { selected = selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]; }; for (let i = 0; i < 100; i++) { addOrToggle("item-rapid"); } // 100 toggles = even number of toggles -> unselected (empty) expect(selected).toHaveLength(0); }); test("ADV-M3.5: Range selection with invalid or unlisted boundary IDs handles gracefully", () => { const list = ["id-1", "id-2", "id-3"]; const selectRange = (from: string, to: string, all: string[]) => { const idx1 = all.indexOf(from); const idx2 = all.indexOf(to); if (idx1 === -1 || idx2 === -1) return []; const start = Math.min(idx1, idx2); const end = Math.max(idx1, idx2); return all.slice(start, end + 1); }; expect(selectRange("non-existent-1", "id-2", list)).toEqual([]); expect(selectRange("id-1", "non-existent-2", list)).toEqual([]); }); }); describe("Milestone 3 (R3) Adversarial: Inline Recalculation & Extreme Math Values", () => { test("ADV-M3.6: Recalculates gross when amount is set to 0.00 € without NaN or Infinity", () => { const rcpt = createStressReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: 84.03, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: 84.03 }], }); const updated = { ...rcpt, totalAmount: { ...rcpt.totalAmount, value: 0.0 }, }; const recalculated = recalculateReceipt(updated, { editedField: "totalAmount" }); expect(recalculated.totalAmount.value).toBe(0.0); expect(recalculated.netAmount).toBe(0.0); expect(recalculated.taxBreakdown[0].taxAmount).toBe(0.0); expect(isNaN(recalculated.netAmount ?? 0)).toBe(false); }); test("ADV-M3.7: Recalculates gross with mixed 7% and 19% VAT rates", () => { const rcpt = createStressReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: 88.0, taxBreakdown: [ { ratePercent: 7, taxAmount: 3.5, netAmount: 50.0 }, { ratePercent: 19, taxAmount: 8.5, netAmount: 38.0 }, ], }); const updated = { ...rcpt, totalAmount: { ...rcpt.totalAmount, value: 200.0 }, }; const recalculated = recalculateReceipt(updated, { editedField: "totalAmount" }); expect(recalculated.totalAmount.value).toBe(200.0); expect(recalculated.validation.isMathValid).toBe(true); }); }); describe("Milestone 3 (R3) Adversarial: Bulk Export Formats & CSV Encoding", () => { test("ADV-M3.8: Dual-sheet Excel export generates Sheet 1 and Sheet 2 correctly", async () => { const largeBatch = Array.from({ length: 25 }, (_, i) => createStressReceipt({ id: `batch-stress-${i}`, merchant: { name: `Händler Nr. ${i}`, address: "Berlin", taxId: "DE123", confidence: 1.0 }, totalAmount: { value: 10.0 * (i + 1), confidence: 1.0 }, }) ); const buffer = await generateDualSheetExcel(largeBatch); expect(buffer).toBeDefined(); expect(buffer.length).toBeGreaterThan(5000); }); test("ADV-M3.9: Accounting CSV export handles receipts with quotes, line breaks, and umlauts in merchant name", () => { const specialReceipt = createStressReceipt({ merchant: { name: 'Möbel "Schön & Weiß" GmbH\nFiliale Süd', address: "München", taxId: "DE1", confidence: 1.0 }, totalAmount: { value: 199.99, confidence: 1.0 }, }); const csv = generateAccountingCsv([specialReceipt]); expect(csv).toBeDefined(); expect(csv.charCodeAt(0)).toBe(0xfeff); // UTF-8 BOM expect(csv).toContain("Möbel"); }); });