Archived
脚本 e2e 逼出的八条:闸看错了表 · 拒回喂指针 · 零行骗过闸与哨兵 · 早收不该算撞墙
凭证到位后 `w05` 五案例连跑六轮,逼出八条**浏览器走查逮不到**的缺陷 —— 脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候永远不会去试 「空着交上去会怎样」。末轮:跑到的四例全部 released,三例被拒 0 (第五例停在大模型账户余额不足,不是代码问题)。 平台侧(六条): - L 数据工单的提交闸没牙 —— 卡没绑表就回落到必交付物,而数据工单要人补的是 `parts.*` 主数据,不在交付物名单里:闸盯着一堆无关的表、一格不缺,空手交卷一路放行。 契约新增 `ExecutionWorkOrder.gateLedgers`(这张卡该拦哪几张表,发卡的人说了算), `gateAddressesOf` 变三级取一:绑的表 > 卡自报 > 交付物。 - M `finish` 撞 doneWhen 为假的拒回只谈「行与列」,而缺的是一次**工具调用**; 现在摆出判据原文 + 那几个名字此刻的读数,`tool_*_calls` 读到 0 的单独点名(`unmetWhy`)。 - N 人点了决策卡,模型每拍再发一张,连拒五拍熔断。把「他选的是 approve」写进拒回**没用** —— 下一拍它又从头翻一次会滚走的窗口。改结构:`answeredCardFact` 把 「这张卡已答过 → 他选的是什么」做成每拍都在的事实行。 - O 模型传 `requirement: null`,工具 `null.trim()` 当场崩(边界上一律经 `text()` 归一)。 - P 拒回里的 `count=(没写过)` 是 S-FEEL 函数名被当成台账名 —— 假线索模型会去追。 - Q/S 零行 = 零缺,闸与哨兵一起放过空手交卷的数据工单(`emptyGate`:卡自报的表一行都没有 = 没完成);「早收」头一次只落观察、第二次起才算被拒(与「重发已答过的卡」同一把尺)。 活动侧(1.2.7 → 1.2.9):三张执行工单各自声明 `gateLedgers`;说明书③补「发卡前骨架必须先有行」、 ⑤补这张卡的前提(一颗 absent 都不剩);哨兵 `parts_profile_rows >= created` 逐颗对账; S0_bind 写明发录入卡那一拍别抢写需求行。 e2e 脚本加 `JCAI_E2E_BEARER` 直供旁路(服务账号 client_credentials 仍是正路)。 门禁 typecheck / lint / depcruise / test(2091)全绿;api:check 的差异仍只在 org/* 与 api/* (工作树里先前那批鉴权改动),card.d.ts 是本轮的纯新增字段。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TaskId } from '@contracts';
|
||||
import { createTurnRunner, echoTitleOf, handsOff, resendVerdict, sameGist, stalledTurns, toolInputHash } from '../internal/usecase/turn-runner.ts';
|
||||
import { createTurnRunner, echoTitleOf, handsOff, resendVerdict, sameGist, stalledTurns, toolInputHash, unmetWhy, answeredCardFact, earlyFinishTitleOf } 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';
|
||||
@@ -992,7 +992,7 @@ describe('宣告完成', () => {
|
||||
* 被拦下)。声明 doneWhen 就是说「这件事完没完可以算出来」,算出来是假,finish 就该被拒——
|
||||
* 不是让模型自己判断,是让基座去核。
|
||||
*/
|
||||
it('声明了 doneWhen 的项:算出来是假,finish 被拒——不许凭一句话绕过基座的判断', async () => {
|
||||
it('声明了 doneWhen 的项:算出来是假,这一下 finish **不生效**(头一次只落观察,不记被拒)', async () => {
|
||||
const h = harness({
|
||||
parsed: STAGE_MODEL,
|
||||
runtime: READY_RUNTIME2,
|
||||
@@ -1005,6 +1005,25 @@ describe('宣告完成', () => {
|
||||
expect(outcome.runtime.items.find((i) => i.id === 'FREE')?.state).toBe('Available');
|
||||
});
|
||||
|
||||
/*
|
||||
* **早收一次不算撞墙,第二次才算**(2026-08-27 W05 e2e:五案例里三例各白吃一次被拒 ——
|
||||
* 模型把「写行 · 调工具 · finish」排在同一串,工具还没轮到调 finish 先到了,下一拍它自己补上)。
|
||||
* 与「重发已答过的卡头两次无事发生」同一把尺;真在原地打转的单仍看得见(第二次起照旧拒,喂熔断计数)。
|
||||
*/
|
||||
it('同一项第二次早收:照旧被拒 —— 熔断要看得见真卡死的单', async () => {
|
||||
const h = harness({
|
||||
parsed: STAGE_MODEL,
|
||||
runtime: READY_RUNTIME2,
|
||||
action: finish('FREE'),
|
||||
steps: [{ item: 'FREE', intent: '干活', guide: '做法', writes: [], reads: [], tools: [], view: 'x', doneWhen: 'some_rows > 0' }],
|
||||
sfeelCondition: () => false,
|
||||
trail: [{ kind: 'observation', title: earlyFinishTitleOf('FREE') }],
|
||||
});
|
||||
const outcome = await h.runner.runTurn(TASK);
|
||||
expect(outcome.status).toBe('running');
|
||||
expect(h.appended.some((m) => m.kind === 'result' && m.title.includes('行动被拒'))).toBe(true);
|
||||
});
|
||||
|
||||
it('声明了 doneWhen 的项:算出来是真,finish 正常收', async () => {
|
||||
const h = harness({
|
||||
parsed: STAGE_MODEL,
|
||||
@@ -1704,6 +1723,58 @@ describe('办完的项:话要说对,按一下不报错', () => {
|
||||
* 每张扣一次调人预算,四次扣完整单 `exhausted`。卡族不同,病是同一个。
|
||||
* 三档由 `resendVerdict` 定:照发 / 无事发生(头两次)/ 第三次起当被拒(熔断看得见)。
|
||||
*/
|
||||
describe('「这张卡人已经答过了」摆成每拍都在的事实行', () => {
|
||||
const sent = (title: string) => ({ kind: 'card-sent', title });
|
||||
it('**决策卡:把人选的那个值摆出来**(2026-08-27 W05 e2e:五个案例四个死在这里 —— 剧本让模型自己去轨迹里翻回传,而那条痕会被挤走)', () => {
|
||||
const line = answeredCardFact([sent('请审核结论'), { kind: 'card-reply', title: '请审核结论', detail: { value: 'approve' } }]);
|
||||
expect(line).toContain('人已经答过了');
|
||||
expect(line).toContain('他选的是「approve」');
|
||||
expect(line).toContain('finish');
|
||||
});
|
||||
|
||||
it('执行工单没有值 → 说清「空答复 = 照原样确认」,不编一个选择出来', () => {
|
||||
const line = answeredCardFact([sent('评估输入确认'), { kind: 'card-reply', title: '评估输入确认' }]);
|
||||
expect(line).toContain('空答复 = 照原样确认');
|
||||
expect(line).not.toContain('他选的是');
|
||||
});
|
||||
|
||||
it('回传之后又发了**别的**卡 → 有新问题在等,这条不摆', () => {
|
||||
const line = answeredCardFact([
|
||||
sent('第一问'),
|
||||
{ kind: 'card-reply', title: '第一问', detail: { value: 'approve' } },
|
||||
sent('另一件完全不同的事'),
|
||||
]);
|
||||
expect(line).toBe('');
|
||||
});
|
||||
|
||||
it('一张卡都没答过 → 空', () => {
|
||||
expect(answeredCardFact([sent('还没答的卡')])).toBe('');
|
||||
expect(answeredCardFact([])).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('doneWhen 为假的拒回:差在哪要说得出口', () => {
|
||||
it('**缺的是一次工具调用就直说** —— 2026-08-27 W05 e2e:五个案例里三个撞上,拒回只教它去查「行与列」,而缺的是 recommend 没调过', () => {
|
||||
const why = unmetWhy('rows > 0 and tool_w05_recommend_calls > 0', { rows: 9, tool_w05_recommend_calls: 0 });
|
||||
expect(why).toContain('缺的不是行也不是列,是一次工具调用');
|
||||
expect(why).toContain('tool_w05_recommend_calls');
|
||||
expect(why).toContain('tool_w05_recommend_calls=0');
|
||||
});
|
||||
|
||||
it('工具调过了(读数 > 0)→ 不点它的名,只把判据与读数摆出来', () => {
|
||||
const why = unmetWhy('rows = 10 and tool_w05_recommend_calls > 0', { rows: 9, tool_w05_recommend_calls: 2 });
|
||||
expect(why).not.toContain('缺的不是行也不是列');
|
||||
expect(why).toContain('rows=9');
|
||||
});
|
||||
|
||||
it('判据里压根没有工具那一档 → 照旧只摆判据与读数', () => {
|
||||
const why = unmetWhy('a_rows = b_rows', { a_rows: 3, b_rows: 4 });
|
||||
expect(why).toContain('a_rows=3');
|
||||
expect(why).toContain('b_rows=4');
|
||||
expect(why).not.toContain('工具调用');
|
||||
});
|
||||
});
|
||||
|
||||
describe('同一张卡再发一遍的三档', () => {
|
||||
const sent = (title: string) => ({ kind: 'card-sent', title });
|
||||
const replied = (title: string) => ({ kind: 'card-reply', title });
|
||||
@@ -1717,6 +1788,21 @@ describe('同一张卡再发一遍的三档', () => {
|
||||
expect(resendVerdict([sent('这一问')], '这一问').kind).toBe('send');
|
||||
});
|
||||
|
||||
it('**人选的那个值当场摆出来**(2026-08-27 W05 e2e:人点了通过,拒回只给「值在轨迹里」这个指针,模型对不准那一条,连发五次同一张卡熔断)', () => {
|
||||
const win = [sent('这一问'), { kind: 'card-reply', title: '这一问', detail: { value: 'approve', note: '试跑代答' } }];
|
||||
expect(resendVerdict(win, '这一问')).toEqual({ kind: 'echo', answered: '这一问', chose: 'approve' });
|
||||
const rejected = resendVerdict([...win, echoed('这一问'), echoed('这一问')], '这一问');
|
||||
expect(rejected).toEqual({ kind: 'reject', answered: '这一问', chose: 'approve' });
|
||||
});
|
||||
|
||||
it('回传痕里没有值(空串 / 缺 detail)→ 不编,退回原来那句指针话术', () => {
|
||||
expect(resendVerdict([sent('这一问'), { kind: 'card-reply', title: '这一问', detail: { value: ' ' } }], '这一问')).toEqual({
|
||||
kind: 'echo',
|
||||
answered: '这一问',
|
||||
});
|
||||
expect(resendVerdict([sent('这一问'), replied('这一问')], '这一问')).toEqual({ kind: 'echo', answered: '这一问' });
|
||||
});
|
||||
|
||||
it('人答过了 → 头两次无事发生(卡不真发、预算不烧)', () => {
|
||||
const win = [sent('这一问'), replied('这一问')];
|
||||
expect(resendVerdict(win, '这一问')).toEqual({ kind: 'echo', answered: '这一问' });
|
||||
|
||||
Reference in New Issue
Block a user