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

106 lines
3.7 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, vi, beforeEach } from 'vitest';
/**
* FileSystemBackend 逻辑测试。
*
* 由于 expo-file-system 在测试环境不可用,我们 mock 它的接口,
* 验证 FileSystemBackend 的 read/writeAtomic/recoverIfNeeded 编排逻辑正确。
*/
// Mock expo-file-system/legacyfileSystemBackend.ts 用 import * as所以直接导出属性
const mockFs = {
documentDirectory: '/mock/',
getInfoAsync: vi.fn(),
readAsStringAsync: vi.fn(),
writeAsStringAsync: vi.fn(),
deleteAsync: vi.fn(),
moveAsync: vi.fn(),
};
vi.mock('expo-file-system/legacy', () => mockFs);
// Mock logger
vi.mock('../src/utils/logger', () => ({
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));
// 延迟导入vi.mock 需要在模块加载前执行)
const { FileSystemBackend } = await import('../src/services/fileSystemBackend');
function resetMock() {
mockFs.getInfoAsync.mockReset();
mockFs.readAsStringAsync.mockReset();
mockFs.writeAsStringAsync.mockReset();
mockFs.deleteAsync.mockReset();
mockFs.moveAsync.mockReset();
}
describe('FileSystemBackend', () => {
beforeEach(resetMock);
it('read() 返回文件内容', async () => {
mockFs.getInfoAsync.mockResolvedValue({ exists: true });
mockFs.readAsStringAsync.mockResolvedValue('2026-01-01 *\n Assets:Bank');
const backend = new FileSystemBackend();
const content = await backend.read();
expect(content).toBe('2026-01-01 *\n Assets:Bank');
});
it('read() 文件不存在时返回空串', async () => {
mockFs.getInfoAsync.mockResolvedValue({ exists: false });
const backend = new FileSystemBackend();
const content = await backend.read();
expect(content).toBe('');
});
it('writeAtomic() 先写 tmp 再删旧文件再 rename', async () => {
mockFs.getInfoAsync.mockResolvedValue({ exists: true });
mockFs.writeAsStringAsync.mockResolvedValue(undefined);
mockFs.deleteAsync.mockResolvedValue(undefined);
mockFs.moveAsync.mockResolvedValue(undefined);
const backend = new FileSystemBackend();
await backend.writeAtomic('new content');
// 验证写入 tmp
expect(mockFs.writeAsStringAsync).toHaveBeenCalledWith('/mock/mobile.bean.tmp', 'new content');
// 验证删除旧文件
expect(mockFs.deleteAsync).toHaveBeenCalledWith('/mock/mobile.bean', { idempotent: true });
// 验证 rename tmp → 正式文件
expect(mockFs.moveAsync).toHaveBeenCalledWith({ from: '/mock/mobile.bean.tmp', to: '/mock/mobile.bean' });
});
it('writeAtomic() 旧文件不存在时不删除', async () => {
mockFs.getInfoAsync.mockResolvedValue({ exists: false });
mockFs.writeAsStringAsync.mockResolvedValue(undefined);
mockFs.moveAsync.mockResolvedValue(undefined);
const backend = new FileSystemBackend();
await backend.writeAtomic('content');
expect(mockFs.deleteAsync).not.toHaveBeenCalled();
expect(mockFs.moveAsync).toHaveBeenCalled();
});
it('recoverIfNeeded() 检测到 tmp 残留时清理并返回 true', async () => {
// 第一次 getInfoAsync检测 tmp返回存在
mockFs.getInfoAsync.mockResolvedValue({ exists: true });
mockFs.deleteAsync.mockResolvedValue(undefined);
const backend = new FileSystemBackend();
const result = await backend.recoverIfNeeded();
expect(result).toBe(true);
expect(mockFs.deleteAsync).toHaveBeenCalledWith('/mock/mobile.bean.tmp', { idempotent: true });
});
it('recoverIfNeeded() 无 tmp 残留时返回 false', async () => {
mockFs.getInfoAsync.mockResolvedValue({ exists: false });
const backend = new FileSystemBackend();
const result = await backend.recoverIfNeeded();
expect(result).toBe(false);
});
});