This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/app/__tests__/save-wake.test.ts
T
xcsandClaude Opus 5 c3fed573ee fix(loop-engine): 收尾复审二审 —— 兜底卡自己会抛 / 清单兜底不看面孔 / 空转写解闸
复审逐条核实上一波(C2/C3/M1/M2)修法本身引入的三处破坏,全部属实并修死:

- I1 二审:配方办不下去时发的那张兜底卡(deps.cards.issue)从前裸调,调人预算
  触顶抛 HumanCallBudgetExceededError 会精确复现 C3 那种无限热循环(推进器每
  2 秒重来、不烧预算、不换状态、界面纹丝不动)。包进 try/catch,失败留痕、
  状态退回 awaiting,不让它抛出去。配了变异验证:拿掉 try/catch 后新用例
  真的变红(runTurn 抛出预算触顶错误),还原后转绿。
- C2 二审:decisionCardFace 的 checklist 兜底从前不看 reviewFace —— 一步只声明
  acceptanceChecklist 而 cardVariant 不是 review(T_release 那种形状)时,配方发
  的普通 decide 卡也会被兜上清单,前端不渲染勾选区、回传门却要求 checked ⊇
  checklist,卡按不下去。改成只在 reviewFace 为真时才退回剧本声明的清单。
- M1/M2 二审:project 动作里 localExisting/localUnkeyed 触发的观察痕与真写
  (localWrote/localDropped)共用 detail.auto + detail.ledger 这两个键,而
  raiseAcceptance 的 revised 判据只认这两个键在不在 —— 一颗料 skipExisting、
  或一行主键算不出来,这条"什么都没动"的痕照样能解开重做闸,人打回重做、
  配方原样重跑一遍,闸却开了。auto-runner 补 detail.changed(真写了/真撤了
  才为真),revised 判据对配方那一路改认这个标记。

另两笔记账:
- save-wake.test.ts 的用例标题写着"验收态就是这一种(没有卡在等人)"是错的,
  pendingCards 把未办结工单也算在内,验收态恰恰有一张开着的验收工单——只改
  标题措辞,不动断言与实现。
- docs/debt.md 补两笔:保存后唤醒的收窄几乎不生效(pendingCards 口径过宽);
  模型路径的写同样会 un-land、只补了配方那一半(独立起一条,排在 W05 重写前)。

pnpm gates 退出码 0(2410 passed / 23 skipped);
git diff --stat b995148...HEAD -- activities/ 为空,九个活动包一字未动。

报告:.superpowers/sdd/2026-08-29-deterministic-steps-auto-recipe/closing-fix-report.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 09:21:02 +08:00

81 lines
3.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { taskIdSchema } from '@contracts';
import { afterSave } from '../save-wake.ts';
const TASK = taskIdSchema.parse('11111111-1111-4111-8111-111111111111');
/** 缺省有一张卡在等人 —— 「保存即铺格」的场景恰恰都是这一种 */
function spy(pending: ReadonlyArray<unknown> = [{ id: 'card-1' }]) {
const calls: string[] = [];
return {
calls,
deps: {
views: { repush: () => { calls.push('repush'); return Promise.resolve(true); } },
task: { wake: () => { calls.push('wake'); return Promise.resolve(true); } },
humanTool: { pendingCards: () => { calls.push('pending'); return Promise.resolve(pending); } },
},
};
}
describe('件上保存之后', () => {
it('真落下了行、且有卡在等人:先重推件,再叫醒任务 —— 配方这才有机会按最新的草稿铺格', async () => {
const { calls, deps } = spy();
await afterSave(deps, TASK, 3);
expect(calls).toEqual(['repush', 'pending', 'wake']);
});
it('一行都没落下:什么也不做 —— 没变的东西不值得叫醒谁', async () => {
const { calls, deps } = spy();
await afterSave(deps, TASK, 0);
expect(calls).toEqual([]);
});
/*
* **没有卡在等人就别叫醒**(2026-08-29 终审 F1):`wake` 对 awaiting-event / awaiting-human
* 一律拨成 running,推进器随即推一拍并记一步预算 —— 而现有九个活动包一个 auto 都没有,
* 人每按一次保存就白烧一步、界面闪一次「AI 正在推进这单」;任务本来 awaiting-human
* (等人裁决)时被叫醒,那一拍结束还会把它降级成 awaiting-event:等的明明还是人。
*/
/*
* **这条用例测的是 `pendingCards` 现读为空的那一种局面 —— 不是验收态**(2026-08-29 复审
* A 项二审:这里原话写着「验收态就是这一种」,是错的)。`pendingCards`
* (`human-tool-service.ts:374`)把**未办结工单**也算在内,而验收态恰恰有一张开着的
* 验收工单 —— 真在验收态时 `pendingCards` 不空,`wake` 照样会发,不落进这条分支。
*/
it('没有卡在等人(pendingCards 现读为空):件照旧重推,**不叫醒**', async () => {
const { calls, deps } = spy([]);
await afterSave(deps, TASK, 3);
expect(calls).toEqual(['repush', 'pending']);
});
it('叫醒失败不抬手,但**不再静默** —— 它是「保存即铺格」的唯一触发器', async () => {
const calls: string[] = [];
const said: string[] = [];
const write = vi.spyOn(globalThis.process.stderr, 'write').mockImplementation((chunk) => { said.push(String(chunk)); return true; });
const deps = {
views: { repush: () => { calls.push('repush'); return Promise.resolve(true); } },
task: { wake: () => Promise.reject(new Error('库连不上')) },
humanTool: { pendingCards: () => Promise.resolve([{ id: 'card-1' }]) },
};
await expect(afterSave(deps, TASK, 1)).resolves.toBeUndefined();
write.mockRestore();
expect(calls).toEqual(['repush']);
expect(said.join('')).toContain('库连不上');
});
it('问不出有没有卡在等人:也不叫醒,照样说一声', async () => {
const said: string[] = [];
const write = vi.spyOn(globalThis.process.stderr, 'write').mockImplementation((chunk) => { said.push(String(chunk)); return true; });
const calls: string[] = [];
const deps = {
views: { repush: () => Promise.resolve(true) },
task: { wake: () => { calls.push('wake'); return Promise.resolve(true); } },
humanTool: { pendingCards: () => Promise.reject(new Error('库连不上')) },
};
await expect(afterSave(deps, TASK, 1)).resolves.toBeUndefined();
write.mockRestore();
expect(calls).toEqual([]);
expect(said.join('')).toContain('库连不上');
});
});