import { AiTraceService } from './ai-trace.service'; describe('AiTraceService receipt masking', () => { const prismaMock = { aiTrace: { findFirst: jest.fn(), findMany: jest.fn(), }, flyerSession: { findMany: jest.fn(), findUnique: jest.fn(), }, }; const service = new AiTraceService(prismaMock as any); beforeEach(() => { jest.clearAllMocks(); }); it('masks sensitive data in receipt prompt and rawOutput', async () => { prismaMock.aiTrace.findFirst.mockResolvedValue({ id: 42, source: 'receipt', status: 'success', createdAt: new Date('2026-05-21T10:00:00.000Z'), userId: 7, sessionId: null, model: 'importer-receipt-ai', durationMs: 240, error: null, prompt: 'Kund email anna@example.com och telefon 070-123 45 67', rawOutput: JSON.stringify({ personnummer: '850101-1234', email: 'anna@example.com', nested: { namn: 'Anna Andersson', phone: '+46701234567', }, }), normalizedOutput: { items: [ { rawName: 'Mjolk', customerEmail: 'anna@example.com', }, ], }, user: { username: 'admin', email: 'admin@example.com', }, }); const result = await service.getTraceById('receipt-42'); expect(result.prompt).not.toContain('anna@example.com'); expect(result.prompt).not.toContain('070-123 45 67'); expect(result.prompt).toContain('[MASKED]'); expect(result.rawOutput).not.toContain('850101-1234'); expect(result.rawOutput).not.toContain('anna@example.com'); expect(result.rawOutput).not.toContain('Anna Andersson'); expect(result.rawOutput).toContain('[MASKED]'); expect(result.normalizedOutput).toEqual({ items: [ { rawName: 'Mjolk', customerEmail: '[MASKED]', }, ], }); }); it('filters flyer list by errors in database query', async () => { prismaMock.flyerSession.findMany.mockResolvedValue([]); await service.listTraces({ source: 'flyer', limit: 20, onlyErrors: true, }); expect(prismaMock.flyerSession.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ items: { none: {} }, }), }), ); }); it('returns flyer prompt/rawOutput and trace counters from aiTrace supplement', async () => { prismaMock.flyerSession.findUnique.mockResolvedValue({ id: 101, userId: 7, createdAt: new Date('2026-05-21T12:00:00.000Z'), sourceFileName: 'willys.pdf', sourceMimeType: 'application/pdf', sourceFileSize: 12345, user: { username: 'admin', email: 'admin@example.com' }, items: [ { id: 1, rawName: 'Tomat', normalizedName: 'tomat', brand: null, categoryHint: 'Grönsaker', categoryId: null, price: null, priceUnit: null, comparisonPrice: null, comparisonUnit: null, weight: null, bundleWeight: null, isBundle: false, bundleItems: [], offerText: null, parseConfidence: 0.9, parseReasons: ['low_confidence'], matchedProductId: null, matchedProductName: null, matchedVia: 'none', matchConfidence: null, matchReasons: [], }, ], }); prismaMock.aiTrace.findMany.mockResolvedValue([ { sessionId: 101, prompt: 'Flyer prompt med email kund@example.com', rawOutput: '{"ok":true}', normalizedOutput: { retryCount: 2, chunkCount: 4 }, }, ]); const result = await service.getTraceById('flyer-101'); expect(result.prompt).toContain('[MASKED]'); expect(result.rawOutput).toContain('{"ok":true}'); expect(result.retryCount).toBe(2); expect(result.chunkCount).toBe(4); expect(result.warnings).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 'parse', code: 'low_confidence', title: 'Låg parsningskvalitet', severity: 'warning', }), ]), ); expect(result.legacyWarnings).toContain('parse:low_confidence'); }); it('keeps multiple token_overlap warnings for same row', async () => { prismaMock.flyerSession.findUnique.mockResolvedValue({ id: 202, userId: 9, createdAt: new Date('2026-05-23T09:00:00.000Z'), sourceFileName: 'willys-v21.pdf', sourceMimeType: 'application/pdf', sourceFileSize: 2222, user: { username: 'admin', email: 'admin@example.com' }, items: [ { id: 11, rawName: 'Tomatmix', normalizedName: 'tomatmix', brand: null, categoryHint: 'Grönsaker', categoryId: null, price: null, priceUnit: null, comparisonPrice: null, comparisonUnit: null, weight: null, bundleWeight: null, isBundle: false, bundleItems: [], offerText: null, parseConfidence: 0.9, parseReasons: [], matchedProductId: null, matchedProductName: null, matchedVia: 'token', matchConfidence: 0.7, matchReasons: ['token_overlap:0.42', 'token_overlap:0.73'], }, ], }); prismaMock.aiTrace.findMany.mockResolvedValue([ { sessionId: 202, prompt: 'prompt', rawOutput: '{"ok":true}', normalizedOutput: { retryCount: 0, chunkCount: 1 }, }, ]); const result = await service.getTraceById('flyer-202'); const tokenWarnings = result.warnings.filter((warning) => warning.code === 'token_overlap'); expect(tokenWarnings).toHaveLength(2); expect(result.legacyWarnings).toEqual( expect.arrayContaining(['match:token_overlap:0.42', 'match:token_overlap:0.73']), ); }); });