import { describe, expect, it } from 'vitest'; import { MockMaintenanceFs, MockMaintenanceDb, scanOrphanFiles, cleanOrphanFiles, runMaintenance } from '../src/services/maintenance'; import { AiVisionProcessor, MockAiVisionProcessor } from '../src/ocr/AiVisionProcessor'; import { setupDeepLinking, parseDeepLink } from '../src/services/deepLink'; import { generateRemark } from '../src/domain/remarkTemplate'; import type { ImportedEvent } from '../src/domain/types'; const evt = (over: Partial = {}): ImportedEvent => ({ id: 'e1', occurredAt: '2026-07-13T14:30:00', amount: '100', currency: 'CNY', direction: 'expense', channel: 'Alipay', counterparty: '星巴克', memo: '拿铁', raw: { fee: '0.5', tags: 'coffee' }, ...over, }); // ============ maintenance ============ describe('maintenance', () => { it('scanOrphanFiles 扫描孤立附件', async () => { const fs = new MockMaintenanceFs(); fs.setFile('/attachments/receipt1.jpg', 1024); // 被引用 fs.setFile('/attachments/orphan.png', 512); // 孤立 fs.setFile('/thumbnails/thumb1.png', 256); // 缩略图 const orphans = await scanOrphanFiles( fs, new Set(['/attachments/receipt1.jpg']), { attachments: '/attachments', thumbnails: '/thumbnails', cache: '/cache' }, ); expect(orphans).toHaveLength(2); // orphan.png + thumb1.png expect(orphans.some(o => o.path === '/attachments/orphan.png')).toBe(true); }); it('cleanOrphanFiles 删除孤立文件', async () => { const fs = new MockMaintenanceFs(); fs.setFile('/attachments/orphan.png', 512); const orphans = [{ type: 'attachment' as const, path: '/attachments/orphan.png', size: 512 }]; const cleaned = await cleanOrphanFiles(fs, orphans); expect(cleaned).toBe(1); expect(fs.deleted).toContain('/attachments/orphan.png'); }); it('runMaintenance 完整维护', async () => { const fs = new MockMaintenanceFs(); fs.setFile('/cache/tmp.txt', 100); const db = new MockMaintenanceDb(); db.issues = ['测试问题']; const report = await runMaintenance( fs, db, new Set(), { attachments: '/attachments', thumbnails: '/thumbnails', cache: '/cache' }, ); expect(report.cleanedCount).toBe(1); // 清理了 cache/tmp.txt expect(db.vacuumed).toBe(true); expect(report.dbIssues).toEqual(['测试问题']); }); }); // ============ AiVisionProcessor ============ describe('AiVisionProcessor', () => { it('未启用时返回 null', async () => { const processor = new AiVisionProcessor({ enabled: false, apiUrl: '', apiKey: '', model: '' }); expect(await processor.recognizeBill('img')).toBeNull(); }); it('Mock 返回预设结果', async () => { const mock = new MockAiVisionProcessor({ id: 'ai1', occurredAt: '2026-07-13', amount: '50', currency: 'CNY', direction: 'expense', channel: 'AI', counterparty: '测试', memo: '', raw: {}, }); const result = await mock.recognizeBill('img'); expect(result?.amount).toBe('50'); }); it('Mock setResponse 切换结果', async () => { const mock = new MockAiVisionProcessor(null); expect(await mock.recognizeBill('img')).toBeNull(); mock.setResponse({ id: 'ai2', occurredAt: 'now', amount: '30', currency: 'CNY', direction: 'income', channel: 'AI', counterparty: '', memo: '', raw: {} }); expect((await mock.recognizeBill('img'))?.direction).toBe('income'); }); }); // ============ deepLink setupDeepLinking ============ describe('deepLink setupDeepLinking', () => { it('返回清理函数(测试环境降级)', () => { const cleanup = setupDeepLinking(); expect(typeof cleanup).toBe('function'); cleanup(); // 不抛错 }); it('注入 handler 被调用', () => { const actions: string[] = []; const cleanup = setupDeepLinking((action) => actions.push(action.type)); // 测试环境无 Linking,handler 不会被调用,但 setup 不报错 cleanup(); expect(actions).toHaveLength(0); }); it('parseDeepLink 仍可用', () => { expect(parseDeepLink('beanmobile://tab/home')?.type).toBe('open-tab'); }); }); // ============ remarkTemplate 16 占位符 ============ describe('remarkTemplate 完整占位符', () => { it('商品名称占位符', () => { const remark = generateRemark('【商品名称】', evt({ memo: '拿铁咖啡' })); expect(remark).toBe('拿铁咖啡'); }); it('货币类型占位符', () => { expect(generateRemark('【货币类型】', evt())).toBe('CNY'); }); it('手续费占位符', () => { expect(generateRemark('手续费【手续费】', evt())).toBe('手续费0.5'); }); it('标签占位符', () => { expect(generateRemark('标签:【标签】', evt())).toContain('coffee'); }); it('交易类型占位符', () => { expect(generateRemark('【交易类型】', evt())).toBe('expense'); }); it('多占位符组合', () => { const remark = generateRemark('【商户名称】【金额】【货币类型】', evt()); expect(remark).toBe('星巴克100CNY'); }); });