文件
hyb-oa/server/tests/unit/services/budgetPool.locking.test.js
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

28 行
2.1 KiB
JavaScript

const { lockBudgetPoolsForUpdate, resolveApplicableBudgetPoolByContext } = require('../../../src/services/budgetPool');
describe('budget pool write protection', () => {
it('locks unique pools in stable order with parameters and rejects a root client', async () => {
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), $transaction: vi.fn() };
await lockBudgetPoolsForUpdate(tx, ['b', 'a', 'b', null]);
const query = tx.$queryRaw.mock.calls[0][0];
expect(query.sql).toMatch(/ORDER BY id FOR NO KEY UPDATE/);
expect(query.values).toEqual(['a', 'b']);
await expect(lockBudgetPoolsForUpdate({ ...tx, $connect: vi.fn() }, ['a'])).rejects.toThrow(/transaction/);
});
it('does not lock preview reads but refreshes usage after a submission lock', async () => {
const first = { id: 'pool', used_amount: '0.00' }; const latest = { ...first, used_amount: '60.00' };
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), budgetPool: { findMany: vi.fn().mockResolvedValueOnce([first]).mockResolvedValueOnce([first]).mockResolvedValueOnce([latest]) } };
const context = { effectiveDate: new Date('2026-09-19') };
expect(await resolveApplicableBudgetPoolByContext(tx, context)).toEqual(first);
expect(tx.$queryRaw).not.toHaveBeenCalled();
expect(await resolveApplicableBudgetPoolByContext(tx, context, { forUpdate: true })).toEqual(latest);
expect(tx.budgetPool.findMany.mock.invocationCallOrder[1]).toBeLessThan(tx.$queryRaw.mock.invocationCallOrder[0]);
expect(tx.$queryRaw.mock.invocationCallOrder[0]).toBeLessThan(tx.budgetPool.findMany.mock.invocationCallOrder[2]);
});
it.each([{ records: [] }, { records: [{ id: 'replacement' }] }])('rejects a candidate changed during the wait: %j', async ({ records }) => {
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), budgetPool: { findMany: vi.fn().mockResolvedValueOnce([{ id: 'old' }]).mockResolvedValueOnce(records) } };
await expect(resolveApplicableBudgetPoolByContext(tx, { effectiveDate: new Date('2026-09-19') }, { forUpdate: true }))
.rejects.toMatchObject({ statusCode: 409, details: { reasonCode: 'budget.pool_changed' } });
});
});