Archived
A2-A7 全部补齐到验收线;顺手把几处「门拦住却不给路」的坑填了
活动:A2 选型 / A3 寻源 / A4 定价 / A6 议价 / A7 归档五件各补成真活动包 —— 契约宽进(input.required 空,归一在活动内)、台账必填声明(含行级 requiredWhen)、 入场哨兵与 L1/L2 判据一律读基座数、两份件(在办可编辑 + 办结静态快照)、 三案例常备、包内自测。e2e 各三案例 released、行动被拒 0、发口正确、提交闸每单真拦。 基座(共享面,改一处影响所有活动,故每处都带回归): · 重发判断走在站上场之前 —— 人答完那步常已收掉,readyToWork 先抛「不能再往它上面挂活」, 于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要时够不着,A1 基线当场熔断; · 决策卡补幂等兜底(与执行工单同一把尺):头两次无事发生,第三次起才算被拒; · 输出没成形先就地重问一次,原话进工作日志、不再摆上卡面; · 工具调用读数 tool_<名>_calls —— 「机检只读基座数」这条纪律第一次覆盖到工具; · 执行卡 steps/standard 补进出参 schema(从前能到卡面全靠厂商 JSON 模式宽松); · 件的就地更新改逐节点 morph,保存先把新值镜像进只读那份 —— 不闪、不等回声。 A1(基线,全程守住):措辞三档对齐(必填/建议/选填)、禁止拿历史价当客户目标价、 卡骨架从步骤剧本搬进说明书(压成一拍连做时剧本没摊开,会发出没有指导书的裸卡)。 契约两处**新增可选字段**(非破坏):FieldDeclaration.requiredWhen、工单 finalHtml; api-report 同批更新。docs/adr 与 docs/archive 的 13 份文件在工作树里不见了, 本次予以恢复而非提交删除。 门禁:typecheck / lint / depcruise / test(1366)全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TaskId } from '@contracts';
|
||||
import { createTurnRunner, handsOff, sameGist, stalledTurns } from '../internal/usecase/turn-runner.ts';
|
||||
import { createTurnRunner, echoTitleOf, handsOff, resendVerdict, sameGist, stalledTurns } from '../internal/usecase/turn-runner.ts';
|
||||
import type { TurnRunnerDeps } from '../internal/usecase/turn-runner.ts';
|
||||
import type { PendingAcceptance } from '../port.ts';
|
||||
import type { CaseModel } from '../internal/domain/cmmn-parse.ts';
|
||||
@@ -521,12 +521,14 @@ describe('干活前先站上场', () => {
|
||||
* 实测的死锁:模型收完 S5 才发现打标结果没写全,此时所有能挂的项都 Completed,
|
||||
* 唯一没完成的只剩标人工激活的 T_release —— 每拍补写都被「要先 start」拒回来,
|
||||
* 连拒五拍熔断交人,而那几行数据本身合法合规。
|
||||
* 2026-08-02 拆墙后,人工激活的项也**挂得上**了(动它就算启动):
|
||||
* 写照落不变,记账从「挂不上、留痕注明」变成「挂上、点亮进行中」—— 更诚实。
|
||||
*/
|
||||
it('标了人工激活的项:写照落、不挂项、项不动 —— 挂项是记账,不是授权', async () => {
|
||||
it('标了人工激活的项:写照落、挂上点亮 —— 授权面仍是台账声明', async () => {
|
||||
const h = harness({ parsed: STAGE_MODEL, runtime: READY_RUNTIME2, action: writeTo('GATED') });
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
expect(h.written).toHaveLength(1);
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'GATED')?.state).toBe('Available');
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'GATED')?.state).toBe('Active');
|
||||
expect(outcome.status).toBe('running');
|
||||
});
|
||||
|
||||
@@ -629,11 +631,18 @@ describe('宣告完成', () => {
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'FREE')?.state).toBe('Completed');
|
||||
});
|
||||
|
||||
it('标了人工激活的项仍要先 start —— 那条纪律管的是触达世界与关键交付', async () => {
|
||||
/*
|
||||
* **人工激活的项:动它就算启动**(2026-08-02 拆墙)。
|
||||
* 从前这儿钉的是「要先 start,不发就拒」;实测那道墙保护不了任何东西
|
||||
* (start 是模型自己的动作,同一串连发即过),却多出一种死法:A1 一单
|
||||
* 连拒五拍熔断,五次全是「S3_confirm 要先 start」,提示词与拒回都写着,
|
||||
* 模型就是学不稳。人工激活真正的分量在引擎那一侧:不替它启动、不替人收 ——
|
||||
* 那两条各有各的测钉着(哨兵不自动激活 / 自动收步跳过人工激活项)。
|
||||
*/
|
||||
it('标了人工激活的项:模型显式动它 = 启动它,就地过 Active 再收', async () => {
|
||||
const h = harness({ parsed: STAGE_MODEL, runtime: READY_RUNTIME2, action: finish('GATED') });
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'GATED')?.state).toBe('Available');
|
||||
expect(outcome.status).toBe('running'); // 拒了不崩,理由喂回下一拍
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'GATED')?.state).toBe('Completed');
|
||||
});
|
||||
|
||||
it('编出来的项收不了', async () => {
|
||||
@@ -784,7 +793,7 @@ describe('说话按轮不按拍:只有交出控制权的那一拍才进对话区
|
||||
});
|
||||
|
||||
it('轮到人了才出气泡:问一句 / 派工单 / 摆岔路 / 等外部 / 提交放行', () => {
|
||||
for (const kind of ['ask', 'human', 'decide', 'listen', 'submit']) {
|
||||
for (const kind of ['human', 'decide', 'listen', 'submit']) {
|
||||
expect(handsOff([{ kind }]), kind).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -878,13 +887,11 @@ describe('画面不再静默丢帧', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 补全卡 —— 对话卡族,与执行工单是两件事。
|
||||
*
|
||||
* SOP 上 A1-S2「按缺口清单追问补全」画的就是它。从前 Agent **发不出对话卡**:
|
||||
* 行动型里只有 `human` 且只能选执行 / 验收两种工单,于是那一步只能拿执行工单顶 ——
|
||||
* 形似而神非:派一张工单去问几个字段,收卡的人得进待处理、办完再回来,重得不成比例。
|
||||
* **补全卡已全局退役**(2026-08-02 用户定)—— 引擎侧这个行动型不再发卡:
|
||||
* 归一层(agent-runner)会把 `ask` 原地转成零输入执行工单,所以正常到不了这里;
|
||||
* 别的调用方直发就如实拒并指对路(human + execution)。
|
||||
*/
|
||||
describe('补全卡', () => {
|
||||
describe('补全卡退役:引擎不再发 elicitation 卡', () => {
|
||||
const ASK = {
|
||||
kind: 'ask',
|
||||
itemId: 'i1',
|
||||
@@ -892,18 +899,9 @@ describe('补全卡', () => {
|
||||
fields: [{ key: 'NE555P_lead_time', label: 'NE555P 交期' }],
|
||||
};
|
||||
|
||||
it('发出去即挂起等填 —— 调人 = 立监听听人', async () => {
|
||||
it('直发 ask 被拒:一张卡都不发,理由指向执行工单', async () => {
|
||||
const h = harness({ parsed: OPEN_MODEL, runtime: OPEN_RUNTIME, action: ASK });
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
expect(outcome.status).toBe('awaiting');
|
||||
expect(h.issued).toEqual([
|
||||
{ workOrder: 'elicitation', card: { ask: '这两条缺交期,麻烦补一下', fields: ['NE555P_lead_time'] } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('一个格子都没有就拒发 —— 空表单等于没问', async () => {
|
||||
const h = harness({ parsed: OPEN_MODEL, runtime: OPEN_RUNTIME, action: { ...ASK, fields: [] } });
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
expect(h.issued).toHaveLength(0);
|
||||
expect(outcome.status).toBe('running'); // 拒了不崩,理由喂回下一拍
|
||||
});
|
||||
@@ -1225,6 +1223,30 @@ describe('办完的项:话要说对,按一下不报错', () => {
|
||||
expect(h.issued).toHaveLength(0);
|
||||
});
|
||||
|
||||
/*
|
||||
* **A1 基线熔断实测(2026-08-03)**:人答完之后那一步已经收掉,模型又发了一遍同一张卡。
|
||||
* 从前 `readyToWork` 走在重发判断之前 —— Completed 项当场抛「不能再往它上面挂活」,
|
||||
* 于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要它的时候够不着,
|
||||
* 同一条拒回重复五遍熔断(13 拍 124k,发口一个都没有)。顺序换过来才叫墙要配路。
|
||||
*/
|
||||
it('重发**已答过**的卡,哪怕那一步已经收掉 → 无事发生,不撞「已办完」的墙', async () => {
|
||||
const h = harness({
|
||||
parsed: DONE_MODEL,
|
||||
runtime: DONE_RUNTIME,
|
||||
action: { kind: 'human', itemId: 'S3', workOrder: 'execution', card: { title: '请审定这份清单', ask: 'a' } },
|
||||
trail: [
|
||||
{ kind: 'card-sent', title: '请审定这份清单' },
|
||||
{ kind: 'card-reply', title: '请审定这份清单' },
|
||||
] as never,
|
||||
});
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
// 没发第二张卡(人不被打扰、humanCalls 不烧),也没被拒
|
||||
expect(h.issued).toHaveLength(0);
|
||||
expect(outcome.status).not.toBe('awaiting');
|
||||
// 收掉的项原样收着 —— 无事发生不该顺手把它推回场上
|
||||
expect(h.envelope.runtime.items.find((i) => i.id === 'S3')?.state).toBe('Completed');
|
||||
});
|
||||
|
||||
it('**再 start 一次收掉的项是无事发生,不是非法迁移** —— 别让两堵墙对着撞', async () => {
|
||||
const h = harness({ parsed: DONE_MODEL, runtime: DONE_RUNTIME, action: { kind: 'start', itemId: 'S3' } });
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
@@ -1233,3 +1255,44 @@ describe('办完的项:话要说对,按一下不报错', () => {
|
||||
expect(h.envelope.runtime.items.find((i) => i.id === 'S3')?.state).toBe('Completed');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* **重发已答过的卡:执行工单与决策卡同一把尺**(2026-08-03,A4 实测逼出来的)。
|
||||
*
|
||||
* 从前只有执行工单那一路有兜底,决策卡一道都没有 —— 同一个岔路口连发四张,
|
||||
* 每张扣一次调人预算,四次扣完整单 `exhausted`。卡族不同,病是同一个。
|
||||
* 三档由 `resendVerdict` 定:照发 / 无事发生(头两次)/ 第三次起当被拒(熔断看得见)。
|
||||
*/
|
||||
describe('同一张卡再发一遍的三档', () => {
|
||||
const sent = (title: string) => ({ kind: 'card-sent', title });
|
||||
const replied = (title: string) => ({ kind: 'card-reply', title });
|
||||
const echoed = (answered: string) => ({ kind: 'observation', title: echoTitleOf(answered) });
|
||||
|
||||
it('没发过这一问 → 照发', () => {
|
||||
expect(resendVerdict([sent('别的事'), replied('别的事')], '这一问').kind).toBe('send');
|
||||
});
|
||||
|
||||
it('发过但**人还没答** → 照发 —— 未答的重发由卡自己去重管,不归这里', () => {
|
||||
expect(resendVerdict([sent('这一问')], '这一问').kind).toBe('send');
|
||||
});
|
||||
|
||||
it('人答过了 → 头两次无事发生(卡不真发、预算不烧)', () => {
|
||||
const win = [sent('这一问'), replied('这一问')];
|
||||
expect(resendVerdict(win, '这一问')).toEqual({ kind: 'echo', answered: '这一问' });
|
||||
expect(resendVerdict([...win, echoed('这一问')], '这一问').kind).toBe('echo');
|
||||
});
|
||||
|
||||
it('第三次起当被拒 —— 那时是真卡死了,熔断兜底要看得见它', () => {
|
||||
const win = [sent('这一问'), replied('这一问'), echoed('这一问'), echoed('这一问')];
|
||||
expect(resendVerdict(win, '这一问')).toEqual({ kind: 'reject', answered: '这一问' });
|
||||
});
|
||||
|
||||
it('问句只少个尾巴也算同一问 —— 变体重发同样烧预算(与 sameQuestion 同源)', () => {
|
||||
const win = [sent('请审定这份受理需求清单,确认无误后我就往下走'), replied('请审定这份受理需求清单,确认无误后我就往下走')];
|
||||
expect(resendVerdict(win, '请审定这份受理需求清单').kind).toBe('echo');
|
||||
});
|
||||
|
||||
it('空问句不拦 —— 认不出身份的卡不该被这道兜底吃掉', () => {
|
||||
expect(resendVerdict([sent(''), replied('')], ' ').kind).toBe('send');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user