- 切换到 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+ 单元测试覆盖核心逻辑
125 lines
5.1 KiB
TypeScript
125 lines
5.1 KiB
TypeScript
import { describe, expect, it, beforeEach } from 'vitest';
|
||
import { useLedgerStore } from '../src/store/ledgerStore';
|
||
import { MemoryBackend } from '../src/domain/mobileStore';
|
||
import type { LedgerFile } from '../src/domain/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
|
||
2026-01-01 open Expenses:Uncategorized CNY
|
||
2026-01-01 open Income:Uncategorized CNY
|
||
include "mobile.bean"
|
||
`,
|
||
};
|
||
|
||
const usdLedger: LedgerFile = {
|
||
path: 'main.bean',
|
||
content: `option "operating_currency" "USD"
|
||
2026-01-01 open Assets:Bank USD
|
||
2026-01-01 open Expenses:Food USD
|
||
include "mobile.bean"
|
||
`,
|
||
};
|
||
|
||
beforeEach(() => {
|
||
useLedgerStore.getState().reset();
|
||
});
|
||
|
||
describe('ledgerStore 写操作', () => {
|
||
it('autoOpenAccounts 跳过已 open 的账户(去重)', async () => {
|
||
await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend());
|
||
|
||
// Assets:Alipay 已存在 → 应跳过
|
||
await useLedgerStore.getState().autoOpenAccounts(['Assets:Alipay', 'Assets:NewAccount']);
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
|
||
// 只新增 Assets:NewAccount,不含 Assets:Alipay 的重复 open
|
||
expect(after).toContain('Assets:NewAccount');
|
||
// 确认 mobileBean 中不含重复的 Assets:Alipay open(它在 main.bean 里声明,不应追加到 mobile.bean)
|
||
expect(after).not.toContain('open Assets:Alipay');
|
||
});
|
||
|
||
it('autoOpenAccounts 对已全部 open 的列表是 no-op', async () => {
|
||
await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend());
|
||
const before = useLedgerStore.getState().mobileBean;
|
||
|
||
await useLedgerStore.getState().autoOpenAccounts(['Assets:Alipay']);
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
|
||
expect(after).toBe(before); // 无变化
|
||
});
|
||
|
||
it('autoOpenAccounts 使用 ledger 的 operating_currency', async () => {
|
||
await useLedgerStore.getState().loadLedger([usdLedger], new MemoryBackend());
|
||
await useLedgerStore.getState().autoOpenAccounts(['Assets:NewUSD']);
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
|
||
// 应写 USD 而非 CNY
|
||
expect(after).toContain('open Assets:NewUSD USD');
|
||
expect(after).not.toContain('open Assets:NewUSD CNY');
|
||
});
|
||
|
||
it('adjustAccountBalance 使用 ledger 的 operating_currency', async () => {
|
||
await useLedgerStore.getState().loadLedger([usdLedger], new MemoryBackend());
|
||
await useLedgerStore.getState().adjustAccountBalance('Assets:Bank', '5000', '2026-07-15');
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
|
||
// balance 断言应使用 USD
|
||
expect(after).toContain('balance Assets:Bank 5000 USD');
|
||
});
|
||
|
||
it('appendTransactionsBatch 在锁内拼接多笔交易', async () => {
|
||
await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend());
|
||
const before = useLedgerStore.getState().mobileBean;
|
||
|
||
await useLedgerStore.getState().appendTransactionsBatch([
|
||
{ date: '2026-07-10', narration: '午餐', postings: [{ account: 'Assets:Alipay', amount: '-20', currency: 'CNY' }, { account: 'Expenses:Food', amount: '20', currency: 'CNY' }] },
|
||
{ date: '2026-07-11', narration: '晚餐', postings: [{ account: 'Assets:Alipay', amount: '-30', currency: 'CNY' }, { account: 'Expenses:Food', amount: '30', currency: 'CNY' }] },
|
||
]);
|
||
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
expect(after).toContain('午餐');
|
||
expect(after).toContain('晚餐');
|
||
expect(after.length).toBeGreaterThan(before.length);
|
||
});
|
||
|
||
it('并发 addTransaction + autoOpenAccounts 不丢失数据', async () => {
|
||
await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend());
|
||
|
||
// 并发执行:添加交易 + 自动开户
|
||
await Promise.all([
|
||
useLedgerStore.getState().addTransaction({
|
||
date: '2026-07-10', narration: '交易A',
|
||
postings: [{ account: 'Assets:Alipay', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY' }],
|
||
}),
|
||
useLedgerStore.getState().autoOpenAccounts(['Assets:NewAcct']),
|
||
]);
|
||
|
||
const after = useLedgerStore.getState().mobileBean;
|
||
// 两者都应存在(互斥锁保证了不丢失)
|
||
expect(after).toContain('交易A');
|
||
expect(after).toContain('Assets:NewAcct');
|
||
});
|
||
|
||
it('并发两笔 addTransaction 都保留', async () => {
|
||
await useLedgerStore.getState().loadLedger([sampleLedger], new MemoryBackend());
|
||
|
||
await Promise.all([
|
||
useLedgerStore.getState().addTransaction({
|
||
date: '2026-07-10', narration: '交易1',
|
||
postings: [{ account: 'Assets:Alipay', amount: '-5', currency: 'CNY' }, { account: 'Expenses:Food', amount: '5', currency: 'CNY' }],
|
||
}),
|
||
useLedgerStore.getState().addTransaction({
|
||
date: '2026-07-10', narration: '交易2',
|
||
postings: [{ account: 'Assets:Alipay', amount: '-8', currency: 'CNY' }, { account: 'Expenses:Food', amount: '8', currency: 'CNY' }],
|
||
}),
|
||
]);
|
||
|
||
const txns = useLedgerStore.getState().ledger!.transactions;
|
||
expect(txns).toHaveLength(2);
|
||
expect(txns.map(t => t.narration).sort()).toEqual(['交易1', '交易2']);
|
||
});
|
||
});
|