Files
accounted/lib/documents/__tests__/classifier.test.ts
T
Jakob Wennberg 3f255ea201 refactor: consolidate AI analyzers into shared lib/ai module and add journal entry reversal columns
Extract shared vision/document analysis logic into lib/ai (vision-client, document-analyzer,
image preprocessing, validation helpers), simplifying invoice-analyzer, receipt-analyzer, and
document classifier. Add migration 046 for journal entry reversal/correction link columns
(reversed_by_id, reverses_id, correction_of_id) required by storno service. Update extension
components and shared UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:14:44 +01:00

120 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock server-only
vi.mock('server-only', () => ({}))
// Mock the core document-analyzer module
const { mockClassify } = vi.hoisted(() => ({
mockClassify: vi.fn(),
}))
vi.mock('@/lib/ai/document-analyzer', () => ({
classifyDocument: mockClassify,
}))
import { classifyDocument } from '../classifier'
describe('classifyDocument', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('classifies a supplier invoice', async () => {
mockClassify.mockResolvedValueOnce({
type: 'supplier_invoice',
confidence: 0.95,
reasoning: 'Contains invoice number, bankgiro, and supplier details',
isReverseCharge: false,
})
const result = await classifyDocument('base64data', 'application/pdf')
expect(result.type).toBe('supplier_invoice')
expect(result.confidence).toBe(0.95)
expect(result.isReverseCharge).toBe(false)
})
it('classifies a receipt', async () => {
mockClassify.mockResolvedValueOnce({
type: 'receipt',
confidence: 0.92,
reasoning: 'Store receipt with line items and total',
})
const result = await classifyDocument('base64data', 'image/jpeg')
expect(result.type).toBe('receipt')
expect(result.confidence).toBe(0.92)
expect(result.isReverseCharge).toBeUndefined()
})
it('classifies a government letter', async () => {
mockClassify.mockResolvedValueOnce({
type: 'government_letter',
confidence: 0.88,
reasoning: 'Letter from Skatteverket',
})
const result = await classifyDocument('base64data', 'application/pdf')
expect(result.type).toBe('government_letter')
expect(result.confidence).toBe(0.88)
expect(result.isReverseCharge).toBeUndefined()
})
it('classifies unknown documents', async () => {
mockClassify.mockResolvedValueOnce({
type: 'unknown',
confidence: 0.5,
reasoning: 'Cannot determine document type',
})
const result = await classifyDocument('base64data', 'image/png')
expect(result.type).toBe('unknown')
expect(result.confidence).toBe(0.5)
})
it('detects reverse charge on EU invoices', async () => {
mockClassify.mockResolvedValueOnce({
type: 'supplier_invoice',
confidence: 0.93,
reasoning: 'EU invoice with reverse charge',
isReverseCharge: true,
})
const result = await classifyDocument('base64data', 'application/pdf')
expect(result.type).toBe('supplier_invoice')
expect(result.isReverseCharge).toBe(true)
})
it('delegates to core classify', async () => {
mockClassify.mockResolvedValueOnce({
type: 'receipt',
confidence: 0.9,
reasoning: 'Receipt',
})
await classifyDocument('base64data', 'image/jpeg')
expect(mockClassify).toHaveBeenCalledWith('base64data', 'image/jpeg')
})
it('propagates errors from core', async () => {
mockClassify.mockRejectedValueOnce(new Error('Vision API call failed after 3 attempts'))
await expect(classifyDocument('base64data', 'image/jpeg')).rejects.toThrow(
'Vision API call failed after 3 attempts'
)
})
it('propagates unsupported MIME type errors', async () => {
mockClassify.mockRejectedValueOnce(new Error('Unsupported file type: text/plain'))
await expect(classifyDocument('base64data', 'text/plain')).rejects.toThrow(
'Unsupported file type: text/plain'
)
})
})