DriftLedger/tests/ocr.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

156 lines
6.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 { matchOcrRule, parseOcrBill } from '../src/domain/ocr';
import { MockAiVisionProvider, MockOcrEngine, OcrProcessor, type OcrProcessResult } from '../src/domain/ocrProcessor';
describe('ocr - matchOcrRule', () => {
it('微信支付规则匹配', () => {
const text = '微信支付 金额 24.50 商户:星巴克 2026-07-13 14:30';
const result = matchOcrRule(text, 'com.tencent.mm');
expect(result.matched).toBe(true);
expect(result.ruleName).toBe('微信支付');
expect(result.event?.amount).toBe('-24.5');
expect(result.event?.counterparty).toBe('星巴克');
expect(result.event?.channel).toBe('WeChat');
});
it('支付宝规则匹配', () => {
const text = '支付宝 收款方:肯德基 金额 35.00 支出';
const result = matchOcrRule(text, 'com.eg.android.AlipayGphone');
expect(result.matched).toBe(true);
expect(result.ruleName).toBe('支付宝');
expect(result.event?.direction).toBe('expense');
});
it('银行卡规则匹配(含尾号)', () => {
const text = '尾号1234 信用卡 消费 100.50 商户:超市 2026-07-13 10:00';
const result = matchOcrRule(text);
expect(result.matched).toBe(true);
expect(result.ruleName).toBe('银行卡');
expect(result.event?.channel).toBe('Bank:1234');
expect(result.event?.memo).toContain('1234');
});
it('收入方向识别', () => {
const text = '微信支付 金额 5000 收款 2026-07-13 14:30';
const result = matchOcrRule(text);
expect(result.matched).toBe(true);
expect(result.event?.direction).toBe('income');
expect(result.event?.amount).toBe('5000'); // 收入为正
});
it('退款方向识别', () => {
const text = '支付宝 金额 30 退款 2026-07-13 14:30';
const result = matchOcrRule(text);
expect(result.event?.direction).toBe('refund');
});
it('无金额不匹配', () => {
const text = '微信支付 商户:星巴克';
const result = matchOcrRule(text);
expect(result.matched).toBe(false);
});
it('非账单内容不匹配', () => {
const result = matchOcrRule('今天天气真好');
expect(result.matched).toBe(false);
});
});
describe('ocr - parseOcrBill 退化解析', () => {
it('规则匹配命中时返回规则结果', () => {
const event = parseOcrBill('微信支付 金额 10 商户:测试');
expect(event?.channel).toBe('WeChat');
});
it('退化:提取金额 + 推断方向', () => {
const event = parseOcrBill('您有一笔 50元 消费');
expect(event).not.toBeNull();
expect(event?.amount).toBe('-50');
expect(event?.direction).toBe('expense');
});
it('无任何金额返回 null', () => {
expect(parseOcrBill('纯文本无金额')).toBeNull();
});
});
describe('OcrProcessor 分层处理', () => {
it('Layer 1 规则命中', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '微信支付 金额 24.50 商户:星巴克 2026-07-13 14:30';
const processor = new OcrProcessor(engine);
const result = await processor.process('img1', 'com.tencent.mm');
expect(result.layer).toBe('layer1-rule');
expect(result.event?.amount).toBe('-24.5');
});
it('Layer 2 退化解析(规则未命中但有金额)', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '某App 消费 88元';
const processor = new OcrProcessor(engine);
const result = await processor.process('img2');
expect(result.layer).toBe('layer2-ocr');
expect(result.event?.amount).toBe('-88');
});
it('无文本时跳过到 Layer 3未启用 AI → none', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '';
const processor = new OcrProcessor(engine);
const result = await processor.process('img3');
expect(result.layer).toBe('none');
expect(result.skipped).toBe('non-bill');
});
it('Layer 3 AI Vision 启用时降级', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '非账单的随机文本';
const aiProvider = new MockAiVisionProvider({
id: 'ai-vision', occurredAt: '2026-07-13', amount: '99', currency: 'CNY', direction: 'expense',
channel: 'AI', counterparty: 'AI识别', memo: '', raw: {},
});
const processor = new OcrProcessor(engine, aiProvider, { aiVisionEnabled: true, landscapeDnd: false });
const result = await processor.process('img4');
expect(result.layer).toBe('layer3-ai');
expect(result.event?.amount).toBe('99');
});
it('横屏免打扰跳过', async () => {
const engine = new MockOcrEngine();
const processor = new OcrProcessor(engine, undefined, { aiVisionEnabled: false, landscapeDnd: true });
const result = await processor.process('img5', undefined, true);
expect(result.skipped).toBe('landscape-dnd');
});
it('去重:同一图片不重复处理', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '微信支付 金额 10 商户:x';
const processor = new OcrProcessor(engine);
const r1 = await processor.process('same-image');
const r2 = await processor.process('same-image');
expect(r1.layer).toBe('layer1-rule');
expect(r2.skipped).toBe('dedup');
});
it('串行化:处理中再次调用跳过', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '微信支付 金额 10 商户:x';
const processor = new OcrProcessor(engine);
const p1 = processor.process('img-a');
const p2 = processor.process('img-b'); // p1 未完成时
const [r1, r2] = await Promise.all([p1, p2]);
expect(r1.layer).toBe('layer1-rule');
expect(r2.skipped).toBe('dedup');
});
it('reset 清除去重状态', async () => {
const engine = new MockOcrEngine();
engine.defaultResponse = '微信支付 金额 10 商户:x';
const processor = new OcrProcessor(engine);
await processor.process('img-reset');
processor.reset();
const result = await processor.process('img-reset');
expect(result.layer).toBe('layer1-rule'); // reset 后可再次处理
});
});