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

125 lines
5.1 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, 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']);
});
});