/** * ledgerStore reset 与 lockGeneration 守卫测试。 * 验证 #1/#2 修复:reset() 递增 lockGeneration,addTransaction 中 gen 守卫生效。 */ import { describe, expect, it, beforeEach, vi } from 'vitest'; // ledgerStore 间接依赖 txKeyStore → expo-file-system,mock 掉避免 react-native flow 语法解析失败 vi.mock('expo-file-system/legacy', () => ({ documentDirectory: '', getInfoAsync: vi.fn().mockResolvedValue({ exists: false }), readAsStringAsync: vi.fn().mockResolvedValue('[]'), writeAsStringAsync: vi.fn().mockResolvedValue(undefined), moveAsync: vi.fn().mockResolvedValue(undefined), })); import { useLedgerStore } from '../src/store/ledgerStore'; import { MemoryBackend } from '../src/domain/platform/mobileStore'; import type { LedgerFile } from '../src/domain/core/types'; const sampleLedger: LedgerFile = { path: 'main.bean', content: `option "operating_currency" "CNY" 2026-01-01 open Assets:Alipay CNY 2026-01-01 open Expenses:Food CNY include "mobile.bean" `, }; beforeEach(() => { useLedgerStore.getState().reset(); }); describe('ledgerStore reset 与 lockGeneration', () => { it('reset 后状态清空', () => { const state = useLedgerStore.getState(); expect(state.ledger).toBeNull(); expect(state.mobileStore).toBeNull(); expect(state.mobileBean).toBe(''); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); it('reset 后重新 loadLedger 正常工作', async () => { await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend()); expect(useLedgerStore.getState().ledger).not.toBeNull(); useLedgerStore.getState().reset(); expect(useLedgerStore.getState().ledger).toBeNull(); // 重新加载 await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend()); expect(useLedgerStore.getState().ledger).not.toBeNull(); expect(useLedgerStore.getState().ledger?.transactions).toBeDefined(); }); it('reset 后 addTransaction 使用新的 mobileStore', async () => { const backend1 = new MemoryBackend(); await useLedgerStore.getState().loadLedger([sampleLedger], backend1); const store1 = useLedgerStore.getState().mobileStore; useLedgerStore.getState().reset(); const backend2 = new MemoryBackend(); await useLedgerStore.getState().loadLedger([sampleLedger], backend2); const store2 = useLedgerStore.getState().mobileStore; // 两个 store 应该是不同的实例 expect(store1).not.toBe(store2); }); it('并发 addTransaction 串行化执行', async () => { await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend()); const draft1 = { date: '2026-07-01', payee: '商户A', narration: '交易1', postings: [ { account: 'Assets:Alipay', amount: '-100', currency: 'CNY' }, { account: 'Expenses:Food', amount: '100', currency: 'CNY' }, ], }; const draft2 = { date: '2026-07-02', payee: '商户B', narration: '交易2', postings: [ { account: 'Assets:Alipay', amount: '-200', currency: 'CNY' }, { account: 'Expenses:Food', amount: '200', currency: 'CNY' }, ], }; // 并发执行两个 addTransaction const [result1, result2] = await Promise.all([ useLedgerStore.getState().addTransaction(draft1), useLedgerStore.getState().addTransaction(draft2), ]); // MobileCommit 返回 content/diff,不包含 success 字段 expect(result1.content).toBeDefined(); expect(result2.content).toBeDefined(); // 两笔交易都应该入账 const bean = useLedgerStore.getState().mobileBean; expect(bean).toContain('商户A'); expect(bean).toContain('商户B'); }); it('reset 后 addTransaction 报错(账本未加载)', async () => { await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend()); const draft = { date: '2026-07-01', payee: '测试商户', narration: '测试交易', postings: [ { account: 'Assets:Alipay', amount: '-50', currency: 'CNY' }, { account: 'Expenses:Food', amount: '50', currency: 'CNY' }, ], }; // reset 后状态清空 useLedgerStore.getState().reset(); // 此时 addTransaction 应该报错 await expect(useLedgerStore.getState().addTransaction(draft)).rejects.toThrow('账本未加载'); }); });