diff --git a/apps/api/test/debts.test.ts b/apps/api/test/debts.test.ts index d9cd2ce..931fddd 100644 --- a/apps/api/test/debts.test.ts +++ b/apps/api/test/debts.test.ts @@ -151,7 +151,7 @@ test('borrow/lend/collect/repay pair balances atomically and survive concurrency 200, ); const backup = (await call(a.token, '/backup')).data; - assert.equal(JSON.parse(packBackup(backup)['manifest.json']).version, 6); + assert.equal(JSON.parse(packBackup(backup)['manifest.json']).version, 7); assert.equal(backup.transfers.length, 5); assert.ok(backup.transfers.some((p: any) => p.operation === 'lend' && p.fee === '-2')); assert.equal( diff --git a/apps/api/test/update-integration.test.ts b/apps/api/test/update-integration.test.ts index 2be4829..c9f28fc 100644 --- a/apps/api/test/update-integration.test.ts +++ b/apps/api/test/update-integration.test.ts @@ -129,8 +129,62 @@ test('real MySQL: groups, scheduled atomic execution, calendar, privacy and ZIP assert.equal(day.expense, '5.50'); assert.equal(day.income, '0.00'); assert.equal((await call('/calendar/day?date=' + today(), a.cookie)).data.items.length, 4); + const transferPlan = await call('/schedules', a.cookie, 'POST', { + ...input, + name: 'Scheduled transfer', + operation: 'transfer', + targetId, + amount: '3', + received: '3', + }); + assert.equal(transferPlan.status, 201); + const transferRace = await Promise.all([ + call('/schedules/run', a.cookie, 'POST', {}), + call('/schedules/run', a.cookie, 'POST', {}), + ]); + assert.equal( + transferRace.reduce((sum, v) => sum + v.data.executed, 0), + 1, + ); + const afterTransfer = (await call('/positions', a.cookie)).data; + assert.equal(afterTransfer.find((p: any) => p.id === sourceId).amount, '-23.5'); + assert.equal(afterTransfer.find((p: any) => p.id === targetId).amount, '28'); + assert.equal((await call('/calendar/day?date=' + today(), a.cookie)).data.items.length, 4); + const failedPlan = await call('/schedules', a.cookie, 'POST', { + ...input, + name: 'Archived account', + }); + assert.equal(failedPlan.status, 201); + const beforeFailure = (await call('/schedules', a.cookie)).data.find( + (p: any) => p.id === failedPlan.data.id, + ); + await call('/positions/' + sourceId, a.cookie, 'PATCH', { + name: 'Daily', + category: 'bank', + groupName: '日常', + archived: true, + }); + const failure = await call('/schedules/run', a.cookie, 'POST', {}); + assert.equal(failure.data.executed, 0); + assert.equal(failure.data.errors.length, 1); + assert.equal( + (await call('/schedules', a.cookie)).data.find((p: any) => p.id === failedPlan.data.id) + .nextAt, + beforeFailure.nextAt, + ); + assert.equal( + (await call('/positions', a.cookie)).data.find((p: any) => p.id === sourceId).amount, + '-23.5', + ); + await call('/positions/' + sourceId, a.cookie, 'PATCH', { + name: 'Daily', + category: 'bank', + groupName: '日常', + archived: false, + }); + assert.equal((await call('/schedules/' + failedPlan.data.id, a.cookie, 'DELETE')).status, 200); const backup = (await call('/backup', a.cookie)).data; - assert.equal(backup.schedules.length, 2); + assert.equal(backup.schedules.length, 3); assert.equal( (await call('/backup/import', b.cookie, 'POST', { confirmed: true, backup })).status, 201, @@ -141,7 +195,32 @@ test('real MySQL: groups, scheduled atomic execution, calendar, privacy and ZIP ['日常', '理财'].sort(), ); const restored = (await call('/backup', b.cookie)).data; - assert.equal(restored.schedules.filter((p: any) => p.completed).length, 1); + assert.equal(restored.schedules.filter((p: any) => p.completed).length, 2); + await db.schedule.createMany({ + data: Array.from({ length: 21 }, (_, i) => ({ + id: randomUUID(), + userId: a.id, + name: 'Batch ' + i, + operation: 'expense', + sourceId, + amount: '1', + received: '0', + nextAt: new Date(Date.now() - 60000), + intervalDays: 0, + enabled: true, + notes: '', + })), + }); + const firstBatch = await call('/schedules/run', a.cookie, 'POST', {}); + assert.equal(firstBatch.data.executed, 20); + assert.equal(firstBatch.data.hasMore, true); + const secondBatch = await call('/schedules/run', a.cookie, 'POST', {}); + assert.equal(secondBatch.data.executed, 1); + assert.equal(secondBatch.data.hasMore, false); + assert.equal( + (await call('/positions', a.cookie)).data.find((p: any) => p.id === sourceId).amount, + '-44.5', + ); await call('/positions/' + sourceId, a.cookie, 'PATCH', { name: 'Daily', category: 'bank', diff --git a/docs/acceptance.md b/docs/acceptance.md index 967f28e..d58ccd2 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -55,4 +55,4 @@ ## 2026-10-02 账户分组、计划与日历 -本轮 28 项单元测试、类型检查和生产构建通过。浏览器模拟数据验证与实际数据库未验证部分详见 [本轮验收](update-2026-10-02.md)。连接数据库发生 PROTOCOL_CONNECTION_LOST,迁移、图库初始化及真实 MySQL 并发集成仍需在可连接环境执行;前文并发余额不足的旧断言已随允许透支需求调整。 +本轮 28 项单元测试、类型检查和生产构建通过。浏览器模拟数据验证与实际数据库未验证部分详见 [本轮验收](update-2026-10-02.md)。首轮连接数据库发生 PROTOCOL_CONNECTION_LOST;2026-10-02 17:54 补充验收已完成迁移、31 个图库图标初始化及全部 9 项真实 MySQL 集成检查。前文并发余额不足的旧断言已随允许透支需求调整。 diff --git a/docs/update-2026-10-02.md b/docs/update-2026-10-02.md index de334e9..8392406 100644 --- a/docs/update-2026-10-02.md +++ b/docs/update-2026-10-02.md @@ -49,4 +49,14 @@ pnpm test:integration - `pnpm test` 28 项通过:透支/负转入余额、债务超额还款拒绝、请求去重、金额范围、债务符号/币种、轨迹裁剪、计划完成/重复/失败推进、日历口径/小数汇总/缺汇率、ZIP v7 分组计划往返及旧版兼容。 - 浏览器使用明确的模拟数据检查分组弹窗切换、转账选择和月历默认今天/日期切换/上下布局及红色支出,未操作真实用户财务数据。 - 新增 `test/update-integration.test.ts`,覆盖真实 MySQL 中计划并发执行一次、透支、补记、分组恢复、隐藏账户与日历隔离,纳入 `pnpm test:integration`。 -- 本环境尝试连接现有数据库时发生 `PROTOCOL_CONNECTION_LOST`,因此本轮未应用数据库迁移、未向实际数据库导入新图标、未运行真实数据库集成测试。上述命令需在数据库可连接后完成;历史验收文档的旧测试结果不代表本轮已通过。 +- 首轮连接数据库发生 `PROTOCOL_CONNECTION_LOST` 时,数据库验收暂未执行;该阻塞已在下面的补充验收中解决。 + +## 数据库恢复后的补充验收(2026-10-02 17:54,香港时间) + +- 已确认 MySQL 连接恢复并通过 SELECT 1;增量应用 `20261002000400_groups_schedules` 成功,`pnpm db:status` 确认全部 12 个迁移完成,schema 最新。 +- 已生成 Prisma 客户端并编译后端,运行图库初始化;数据库检查确认 31 个固定内置图标 ID 均有图片数据且为共享图标。 +- 在独立端口 3101 启动当前工作树后端,`pnpm test:integration` 9 项全部通过。测试使用随机验收账号,测试结束按对应 ID/用户名清理自己的数据,未使用真实用户账号执行记账或清空。 +- 新增专项进一步验证:定时支出/转账并发只记一次、透支后的精确余额、归档账户执行失败保留到期时间和余额、21 项到期计划按 20 + 1 分批处理、内部转账不计日历收支、ZIP 分组/计划状态恢复及隐藏账户隔离。 +- 修复债务集成测试中仍断言 manifest v6 的旧预期,当前应为 v7。连续重跑触发临时服务的正常登录限流后,重启本次临时测试服务再执行,未修改限流规则。 +- 本轮 `pnpm test` 28 项通过,`pnpm typecheck` 通过;后端编译通过,前端生产构建在此前实现验收已通过,本轮未修改前端代码。 +- 上一轮遗留的数据库迁移、图标初始化及真实 MySQL 集成验收现已完成。浏览器布局验收仍沿用此前模拟数据结果,没有新增真实浏览器端到端验收。 diff --git a/update.md b/update.md index 793c67a..33d61d4 100644 --- a/update.md +++ b/update.md @@ -23,3 +23,5 @@ - 验证与部署边界见 [更新说明](docs/update-2026-10-02.md)。 + +数据库验收完成:2026-10-02 17:54(香港时间)。12 个迁移状态最新,31 个内置图标已初始化;28 项单元测试、9 项真实数据库集成检查通过。