feat(ai): add AI trace tracking and admin panel
- Add AiTrace model to Prisma schema with relations to User - Implement AiTraceService with CRUD operations for AI traces - Add new admin panel for AI traces with filtering and detail views - Integrate trace persistence in receipt import flow - Add API endpoints for listing and retrieving AI traces - Update Flutter admin UI with new AI tab and navigation - Add new domain models for AI traces and details - Add migration for AiTrace table creation BREAKING CHANGE: None
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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: {} },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user