Add full application: receipt scanning, auth, billing, and account deletion

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>
This commit is contained in:
Timo
2026-08-19 20:59:04 +02:00
parent 650a74da97
commit 84b9987c49
415 changed files with 96619 additions and 0 deletions

View File

@@ -0,0 +1,790 @@
/**
* Adversarial Spotlight & Interaction Contract Verification Suite
* Challenger M1_2 Empirical Harness
*/
import fs from "fs";
import path from "path";
import { ProcessedReceipt } from "../src/lib/schema/receipt";
// Simple assertion helper
function assert(condition: boolean, message: string) {
if (!condition) {
throw new Error(`[ASSERTION FAILED]: ${message}`);
}
}
function assertEquals<T>(actual: T, expected: T, message: string) {
if (actual !== expected) {
throw new Error(`[ASSERTION FAILED]: ${message} | Expected: ${expected}, Got: ${actual}`);
}
}
// Mock test receipt records
const mockReceipts: ProcessedReceipt[] = [
{
id: "rec-001",
merchant: { name: "Aral Tankstelle", address: "München", taxId: "DE12345", confidence: 0.99 },
date: { isoDate: "2026-08-15", time: "08:42", confidence: 0.98 },
documentType: "KASSENBON",
receiptNumber: "ARAL-9988",
currency: "EUR",
totalAmount: { value: 68.45, confidence: 0.99 },
netAmount: 57.52,
taxBreakdown: [{ ratePercent: 19, taxAmount: 10.93, netAmount: 57.52 }],
lineItems: [{ description: "Super Plus", quantity: 35.1, unitPrice: 1.95, price: 68.45, taxRate: 19 }],
suggestedCategory: "Tanken & KFZ",
validation: {
isMathValid: true,
isDuplicateSuspected: false,
needsUserReview: false,
reviewField: "none",
reviewReason: null,
},
imageHash: "hash-aral-001",
originalFileName: "aral.jpg",
fileSizeBytes: 184000,
createdAt: "2026-08-15T08:42:00.000Z",
updatedAt: "2026-08-15T08:42:00.000Z",
status: "ready",
},
{
id: "rec-002",
merchant: { name: "Trattoria Bella Vista", address: "Berlin", taxId: "DE98765", confidence: 0.95 },
date: { isoDate: "2026-08-14", time: "20:15", confidence: 0.95 },
documentType: "BEWIRTUNGSBELEG",
receiptNumber: "TRAT-1002",
currency: "EUR",
totalAmount: { value: 84.50, confidence: 0.96 },
netAmount: 70.97,
taxBreakdown: [
{ ratePercent: 7, taxAmount: 3.08, netAmount: 44.0 },
{ ratePercent: 19, taxAmount: 10.45, netAmount: 26.97 },
],
lineItems: [
{ description: "Pasta Tartufo", quantity: 2, unitPrice: 22.0, price: 44.0, taxRate: 7 },
{ description: "Vino Rosso", quantity: 1, unitPrice: 28.0, price: 28.0, taxRate: 19 },
{ description: "Trinkgeld Bewirtung", quantity: 1, unitPrice: 12.5, price: 12.5, taxRate: 0 },
],
suggestedCategory: "Bewirtung",
validation: {
isMathValid: true,
isDuplicateSuspected: false,
needsUserReview: false,
reviewField: "none",
reviewReason: null,
},
imageHash: "hash-trattoria-002",
originalFileName: "trattoria.jpg",
fileSizeBytes: 210000,
createdAt: "2026-08-14T20:15:00.000Z",
updatedAt: "2026-08-14T20:15:00.000Z",
status: "ready",
},
{
id: "rec-003",
merchant: { name: "REWE City", address: "Köln", taxId: "DE554433", confidence: 0.94 },
date: { isoDate: "2026-08-13", time: "17:30", confidence: 0.95 },
documentType: "KASSENBON",
receiptNumber: "REWE-7766",
currency: "EUR",
totalAmount: { value: 24.80, confidence: 0.95 },
netAmount: 22.73,
taxBreakdown: [
{ ratePercent: 7, taxAmount: 1.31, netAmount: 18.71 },
{ ratePercent: 19, taxAmount: 0.76, netAmount: 4.02 },
],
lineItems: [
{ description: "Bio Milch", quantity: 2, unitPrice: 1.49, price: 2.98, taxRate: 7 },
{ description: "Dinkelbrot", quantity: 1, unitPrice: 3.49, price: 3.49, taxRate: 7 },
{ description: "Küchenrolle", quantity: 1, unitPrice: 4.78, price: 4.78, taxRate: 19 },
],
suggestedCategory: "Material & Einkauf",
validation: {
isMathValid: false,
isDuplicateSuspected: false,
needsUserReview: true,
reviewField: "taxBreakdown",
reviewReason: "Abweichung",
},
imageHash: "hash-rewe-003",
originalFileName: "rewe.jpg",
fileSizeBytes: 165000,
createdAt: "2026-08-13T17:30:00.000Z",
updatedAt: "2026-08-13T17:30:00.000Z",
status: "needs_review",
},
];
// Replicate Spotlight Items generation logic for rigorous unit & contract testing
function buildCommandItems(options: {
isDe: boolean;
localReceipts: ProcessedReceipt[];
onClose: () => void;
onTriggerScan?: () => void;
onLoadDemo?: () => void;
onSelectReceipt?: (receipt: ProcessedReceipt) => void;
routerPush: (path: string) => void;
}) {
const { isDe, localReceipts, onClose, onTriggerScan, onLoadDemo, onSelectReceipt, routerPush } = options;
const actionItems = [
{
id: "action-upload",
title: isDe ? "+ Neuen Beleg hochladen / scannen" : "+ Upload / scan new receipt",
subtitle: isDe ? "Öffnet Datei-Dialog für Kassenbon-Upload" : "Opens file picker for receipt upload",
category: "actions" as const,
badge: "SCAN",
action: () => {
onClose();
if (onTriggerScan) {
onTriggerScan();
} else {
routerPush("/dashboard");
}
},
},
{
id: "action-demo",
title: isDe ? "Demo-Belege laden" : "Load demo receipts",
subtitle: isDe ? "Generiert Aral, Rewe & Trattoria Musterbelege" : "Seeds Aral, Rewe & Trattoria sample receipts",
category: "actions" as const,
badge: "DEMO",
action: () => {
onClose();
if (onLoadDemo) {
onLoadDemo();
} else {
routerPush("/dashboard");
}
},
},
{
id: "action-excel",
title: isDe ? "Excel-Export (.xlsx) erstellen" : "Generate Excel export (.xlsx)",
subtitle: isDe ? "Dual-Sheet Arbeitsmappe mit dynamischen =SUM() Formeln" : "Dual-sheet workbook with dynamic =SUM() formulas",
category: "actions" as const,
badge: "XLSX",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
{
id: "action-csv",
title: isDe ? "Buchhaltungs-CSV exportieren" : "Export accounting CSV",
subtitle: isDe ? "Semikolon, Dezimalkomma, UTF-8 BOM öffnet in Excel" : "Semicolon, decimal commas, UTF-8 BOM opens in Excel",
category: "actions" as const,
badge: "CSV",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
];
const receiptItems = localReceipts.map((r) => ({
id: `receipt-${r.id}`,
title: `${r.merchant?.name || "Unbekannter Händler"}${r.totalAmount?.value ? r.totalAmount.value.toFixed(2) + " €" : ""}`,
subtitle: `${r.date?.isoDate || "Kein Datum"}${r.suggestedCategory || "Beleg"} ${r.receiptNumber ? "• #" + r.receiptNumber : ""}`,
category: "receipts" as const,
badge: r.validation?.isMathValid ? "VALID" : "REVIEW",
action: () => {
onClose();
if (onSelectReceipt) {
onSelectReceipt(r);
} else {
routerPush("/dashboard/activity");
}
},
}));
const navItems = [
{
id: "nav-overview",
title: isDe ? "Dashboard Übersicht" : "Dashboard Overview",
subtitle: "/dashboard",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard");
},
},
{
id: "nav-activity",
title: isDe ? "Scan-Archiv & Belege" : "Scan Activity & Archive",
subtitle: "/dashboard/activity",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/activity");
},
},
{
id: "nav-export",
title: isDe ? "Export-Zentrale (Excel & CSV)" : "Export Dispatcher (Excel & CSV)",
subtitle: "/dashboard/export",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
{
id: "nav-settings",
title: isDe ? "System- & KI-Einstellungen" : "System & AI Settings",
subtitle: "/dashboard/settings",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/settings");
},
},
{
id: "nav-landing",
title: isDe ? "Landing Page & Hero Scanner" : "Landing Page & Hero Scanner",
subtitle: "/",
category: "navigation" as const,
badge: "HOME",
action: () => {
onClose();
routerPush("/");
},
},
];
return { actionItems, receiptItems, navItems };
}
function filterCommandItems(
items: { actionItems: any[]; receiptItems: any[]; navItems: any[] },
query: string
) {
const q = query.toLowerCase().trim();
const filteredActions = items.actionItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
const filteredReceipts = items.receiptItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
const filteredNav = items.navItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
return {
filteredActions,
filteredReceipts,
filteredNav,
filteredItems: [...filteredActions, ...filteredReceipts, ...filteredNav],
};
}
// ----------------- TEST SUITE -----------------
console.log("================================================================");
console.log("ADVERSARIAL CHALLENGER M1_2: SPOTLIGHT & INTERACTION CONTRACTS");
console.log("================================================================\n");
let passed = 0;
let total = 0;
function runTest(name: string, fn: () => void) {
total++;
try {
fn();
console.log(`${name}`);
passed++;
} catch (err: any) {
console.error(`${name}`);
console.error(` ${err.message}`);
throw err;
}
}
// 1. Keyboard Shortcut Listener Tests (TopNav.tsx & Global contracts)
runTest("TopNav keydown: Cmd+k triggers modal toggle on macOS", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventMac = { metaKey: true, ctrlKey: false, key: "k", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventMac.metaKey || eventMac.ctrlKey) && eventMac.key.toLowerCase() === "k") {
eventMac.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on metaKey + k");
assertEquals(eventMac.defaultPrevented, true, "Default event should be prevented");
});
runTest("TopNav keydown: Ctrl+k triggers modal toggle on Windows/Linux", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventWin = { metaKey: false, ctrlKey: true, key: "k", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventWin.metaKey || eventWin.ctrlKey) && eventWin.key.toLowerCase() === "k") {
eventWin.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on ctrlKey + k");
assertEquals(eventWin.defaultPrevented, true, "Default event should be prevented");
});
runTest("TopNav keydown: Case-insensitive 'K' (Caps Lock or Shift) is recognized", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventShift = { metaKey: true, ctrlKey: false, key: "K", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventShift.metaKey || eventShift.ctrlKey) && eventShift.key.toLowerCase() === "k") {
eventShift.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on metaKey + uppercase K");
});
runTest("TopNav keydown: Unrelated keys (e.g. Cmd+S, Ctrl+P, 'k' alone) do NOT trigger Spotlight", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const tests = [
{ metaKey: true, ctrlKey: false, key: "s" },
{ metaKey: false, ctrlKey: true, key: "p" },
{ metaKey: false, ctrlKey: false, key: "k" },
{ metaKey: true, ctrlKey: false, key: "Meta" },
];
for (const e of tests) {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
toggle();
}
}
assertEquals(isOpen, false, "Spotlight should not open on non-K shortcuts or K without modifier");
});
// 2. SpotlightDialog Arrow Navigation & Selection
runTest("SpotlightDialog keydown: ArrowDown navigates cyclically", () => {
const items = [1, 2, 3, 4];
let selectedIndex = 0;
const navigateDown = () => {
selectedIndex = items.length > 0 ? (selectedIndex + 1) % items.length : 0;
};
navigateDown(); // 1
assertEquals(selectedIndex, 1, "Should move to index 1");
navigateDown(); // 2
assertEquals(selectedIndex, 2, "Should move to index 2");
navigateDown(); // 3
assertEquals(selectedIndex, 3, "Should move to index 3");
navigateDown(); // 0 (wrap-around)
assertEquals(selectedIndex, 0, "Should wrap around to index 0");
});
runTest("SpotlightDialog keydown: ArrowUp navigates cyclically backwards", () => {
const items = [1, 2, 3, 4];
let selectedIndex = 0;
const navigateUp = () => {
selectedIndex = items.length > 0 ? (selectedIndex - 1 + items.length) % items.length : 0;
};
navigateUp(); // 3 (wrap-around backwards)
assertEquals(selectedIndex, 3, "Should wrap backwards to index 3");
navigateUp(); // 2
assertEquals(selectedIndex, 2, "Should move to index 2");
navigateUp(); // 1
assertEquals(selectedIndex, 1, "Should move to index 1");
navigateUp(); // 0
assertEquals(selectedIndex, 0, "Should move to index 0");
});
runTest("SpotlightDialog keydown: Arrow navigation with empty results does not throw or return NaN", () => {
const items: any[] = [];
let selectedIndex = 0;
const navigateDown = () => {
selectedIndex = items.length > 0 ? (selectedIndex + 1) % items.length : 0;
};
const navigateUp = () => {
selectedIndex = items.length > 0 ? (selectedIndex - 1 + items.length) % items.length : 0;
};
navigateDown();
assertEquals(selectedIndex, 0, "Empty list down should be 0");
navigateUp();
assertEquals(selectedIndex, 0, "Empty list up should be 0");
assert(!Number.isNaN(selectedIndex), "Index must not be NaN");
});
runTest("SpotlightDialog keydown: Escape triggers onClose", () => {
let closed = false;
const onClose = () => { closed = true; };
const eventEsc = { key: "Escape", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if (eventEsc.key === "Escape") {
eventEsc.preventDefault();
onClose();
}
assertEquals(closed, true, "Escape must close dialog");
assertEquals(eventEsc.defaultPrevented, true, "Escape must prevent default");
});
runTest("SpotlightDialog keydown: Enter invokes action of selected item", () => {
let actionExecuted = false;
const items = [
{ id: "1", action: () => { actionExecuted = false; } },
{ id: "2", action: () => { actionExecuted = true; } },
];
const selectedIndex = 1;
const eventEnter = { key: "Enter", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if (eventEnter.key === "Enter") {
eventEnter.preventDefault();
if (items[selectedIndex]) {
items[selectedIndex].action();
}
}
assertEquals(actionExecuted, true, "Enter must execute selected item action");
assertEquals(eventEnter.defaultPrevented, true, "Enter must prevent default");
});
runTest("SpotlightDialog keydown: Enter with empty list is safely ignored", () => {
const items: any[] = [];
const selectedIndex = 0;
let executed = false;
if (items[selectedIndex]) {
items[selectedIndex].action();
executed = true;
}
assertEquals(executed, false, "Enter on empty list should do nothing safely");
});
// 3. Search Filtering Adversarial Cases
runTest("Search Filtering: Empty query returns all 4 actions, all receipts, and all 5 navigation links", () => {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
const { filteredActions, filteredReceipts, filteredNav, filteredItems } = filterCommandItems(items, "");
assertEquals(filteredActions.length, 4, "Should have 4 actions");
assertEquals(filteredReceipts.length, 3, "Should have 3 receipts");
assertEquals(filteredNav.length, 5, "Should have 5 navigation links");
assertEquals(filteredItems.length, 12, "Total items should be 12");
});
runTest("Search Filtering: Case-insensitive & whitespace trimmed matching", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const r1 = filterCommandItems(items, " ARAL ");
assertEquals(r1.filteredReceipts.length, 1, "Should find Aral receipt");
assertEquals(r1.filteredReceipts[0].title.includes("Aral"), true, "Title must contain Aral");
const r2 = filterCommandItems(items, "TrAtToRiA");
assertEquals(r2.filteredReceipts.length, 1, "Should find Trattoria receipt");
const r3 = filterCommandItems(items, " rewe ");
assertEquals(r3.filteredReceipts.length, 1, "Should find Rewe receipt");
});
runTest("Search Filtering: Filter by currency amount and receipt number", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
// Search by amount "68.45"
const rAmount = filterCommandItems(items, "68.45");
assertEquals(rAmount.filteredReceipts.length, 1, "Should match 68.45 €");
assertEquals(rAmount.filteredReceipts[0].id, "receipt-rec-001", "Should be Aral receipt");
// Search by receipt number "TRAT-1002"
const rNum = filterCommandItems(items, "TRAT-1002");
assertEquals(rNum.filteredReceipts.length, 1, "Should match receipt number TRAT-1002");
assertEquals(rNum.filteredReceipts[0].id, "receipt-rec-002", "Should be Trattoria receipt");
});
runTest("Search Filtering: Filter by category tag (e.g. Bewirtung, Tanken)", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rCat = filterCommandItems(items, "Bewirtung");
assertEquals(rCat.filteredReceipts.length, 1, "Should match Bewirtung category");
assertEquals(rCat.filteredReceipts[0].id, "receipt-rec-002", "Should be Trattoria");
});
runTest("Search Filtering: Filter by Action names (e.g. 'Excel', 'CSV', 'Demo', 'Upload')", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
// Both export actions legitimately mention Excel (the CSV opens in it), so a
// bare "Excel" query must return both — filtering by the format token is what
// has to disambiguate them.
const rExcel = filterCommandItems(items, "Excel");
assertEquals(rExcel.filteredActions.length, 2, "Should find both export actions");
const rXlsx = filterCommandItems(items, ".xlsx");
assertEquals(rXlsx.filteredActions.length, 1, "Should find Excel action");
assertEquals(rXlsx.filteredActions[0].id, "action-excel", "ID must be action-excel");
const rCsv = filterCommandItems(items, "CSV");
assertEquals(rCsv.filteredActions.length, 1, "Should find accounting CSV action");
assertEquals(rCsv.filteredActions[0].id, "action-csv", "ID must be action-csv");
});
runTest("Search Filtering: Filter by navigation path (e.g. '/dashboard/export', 'settings')", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rSettings = filterCommandItems(items, "settings");
assertEquals(rSettings.filteredNav.length, 1, "Should find settings nav item");
assertEquals(rSettings.filteredNav[0].id, "nav-settings", "ID must be nav-settings");
const rActivity = filterCommandItems(items, "/dashboard/activity");
assertEquals(rActivity.filteredNav.length, 1, "Should find activity nav item by route substring");
});
runTest("Search Filtering: Zero-match search returns empty results gracefully", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rNone = filterCommandItems(items, "XYZ999NONEXISTENT");
assertEquals(rNone.filteredItems.length, 0, "No items should match");
assertEquals(rNone.filteredActions.length, 0, "No actions");
assertEquals(rNone.filteredReceipts.length, 0, "No receipts");
assertEquals(rNone.filteredNav.length, 0, "No nav");
});
// 4. Action Invocation & Routing Verification
runTest("Action Invocation: Quick Action 'upload' calls onClose and custom trigger or router fallback", () => {
let closed = false;
let scanTriggered = false;
let pushedRoute = "";
const itemsWithTrigger = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onTriggerScan: () => { scanTriggered = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithTrigger.actionItems.find((a) => a.id === "action-upload")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(scanTriggered, true, "onTriggerScan must be called");
assertEquals(pushedRoute, "", "routerPush should not be called when onTriggerScan is provided");
// Test fallback without onTriggerScan
closed = false;
pushedRoute = "";
const itemsWithoutTrigger = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutTrigger.actionItems.find((a) => a.id === "action-upload")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard", "routerPush should fallback to /dashboard");
});
runTest("Action Invocation: Quick Action 'demo' calls onClose and custom trigger or router fallback", () => {
let closed = false;
let demoTriggered = false;
let pushedRoute = "";
const itemsWithDemo = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onLoadDemo: () => { demoTriggered = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithDemo.actionItems.find((a) => a.id === "action-demo")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(demoTriggered, true, "onLoadDemo must be called");
// Fallback
closed = false;
const itemsWithoutDemo = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutDemo.actionItems.find((a) => a.id === "action-demo")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard", "routerPush should fallback to /dashboard");
});
runTest("Action Invocation: Quick Actions 'excel' and 'csv' route to /dashboard/export", () => {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
items.actionItems.find((a) => a.id === "action-excel")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/export", "Must route to /dashboard/export");
closed = false;
pushedRoute = "";
items.actionItems.find((a) => a.id === "action-csv")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/export", "Must route to /dashboard/export");
});
runTest("Action Invocation: Receipt item selection calls onSelectReceipt or routes to /dashboard/activity", () => {
let closed = false;
let selectedReceipt: any = null;
let pushedRoute = "";
const itemsWithSelect = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onSelectReceipt: (r) => { selectedReceipt = r; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithSelect.receiptItems[0].action();
assertEquals(closed, true, "onClose must be called");
assertEquals(selectedReceipt?.id, "rec-001", "Selected receipt must match rec-001");
// Fallback
closed = false;
const itemsWithoutSelect = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutSelect.receiptItems[0].action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/activity", "Must route to /dashboard/activity on fallback");
});
runTest("Action Invocation: Navigation links route to their respective pages", () => {
const routes = [
{ id: "nav-overview", expected: "/dashboard" },
{ id: "nav-activity", expected: "/dashboard/activity" },
{ id: "nav-export", expected: "/dashboard/export" },
{ id: "nav-settings", expected: "/dashboard/settings" },
{ id: "nav-landing", expected: "/" },
];
for (const r of routes) {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
const item = items.navItems.find((n) => n.id === r.id);
assert(item !== undefined, `Nav item ${r.id} should exist`);
item?.action();
assertEquals(closed, true, `onClose must be called for ${r.id}`);
assertEquals(pushedRoute, r.expected, `Target path must be ${r.expected}`);
}
});
// 5. English Localization Verification
runTest("Localization: English strings render appropriately when language === 'en'", () => {
const itemsEn = buildCommandItems({
isDe: false,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
assertEquals(itemsEn.actionItems[0].title.startsWith("+ Upload / scan"), true, "English action title");
assertEquals(itemsEn.actionItems[1].title, "Load demo receipts", "English demo title");
assertEquals(itemsEn.navItems[0].title, "Dashboard Overview", "English overview nav title");
});
// 6. Source Code Static Token & Lifecycle Invariant Audits
runTest("Source Audit: SpotlightDialog.tsx contains zero rounded-, shadow-, or gradient classes", () => {
const spotlightCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/SpotlightDialog.tsx"), "utf-8");
const forbiddenPatterns = [
/\brounded(-[a-z0-9]+)?\b/g,
/\bshadow(-[a-z0-9]+)?\b/g,
/\bbg-gradient-[a-z0-9-]+\b/g,
];
for (const pattern of forbiddenPatterns) {
const matches = spotlightCode.match(pattern);
assertEquals(matches, null, `Found forbidden style match ${matches} in SpotlightDialog.tsx`);
}
});
runTest("Source Audit: TopNav.tsx contains zero rounded-, shadow-, or gradient classes", () => {
const topNavCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/TopNav.tsx"), "utf-8");
const forbiddenPatterns = [
/\brounded(-[a-z0-9]+)?\b/g,
/\bshadow(-[a-z0-9]+)?\b/g,
/\bbg-gradient-[a-z0-9-]+\b/g,
];
for (const pattern of forbiddenPatterns) {
const matches = topNavCode.match(pattern);
assertEquals(matches, null, `Found forbidden style match ${matches} in TopNav.tsx`);
}
});
runTest("Source Audit: Both TopNav.tsx and SpotlightDialog.tsx properly unbind keydown event listeners", () => {
const topNavCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/TopNav.tsx"), "utf-8");
const spotlightCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/SpotlightDialog.tsx"), "utf-8");
assert(topNavCode.includes('window.removeEventListener("keydown"'), "TopNav must unbind keydown");
assert(spotlightCode.includes('window.removeEventListener("keydown"'), "SpotlightDialog must unbind keydown");
});
console.log("\n================================================================");
console.log(`RESULTS: ${passed} / ${total} TESTS PASSED (100% SUCCESS)`);
console.log("================================================================\n");