Archived
fix(loop-engine): auto 收步补两处 —— 真测 carded 门 + 半 auto 不抢模型的活
评审 Important:「卡还没发过就不收」那条用例其实没走到 carded 门(命中的是 Task 7 既有的发卡分支),换一条真正让 card 被 when 挡掉、只有别的笔照写的用例才让执行落进 carded 门;原用例改名留作 Task 7 那条路径的回归,逻辑不动。 R9(基于评审 Minor #1 的裁定):收步条件加一项 guide 为空 —— 半 auto(声明了非空 guide)的步,人答过卡之后那份判断仍是模型的活,不该被这段抢在模型前面收掉。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -2313,7 +2313,13 @@ describe('auto 步:配方在拍首跑', () => {
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).toBe('Completed');
|
||||
});
|
||||
|
||||
it('卡还没发过就不收 —— 那才是替人签字', async () => {
|
||||
/*
|
||||
* 这条原名「卡还没发过就不收 —— 那才是替人签字」——评审 trace 出它验的其实是 Task 7
|
||||
* 既有的「还没发过就发卡」那条路(:1998 `auto.issuedCardId !== undefined` 提前返回),
|
||||
* 从没走到 Task 8 新加的 `carded` 门(:2019 起那一段)。名字改准,逻辑不动 ——
|
||||
* 它仍是一条有效回归(证明配方在没发过卡时确实把卡发了出去),只是不再冒充「验过 carded 门」。
|
||||
*/
|
||||
it('还没发过卡 → 这一拍把卡发出去(Task 7 既有路径,不是 carded 门 —— 见下一条)', async () => {
|
||||
const h = harness({
|
||||
parsed: AUTO_MODEL,
|
||||
runtime: AUTO_RUNTIME,
|
||||
@@ -2333,4 +2339,79 @@ describe('auto 步:配方在拍首跑', () => {
|
||||
expect(h.issued).toHaveLength(1);
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).not.toBe('Completed');
|
||||
});
|
||||
|
||||
/*
|
||||
* **真正把执行推到 :2019 且 carded === false 的用例**(评审 Important)。
|
||||
*
|
||||
* 配方两笔:`project` 无条件照写(worldTouched 不受它影响)、`card` 带 `when` 这一拍算假、
|
||||
* 被跳过 —— 卡**没有**发出去,`autoRunner.run` 回的是 `issuedCardId: undefined`。
|
||||
* `auto.wrote > 0` 触发一次 doneWhen 重扫,可这一步没声明 doneWhen,重扫不改变局面,
|
||||
* 于是**第一次**真的落到收步判断那一段:`recipeIssuesCard(current.auto)` 因为配方里
|
||||
* 结构上确实有一笔 `card`(不看这一拍它有没有被 `when` 挡住)算出 true,`carded` 却是假
|
||||
* ——这道门就是靠 `carded` 才没把它错收。`carded &&` 这个条件若被误删,这条会变红。
|
||||
*/
|
||||
it('配方这一拍卡被 when 挡住没发、只有别的笔照写 → carded 是假,这一步不该被收', async () => {
|
||||
const h = harness({
|
||||
parsed: AUTO_MODEL,
|
||||
runtime: AUTO_RUNTIME,
|
||||
sfeelCondition: () => false,
|
||||
steps: [
|
||||
{
|
||||
item: 'S1',
|
||||
guide: '',
|
||||
writes: [],
|
||||
reads: [],
|
||||
tools: [],
|
||||
auto: {
|
||||
do: [
|
||||
{ kind: 'project', into: 'quote_intake.record', from: { once: true }, fields: { pn: 'A', eau: 1 } },
|
||||
{
|
||||
kind: 'card',
|
||||
when: '恒假',
|
||||
as: 'decide',
|
||||
variant: 'review',
|
||||
title: '结论审核',
|
||||
options: [{ value: 'approve', label: '确认通过' }],
|
||||
basis: [{ k: 'a', v: 'b' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const out = await h.runner.runTurn(TASK);
|
||||
// project 照写,card 被 when 挡掉没发
|
||||
expect(h.written).toHaveLength(1);
|
||||
expect(h.issued).toHaveLength(0);
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).not.toBe('Completed');
|
||||
});
|
||||
|
||||
/*
|
||||
* **R9**(基于评审 Minor #1 的裁定):半 auto(声明了非空 `guide`)的步,卡答过了也不该
|
||||
* 被这段收掉 —— 半 auto 存在的意义就是「确定的先做完,剩下的判断仍问模型」,人答完卡之后
|
||||
* 那份判断(读人写的自由文本、落到具体的行上)恰恰是模型该干的活。
|
||||
* 用默认的 `submit` 动作(harness 不传 `action` 时的缺省值):它只发验收卡、不碰项状态,
|
||||
* 这样「S1 没被收成 Completed」这条断言测的确实是「没被这段收掉」,不是被模型自己收掉的假阳性。
|
||||
*/
|
||||
it('R9:半 auto(有 guide)的步卡也答过了 —— 不该被收在模型之前,那份判断仍是模型的活', async () => {
|
||||
const h = harness({
|
||||
parsed: AUTO_MODEL,
|
||||
runtime: { items: [{ id: 'S1', state: 'Active', worldTouched: true }] } as never,
|
||||
unanswered: [],
|
||||
sfeelCondition: () => false,
|
||||
steps: [
|
||||
{
|
||||
item: 'S1',
|
||||
guide: '把结论落进台账,再判断该不该收步',
|
||||
writes: [],
|
||||
reads: [],
|
||||
tools: [],
|
||||
auto: { do: [{ kind: 'card', as: 'decide', variant: 'review', title: '结论审核', options: [{ value: 'approve', label: '确认通过' }], basis: [{ k: 'a', v: 'b' }] }] },
|
||||
},
|
||||
],
|
||||
});
|
||||
const out = await h.runner.runTurn(TASK);
|
||||
expect(h.decided).toHaveLength(1);
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).not.toBe('Completed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2021,10 +2021,15 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
* 这一步声明了 auto、卡发过(worldTouched)、这一拍没有未答的卡(前面 unanswered 把住)、
|
||||
* 配方这一拍也没再发新卡。**人已经签了字,收步是照实记录,不是替人签字** ——
|
||||
* 「人工激活项不给 doneWhen」那条纪律防的是引擎抢在人前面,这里人在前面。
|
||||
*
|
||||
* **半 auto(声明了非空 `guide`)的步不走这条近道**(R9,评审 Minor #1 逼出来的裁定):
|
||||
* 半 auto 存在的意义就是「确定的先做完,剩下的判断仍问模型」—— 人答完卡之后那份判断
|
||||
* (读人写的自由文本、落到具体的行上)恰恰是模型该干的活。收在它前面,等于把半 auto
|
||||
* 的后半截整个删掉。当前没有活动踩到这个组合,但机制不该埋这个坑。
|
||||
*/
|
||||
const noDoneWhen = current.doneWhen === undefined || current.doneWhen.trim() === '';
|
||||
const carded = stateRuntime.items.find((i) => i.id === current.item)?.worldTouched === true;
|
||||
if (noDoneWhen && carded && recipeIssuesCard(current.auto)) {
|
||||
if (noDoneWhen && carded && recipeIssuesCard(current.auto) && current.guide.trim() === '') {
|
||||
const active = readyToWork(parsed, stateRuntime, current.item);
|
||||
const settled = transitionItem(active, current.item, 'Completed');
|
||||
await deps.memory.append({
|
||||
|
||||
Reference in New Issue
Block a user