DriftLedger/tests/logger.test.ts
fengmengqi bf04400852 feat: OCR 模型按需下载 + 三层独立开关 + 日志持久化 + 原生浮层主题同步 + 文档重构
OCR 模型按需下载(P8 瘦身):
- 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB
- 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝
- OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户
- settingsStore 持久化 ocrModelVersion / ocrModelDir
- 自动化页集成模型状态检查与一键下载 UI

OCR 三层独立控制:
- OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关
- 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision)
- OCR 处理增加耗时与字符数日志

日志系统升级:
- logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log)
- 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx
- 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理
- _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号)

原生浮层 UI 主题同步(P6):
- 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生
- 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取
- FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal
- FloatingHelper / FloatingTip 同步适配
- _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送

UI 与组件增强:
- FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange
- 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗
- 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择
- AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型
- 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗)

去重优化:
- 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复
- DedupResult 新增 matchedItem 返回匹配对比项

文档重构:
- README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表)
- 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图)
- 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md
- UI 重设计文档(design spec + p1-p8)移入 docs/design/

其他:
- i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键
- ppocr Config Plugin 修复 import 注入去重;size-optimization 增强
- 新增测试:logger.test.ts、floating-ui-config.test.ts
2026-07-23 19:03:38 +08:00

168 lines
5.8 KiB
TypeScript
Raw Permalink 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, vi } from 'vitest';
import { LogFileBackend, LogFileInfo, LogLevel, createLogger } from '../src/utils/logger';
class MemoryLogBackend implements LogFileBackend {
public files: Map<string, string> = new Map();
async appendLog(dateStr: string, logLine: string): Promise<void> {
const existing = this.files.get(dateStr) ?? '';
this.files.set(dateStr, existing + logLine);
}
async readLogFile(dateStr: string): Promise<string> {
return this.files.get(dateStr) ?? '';
}
async listLogFiles(): Promise<LogFileInfo[]> {
const res: LogFileInfo[] = [];
for (const [date, content] of this.files.entries()) {
res.push({
name: `app-${date}.log`,
path: `/mock/logs/app-${date}.log`,
size: Buffer.byteLength(content, 'utf8'),
date,
});
}
return res.sort((a, b) => b.date.localeCompare(a.date));
}
async deleteLogFile(dateStr: string): Promise<void> {
this.files.delete(dateStr);
}
async clearAllLogFiles(): Promise<void> {
this.files.clear();
}
}
describe('LoggerImpl 持久化与高级功能测试', () => {
it('应当能正确初始化 LogFileBackend 并将日志追加写入内存文件', async () => {
const logger = createLogger();
logger.consoleOutput = false;
const backend = new MemoryLogBackend();
await logger.initFileBackend(backend);
logger.info('testTag', '测试消息1', { foo: 'bar' });
logger.error('testTag', '崩溃报错消息');
await logger.flushQueue();
const todayStr = new Date().toISOString().slice(0, 10);
const fileContent = await backend.readLogFile(todayStr);
expect(fileContent).toContain('测试消息1');
expect(fileContent).toContain('崩溃报错消息');
expect(fileContent).toContain('"foo":"bar"');
});
it('应当能正确按日期读取文件中的历史日志条目', async () => {
const logger = createLogger();
logger.consoleOutput = false;
const backend = new MemoryLogBackend();
const historicalDate = '2026-07-20';
const logLine1 = JSON.stringify({
timestamp: '2026-07-20T10:00:00.000Z',
level: LogLevel.WARN,
tag: 'network',
message: '网络超时',
});
const logLine2 = JSON.stringify({
timestamp: '2026-07-20T10:05:00.000Z',
level: LogLevel.ERROR,
tag: 'db',
message: '数据库锁死',
});
await backend.appendLog(historicalDate, `${logLine1}\n${logLine2}\n`);
await logger.initFileBackend(backend);
const loadedLogs = await logger.loadLogsFromFile(historicalDate);
expect(loadedLogs).toHaveLength(2);
expect(loadedLogs[0].tag).toBe('network');
expect(loadedLogs[0].level).toBe(LogLevel.WARN);
expect(loadedLogs[1].message).toBe('数据库锁死');
});
it('应当能自动清理超过指定保留天数(如 7 天)的历史日志文件', async () => {
const logger = createLogger();
logger.consoleOutput = false;
const backend = new MemoryLogBackend();
// 假设今天 2026-07-22注入 10 天前与 2 天前的日志文件
await backend.appendLog('2026-07-01', 'old logs\n'); // 超过 7 天
await backend.appendLog('2026-07-21', 'recent logs\n'); // 保留
await logger.initFileBackend(backend, { maxDays: 7 });
const files = await logger.getLogFiles();
const dates = files.map(f => f.date);
expect(dates).not.toContain('2026-07-01');
expect(dates).toContain('2026-07-21');
});
it('应当能格式化导出人类可读文本字符串', () => {
const logger = createLogger();
logger.consoleOutput = false;
logger.info('billPipeline', '接收到微信账单', { amount: 15.5 });
logger.warn('dedup', '发现重复交易', { id: 'tx-123' });
const exportText = logger.exportAsFormattedText();
expect(exportText).toContain('[INFO] [billPipeline] 接收到微信账单');
expect(exportText).toContain('[WARN] [dedup] 发现重复交易');
expect(exportText).toContain('"amount":15.5');
});
it('query 方法应当支持按关键词过滤日志', () => {
const logger = createLogger();
logger.consoleOutput = false;
logger.info('tagA', '普通记账事件');
logger.error('tagB', '支付接口网络超时', { status: 504 });
const result = logger.query({ keyword: '超时' });
expect(result).toHaveLength(1);
expect(result[0].tag).toBe('tagB');
});
it('clearAllLogs 应当同时清空内存与磁盘中的全部日志', async () => {
const logger = createLogger();
logger.consoleOutput = false;
const backend = new MemoryLogBackend();
await logger.initFileBackend(backend);
logger.info('tag', '一条日志');
await logger.flushQueue();
expect(logger.getLogs()).toHaveLength(1);
expect((await backend.listLogFiles()).length).toBeGreaterThan(0);
await logger.clearAllLogs();
expect(logger.getLogs()).toHaveLength(0);
expect((await backend.listLogFiles()).length).toBe(0);
});
it('getTodayLogs 应当能正确合并磁盘上的历史 session 日志与当前内存中的新日志(自动去重)', async () => {
const logger = createLogger();
logger.consoleOutput = false;
const backend = new MemoryLogBackend();
await logger.initFileBackend(backend);
const todayStr = new Date().toISOString().slice(0, 10);
const earlierSessionLog = JSON.stringify({
timestamp: `${todayStr}T08:00:00.000Z`,
level: LogLevel.INFO,
tag: 'layout',
message: '早前 Session 启动日志',
});
await backend.appendLog(todayStr, `${earlierSessionLog}\n`);
// 当前 Session 新记录的内存日志
logger.info('billPipeline', '新 Session 产生的日志');
const todayLogs = await logger.getTodayLogs();
expect(todayLogs.some(e => e.message === '早前 Session 启动日志')).toBe(true);
expect(todayLogs.some(e => e.message === '新 Session 产生的日志')).toBe(true);
});
});