import { describe, expect, it, vi, beforeEach } from 'vitest'; /** * FileSystemBackend 逻辑测试。 * * 由于 expo-file-system 在测试环境不可用,我们 mock 它的接口, * 验证 FileSystemBackend 的 read/writeAtomic/recoverIfNeeded 编排逻辑正确。 */ // Mock expo-file-system/legacy(fileSystemBackend.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); }); });