DriftLedger/tests/maintenance.test.ts
fengmengqi f6437b83fe feat: 初始化完整应用框架
- 切换到 expo-router 文件路由,删除 App.tsx
- 新增 5 个 Expo 原生插件:ppocr (OCR), accessibility (账单抓取),
  notification-listener, screenshot-monitor, sms-receiver
- 实现核心领域逻辑:billPipeline (账单流水), dedup (去重),
  transferRecognizer (转账识别), ruleEngine + categories (双轨制分类),
  budgets, creditCards, recurring, sync, ocrProcessor
- 增强 ledger.ts:支持 balance assertion, option, pad/note 指令,
  posting 级 metadata, cost/price 解析
- 新增完整 UI:tabs (首页/报表/设置), 交易详情, 预算, 日历热力图,
  分类管理, 信用卡, 定期交易, 规则管理
- 实现 Zustand 状态管理:ledgerStore, importStore, settingsStore,
  metadataStore, automationStore + 持久化
- 新增 AI 功能:chatAssistant, monthlySummary, voiceInput
- 实现多端同步:gitSync, webdavSync, icloudSync
- 新增主题系统 (tokens/presets) 和 i18n (zh/en)
- 添加 30+ 单元测试覆盖核心逻辑
2026-07-15 10:01:31 +08:00

131 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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> = {}): 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));
// 测试环境无 Linkinghandler 不会被调用,但 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');
});
});