Repository navigation
Conversation
单仓单包变成 workspace:packages/* + apps/* + server 三处成员。 server 保留 testpilot-server 这个名字与目录布局(改名 apps/gateway 留到搬迁那一步),但它的独立 lockfile 删掉了——网关一旦 import @testpilot/harness-core,两份 lockfile 就会各解各的版本。 onlyBuiltDependencies 上移到 workspace 根:better-sqlite3 / esbuild / puppeteer 的构建脚本要在根这一层放行,否则子包装不上。 .gitignore:放行 .env.example(它是文档,不是密钥),忽略 .data-selftest/——自举跑的是本产品的第二个实例,那份状态不属于本仓库。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
垂类无关的那一半,独立成包。六块:
graph/ 自研 DAG 运行时。连线用 inKind/outKind 在保存时校验,载荷用
zod 在运行时校验;稳定拓扑序、按节点缓存产物、only/from 部分
重跑、断点、预算三闸(budget 与 done 是两种结束)、消融集、
逐节点成本、版本化与逐条 diff。没有用 LangGraph,理由见
docs/spec/04 选型表。
harness/ birpc over fork IPC 的进程监工:每进程重启策略、心跳巡检把
「卡死」当崩溃、优雅停机与孤儿清理、声明式 capability recipe
(tcp/http/rpc 健康探针)。两个方向都不设 RPC 截止时间——一次
用例要跑几分钟——存活性交给心跳,而通道一断,在途调用立刻以
「对面没了」失败:慢是允许的,永远等下去不是。
model/ 跨进程串行闸(借还 + 崩溃归还)、上下文预算(份额分配 + 余量
再分配 + 截断量要报出来)、ChatRequest 强制拆 stable/variable
两半(前缀缓存只在前导字节一致时命中)、FakeModel 与
RecordedModel(VCR:replay 遇到没录过的请求报错,不偷偷打真
模型,否则测试会在一台机器上过、另一台上挂)。
obs/ 事件信封 + 落盘 + 广播(先落盘后广播)+ 保留策略。lineage 从
第一天就开,历史补不回来。
eval/ Coverage(确定性关键词,语义只做兜底且标注为不可比)、以 Fail
为正类的 P/R/F1 + 滑向判定、McNemar 精确检验、配对数值比较报
方向计数、消融、缺陷检出按变异测试口径给分。
config/ harness.config.ts 的解析与合并,env 覆盖文件。
interpolate 从 server/src 原样搬入 util/:${env.*}/${secret.*} 的解析
是执行与生成两边共用的,留在网关里就得跨进程复制一份。
144 个离线测试,不打模型。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
core 之上的垂类那一半,以 testingPack() 自我打包,网关与 agent 各调一次
registerPack。核心的验收测试是反面的:一个 core 从没听说过的假垂类
(合同审阅)也能注册并跑起来。
casegen/ G1:spec → stories → 按设计方法展开 → 文本用例 → 门禁①。
设计节点按故事逐个调用(短调用命中前缀缓存,且一个故事的坏
回复不该赔上整批)。门禁①只打分标记不拒收,理由写在文件头:
上一版那种拒收门开火十几次、真阳性零次。
attribute.ts:出处是查出来的不是问出来的——材料按分隔符拆回
一份份文档,一句原话属于哪份由确定性字符串定位决定,模型自己
填的 source 只在定位不到时兜底,且必须指向这次真的读过的文档。
codegen/ G2:文本用例 → 代码 → 门禁② → 有界修复。代码的含义以归一化
动作表保存(执行、门禁、修复三方必须对同一件事说话);公共
前置被提取成片段被引用而不是复制;修复循环双口径发布通过率
(loose 与 strict 并排,扣掉「只有把断言改松才变绿」的那些)。
门禁②有拒收权但只对事实:没有断言、用了不允许的调用、把凭证
写死——编造的凭证不只是失败,它以嫁祸产品的方式失败。
exec/ 从网关整体搬入:用例执行、探索、实时调试、诊断、链上断言、
注入式钱包、会话捕获。oracle.ts 是新的:text/noText/url/count
(tier1)与 delta(tier2)由程序判定、记 decidedBy: machine,
不需要选择器也不需要模型——在这之前 tier 只是一个写在产物里的
形容词,执行时拿到的全是 judge。
baselines/视觉基线(像素)与性能基线(数值),共模失效时唯一还站得住的
那类锚。
critic.ts HarnessCritic:读失败轨迹提 harness 级改动。它只提案,配对
评测裁决。禁令有输出侧检查——只写在提示词里的规则没人执行。
guard.ts 域名白名单 + 不可逆动作拦截。
failure.ts 失败归因分桶:infra 的红和断言的红长得一样,混在一起会让人
去查一个没有问题的地方。
142 个离线测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
一次长跑不该占着网关的事件循环,网关重启也不该打断它。
apps/agent 图运行时的所有者:runGraph 跑在这里,节点产物归它。它没有
库、没有环境绑定、也没有浏览器——需要执行一条用例时反过来
请网关的 execCase。
apps/runner 浏览器那一侧全在这里:用例执行、截图与性能捕获、链上断言、
两种形态的探索、实时调试、dapp 校验、钱包检查、MetaMask
冒烟、会话捕获。验收判据是可数的:server/src 里 launchSession
出现 0 次。
两者都由 core 的监工托管:心跳、重启策略、优雅停机。smoke 测试是端到端
的——真的 fork 起来、答健康检查、能跳心跳。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
浏览器与图运行时都搬走之后,网关剩下的是库、环境、密钥与那条把它们接起来
的线。新增的路由按职责分文件,不再堆在 index.ts 里。
procs/ws /api/processes 起停与 /ws。WS 带版本协商(版本不符直接拒连,
前端不再重连死循环)与 lastEventId 续传,落后太多的客户端被
要求 resync 而不是收到半截流。子进程上报的事件在网关边界过
zod:坏 kind 变成 WIRE_INVALID 错误事件,不进 lineage。
graphs 图的保存即校验、版本落库与逐条 diff、异步起跑、单节点
only/from 重跑、断点、预算与续跑。续跑现在带着已花费一路走
下去——此前设了上限的运行一被续上就变成不限额的,而且最后
记下的花费比真的花的少:一个少报花费的报告比没有报告更糟,
因为它看起来像一次测量。
review 产物落库不等于入库:生成的用例先进复核队列,批准才进看板,
批准时把出处(故事/方法/tier/门禁分/来源运行)一起带上。
编辑、逐字段 diff、用同一把尺子重打分、批量调整、按门禁意见
重生成。产物本身不被改写——它是门禁打过分的证据。
evals/ 配对评测(两臂可差消融开关、节点参数与图版本,并校验两臂是否
evalspecs 真的不同)、缺陷检出、Critic。评测定义进仓库(evals/*.json),
此前它只活在一次 POST body 里:改了没人看得见的判断依据,
不再是判断依据。
exec 对 runner 的 RPC 外壳:执行、诊断、交互、取消。
trace 需求追溯:引文按形状找材料节点去比,材料与 spec 分开判——
拿被转述压缩过的 spec 当原文比,会把整理者换的说法判成编造。
codeline 用例 → 代码 → 变更的那条线。
continue 跑到门禁之后怎么往下走,做成显式的续跑候选。
pending 待审批基线(视觉 + 性能)。
materials /api/materials:材料改成勾选。路径拼对了、选的却是另一份,
此前不会报错,只会在二十分钟后变成一批答非所问的用例。
datadir TP_DATA_DIR / TP_INSTANCE 实例隔离——自举要同时跑两个
TestPilot,它们绝不能共用一个库。
queue 换 p-queue,接口不变。
runs 台账加 origin/projectId/wfRunId:修复环跑过的用例现在会写进台账,
三种来源分开显示且可筛(同一个「通过」在三种里含义不同)。通过率不再把
MODEL_UNAVAILABLE 这类环境失败算进分母,被排除的条数单独显示——悄悄
缩小分母也是一种谎。历史执行不回填:repair 报告里没有执行时刻,补一个
等于伪造。
网关重启后把仍标 running 的运行与评测改判 interrupted:一个可以永远卡住
的状态没人会信。
98 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
原来是路由器 + 五个外壳 + 十九个分节,人要先知道东西在哪扇门后面才能开始。
现在是一个 Workspace:左边画布是执行图的真实视图,右边是选中节点的产物,
其余一切按需从 drawer 里拉出来。
Workspace 画布可编辑(增删节点、拉线、拖动摆位),草稿在浏览器里,保存
才发给网关且必然新起一版。节点状态由 wf.* 事件驱动,中途打开
页面从 lineage 补齐。节点卡片上的红点即断点;被点名要跑的节点
它自己的断点不拦,否则「继续」会永远停在同一个节点上。
顺带修掉一个一直存在的真 bug:画布是受控模式却没接
onNodesChange,React Flow 的测量结果每次重建都被抹掉——所以
fitView 一直是空操作、运行中的节点还会闪。
Review 复核队列:编辑、逐字段 diff、重打分、批量调整、重生成。查找
替换排第一是有出处的:这个项目反复出现的缺陷就是生成的用例
编凭证,修法是同一个替换。
Evals 配对评测与 Critic 建议。被评对象、两臂差什么、提示词指纹不同
时直接判定「这是两个 harness,不是两个臂」。
Processes 进程卡片:类型/PID/心跳/重启次数/当前任务/内存/CPU/累计成本。
最后一项是自举用例抓出来缺的。
Capabilities 能力卡(状态/健康检查/起停)+ Chat。Chat 只起草,从不落地:
校验通过才出现「保存」,保存与启动是两次决定。
Trace 需求追溯双向视图,引文分「材料/spec/查不到」三档并可筛——一个
只能报总数、指不到具体哪几条的检查等于没有报。
CodeLine 资产 → 测试代码 / 变更,行级 diff。
Baselines 待审批基线。
Delivery 交付。开 PR / 写 CI / 覆盖回传是明确标着「未实现」的说明卡而
不是按钮:一个假装能用的交付页,正是这件事最不能有的东西。
DiagnoseDrawer 跨产物推理按需打开,附着在它解释的那个对象上。常驻 340px
对话栏已删——一条永远在那里的对话栏,是在宣告这个产品主要靠
聊天驱动;它不是,也不该是。
网关地址从写死 localhost:5301 改成 ?api= 可切换并记住,顶栏显示实例名
(sut · 127.0.0.1:5401)。此前没法让一个前端指向被测实例,自举的执行环
一直做不起来,就卡在这一行。
看板卡片显示故事/方法/tier/门禁分/退化标记,tier 没有机器判据时显示
t1?——声称不能印得像属性。九条旧路径全部落到正确新址。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fixtures/mock-spec Acme Portal 登录模块(如实描述内置 /testlogin,4 段
约 1500 字)+ 人写的 12 条黄金清单,其中 3 条 heldOut
永不参与调参。规模照顾本地 27B 的上下文。
fixtures/self-test 从 docs/03 的交互契约手抽的清单,自举用。必须人写:
它是 Coverage 这个数唯一的意义来源,模型生成就等于让
被测者出题。
fixtures/recordings VCR 录像,L1 图级测试回放用。
evals/ 三份评测定义:design-methods / oracle-grading /
selftest-dedupe。此前定义只活在一次 POST body 里,
结果进了库、定义本身没有历史、进不了 review。
其中最要紧的设计是 expect 不是断言:它记下跑之前的预判,跑完与实际并排
显示,但不会让评测失败——一个会红的评测会被人调到绿为止,而那正是评测
本该防住的事。预判错了是这份评测最有价值的一次输出。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
从单文件「harness-agent 重构架构与分阶段方案」拆开,每份回答一个问题: 01 设计依据与实测数据(每条决定背后的数字与出处)、02 业务规格与用户 故事、03 UI 交互规格、04 技术架构、05 harness 架构、06 通讯协议、 07 自举与自我迭代、08 分阶段路线与验收、09 交互重构调研(含原型 HTML)、10 原型落地记录与不合理清单。 02 与 03 不只是给人看的:它们是自举时喂给 harness 的真实输入语料,所以 写作要保持机器可读的规整结构。改这两份等于改测试输入。 08 是逐项进度表,四个自举里程碑目前的实况写在里面,都是否定结论: M1 Coverage 0.50/0.58 未达 0.7,区分实验 p=0.375 判定「每故事摊太薄」 不成立;M2 三轮 1/4 通过、零退化,根因是阶段一标的 tier 在执行层兑现 不了;M3 闭环跑通但 Critic 那条建议被配对评测判为 p=1.0 无方向——这次 闭环真正的产出是它拦住了一条听起来对的建议;M4 链路通了但缺陷是人为 注入的,按口径不算达成。 10 的第二节是「已记未修」,五条,其中最要紧的一条是:产出 → 看板 → 夜间回归这半条链从来没有真正合上过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
此前唯一的靶子是 /testlogin——50 行 HTML、一个登录框、1500 字素材、
12 条清单。它摊不开设计方法,也摊不开 tier;M1 的缺口被诊断为「素材不够
具体」,而这个素材本来就没有多少东西可以具体。
SauceDemo(Sauce Labs 的公开 E2E 练习靶站)作为基准:14 个故事,覆盖登录
六种账号、四种排序、购物车增删、结账三步校验、税率计算、未登录访问拦截。
它还自带缺陷注入——problem_user / performance_glitch_user / error_user /
visual_user 四个账号是站点故意做坏的,同一份用例在 standard_user 上该全绿、
在它们上面该有用例变红。
swag-labs.md 素材。**由人对着运行中的站点逐项实测写成**,不是凭记忆
也不是读代码:六种账号的登录结果、每一个必填校验的错误
原文、四种排序的首件、结账三步的每一个字段、税率
($39.98 → 税 $3.20 → 合计 $43.18,即 8%)都有一次实际
观测作为出处。文档开头写明了它的已知偏差:素材作者读过
站点的实际行为,所以由它算出的 Coverage 量的是「能不能把
素材拆成用例」,不量「能不能补上素材没写的东西」。要去掉
这个偏差需要一份独立作者写的规格。
gold-checklist.json 21 条,人写,其中 4 条 heldOut。已用 4 条手写探针验证
关键词不是坏的(命中 G-01/G-05/G-11/G-17)——一份关键词
写错的清单会把诚实的产出判成没覆盖,而那个数看起来和
harness 真的不行一模一样。
护栏放行 www.saucedemo.com:基准的重头就是结账,而 checkout/pay/下单 正是
blockIrreversible 在白名单外要拦的东西。放行的依据是这个站点上没有真实的
东西,Reset App State 能撤销一切。实测过这条不对称:同样一条「点击 Finish
完成下单」,在 saucedemo 上放行,在别的域名上以 GUARD_IRREVERSIBLE 拒绝。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`design.cases` 的规格靠图上一个 `specText` 参数传,而**建图时只有规格的路径、 没有内容**(文件要等节点运行时才读)。于是 `g1Graph` 无条件填的 `params.specText ?? params.spec.text ?? ""` 每次都落到空串,图里存下一个看起来 像「已接线」的空参数。 实测证据:存过的 7 个图版本(g1-text-cases v1–v4、selftest-g1 v1–v3)全部 specText 缺失;21 次历史运行全部没有 override。写每一条用例的那次调用只看得见 故事的标题与验收标准,规格一个字都没进过提示词。 这动摇了哪些结论——不是说补上就会变好,而是那几个结论建立在一个不成立的前提上: - M1 的根因分析(07 §7)把缺口归给「清单比文档具体」,而 design 节点没读过那份文档 - 5 条 vs 10 条区分实验(p=0.375)两臂都是盲的 - M3 的 oracleGuidance 配对评测(p=1.0)两臂同样都是盲的 - fitToBudget 的截断机制在生产里从未生效过——被预算的那半永远是空串 一处反讽:复核队列的 regenerate 是**有**规格的。重写一条用例时模型看得到规格, 第一次写的时候看不到。 修法与出处(origin / derivedFrom)已有的做法一致:规格跟着故事一起往下走 (`StoryBundle.specText`)。参数保留为覆盖,但**空参数不再覆盖**——参数作为覆盖 是对的,一个空的不该压掉真正的规格。图在按路径给规格时不再写这个键。 没被发现是因为没有任何一处检查过「规格进没进提示词」:产出看起来完全正常,只是 系统性地薄。所以新测试断言的不是「有这个字段」,而是那段文字出现在请求里。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
批准写进看板的 `steps` 一律取自**阶段一**的文本用例,而修复循环改的是阶段二的 动作。于是它加的等待、改的措辞一条都传不下去——看板今晚仍然按原始步骤跑,而且 可能因为修复循环**已经修好的那个理由**而挂。 现在步骤按优先级取:人在队列里的编辑 → 阶段二真的跑过的动作 → 阶段一的文本。 人的编辑仍然排最前,那是唯一一次有人看过这条用例并且表过态。 共享前置在这里展开:看板没有片段这个概念,一条用例要能独立跑起来。断言不进步骤 ——它是判决,看板用 `expected` 与 `oracle` 表达。 顺带把 codeline.ts 里一句说过头的注释改准。它写着「The runnable code belongs to a case — it is what will execute tonight」,而代码里没有任何一处会执行那段源码: 执行的是从它解析出来的动作表。这与「保留含义而不是信任文本」是同一个决定, 但注释把它说成了另一件事。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
两处都是实测撞到的。 **判据被打回原形。** `specForCase` 无条件写 `aiAssert`,不读 `tc.oracle`——平台内 刚兑现的 tier1/tier2 一导出就全变回「模型看一眼截图然后表态」,导出的套件整套退回 tier3,而 tier 这个标签好不容易才摆脱这个状态。现在导出带一份 `tests/oracle.ts`, 语义与 `exec/oracle.ts` 一致(文本按可见正文包含、次数按字面量出现次数、delta 在 动作前后各读一次),有 oracle 就由程序判定,没有才退回 judge——和平台内同一条规则。 `delta` 需要的前置快照在动作之前取:关系需要两次观察。 **中文标题的用例互相覆盖。** 导出的 `slug` 剥掉了 CJK,而这个平台产出的用例标题 **就是**中文的——每条标题都塌成空串、退回 `"case"`,于是同一优先级下所有用例写进 同一个文件,一批用例导出之后只剩最后一条,一声不响。现在保留 CJK,并且撞名时带上 用例 id:一个静默覆盖掉另一条用例的导出,比导出失败更糟,它看起来是成功的。 顺带只解构真的用到的 fixture——一条由程序判定的用例不该顺手把判定模型的 fixture 也拉起来。 另修 g2.test.ts 一处早就存在的类型错误(`textCaseFor` 只收三种设计方法,而那条 真实误报用例的方法是 state-transition)。测试跑得过所以一直没人看 tsc。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
清库是把旧 .data **移走**而不是删掉(lineage 补不回来),但 .data.bak-* 没在忽略 名单里——它带着加密密钥、整个库和全部 artifacts。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
此前这一层恒等于**两屏**:入口页,加上一次模型调用往前走一屏,收工。对任何一个 登录页之后还有几屏的产品,材料里都缺着大半——而缺的部分在下游完全看不出来: 规格照样整理得出来、用例照样生成得出来,只是系统性地少了那几屏对应的一切, 最后表现为一个没有解释的覆盖率数字。 现在是 loop-until-dry:每轮采一屏、判是不是没见过的、没见过就继续往前走, **连续 N 轮没有新界面就停**。不用「走满 N 轮」是因为一个产品有几屏事先不知道—— 走满固定轮数要么半途而废,要么在最后一屏上原地打转还接着花钱。 判重取「地址 + 控件集合」:只看地址会把同一个列表页的两次分页当成两屏, 只看正文会把加载态和加载完当成两屏。控件是这一屏**能做什么**,而探索问的正是这个。 给模型的指令里带上已经去过的地方。不带,它会在同一个主按钮上来回点,每一轮都花掉 一次模型调用而什么也没多看到。同时明令不许登出、不许删除、不许提交不可逆的东西。 材料末尾多出一段「这次探索走到哪为止」:采到几屏、为什么停、哪些地方没能走进去, 以及一句「没有出现在这里的功能,是没有被看到,不是不存在」。这是这份材料唯一能 自证薄不薄的地方——`spec.compose` 被要求把材料没说的记进「没有答案的地方」, 而它只有在材料自己说了「这里我没看到」的时候才做得到。 两个参数:`maxScreens`(默认 6,成本闸——每往前一屏一次模型调用,本地模型几十秒) 与 `dryRounds`(默认 2)。屏数与停止原因随节点产物报出来,只采到一屏时单独说一句。 网关那侧的 notes 上限从 24000 提到 60000:探索改成循环之后,旧上限会在第四五屏上 把后面的界面**整段切掉**,而被切掉的部分在下游看不出来——材料看起来是完整的,只是短。 循环本身要浏览器和模型才跑得起来,所以测试钉的是接线与自陈(参数有没有传下去、 屏数有没有报出来、只采到一屏时有没有明说)。循环行为待一次真跑验证。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
以代码为准逐节点整理:材料的三个来源 → 唯一的规格产出处 → 故事 → 用例 → 门禁① → 复核队列 → 代码 → 门禁② → 执行与有界修复 → 看板 → 导出。每一层写清它是靠什么把 粗粒度的东西拆细的——四件套(角色禁令 / schema 锁死 / 字段级判据 / 交卷后回查), 外加 stable-variable 那条前缀缓存的硬约束。 §3.0 是这次整理最重要的产出:`design.cases` 在 2026-08-27 之前从来没有见过规格。 7 个存过的图版本全部 specText 缺失、21 次历史运行全部没有 override,写用例的那次 调用只看得见故事的标题与验收标准。它动摇了 M1 的根因分析、5 vs 10 区分实验与 M3 的 配对评测——不是说补上就会变好,而是那几个结论建立在一个不成立的前提上。 §13 从「三处链是断的」改成「三处断链(已修)」,逐条写「修之前 / 修之后」,并补上 第四处(导出的 slug 剥掉 CJK,一批中文用例只剩最后一条)。末尾归纳了这四处的共同点: **全是「看起来成功」的失败**——产物结构完整、照样能跑、照样导得出来,只是少了它本该 带上的那一半。唯一的防法是让每一处都能自陈,这一轮加的检查全是这一类。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上一版只在离线测试里验过接线,循环本身没跑过。真跑第一次就死在我自己写的指令上, 之后每一次跑都换出一个新问题。八个,只有一个是离线测试抓得到的。 **① 指令给太大。** 把目标 + 策略 + 已访问列表 + 三条禁令塞进一次 `aiAction`, Midscene 拆不动,`Replanning 10 times` 之后放弃,一轮白烧七分钟。它要的是「点某个按钮」 这种一句话能说完的事。改成:**去哪由代码决定,动作只有一句话**。 **② 隐藏元素被算进控件表。** 抽屉菜单里的项一直在 DOM 里,于是「菜单打开了」这件事 判重根本看不见(控件集合前后一样),一次成功的点击被记成「没有新界面」;同时探索会去点 一个屏幕上根本不存在的东西。改成只采 `checkVisibility()` 为真且尺寸非零的。 **③ `tried` 按屏幕签名记。** 菜单一开签名就变,同一个按钮在新签名下又成了没试过的—— 菜单开↔关来回抖,每轮都把对方的控件当新的。**这是我上一条修复自己造的回环。** 签名的用途是「这算不算一屏新的」,不是「我做过什么」;后者跟着地址走才稳。 **④ `href === here` 被当成「不用去」。** SauceDemo 的商品链接全是 `href="#"`,解析出来 等于当前地址,于是商品详情一个都进不去。**一条把整块功能判成「不用去」的规则,比没有规则 更糟,因为它看起来在正常工作。** 现在它意味着「这是 JS 驱动的链接,点它」。 **⑤ `a[href]` 漏掉没有 href 的链接。** 购物车就是这样一个纯图标 `<a>`,`innerText` 为空、 没有 href——而它是通往购物车与结账三步的唯一入口。选择器放宽到 `a`,文案用 `title` / `data-test` / href 路径兜底。 **⑥ `goBack` 空转。** 走到历史开头后 `goBack()` 什么也不做,而这一轮又没试任何控件, 于是一直「退」到预算烧光——实测一次 40 轮里 32 轮是这么没的。退了一步却回到同一屏就停。 **⑦ esbuild 的 keepNames 把 `__name` 带进了 `page.evaluate`。** `const f = (x) => …` 这种具名内部函数会被包成 `__name(f, "f")`,而 `__name` 只存在于 打包产物里;evaluate 传的是函数源码,到页面里就是 `ReferenceError`。表现为「0 个控件」, 静悄悄的——顺手把吞掉它的那个 `.catch(() => [])` 改成会说话的。 **⑧ 真实鼠标点击对 JS 链接无效。** 实测 `page.click` 点商品链接不换屏,页内 `el.click()` 换。探索要的是覆盖,不是交互保真——保真是执行用例那一层的事。 除此之外两条不是 bug 的改动,方向是同一个——**把模型从不该它管的地方拿掉**: - 同源链接走 `page.goto`,零模型调用:点一个 `<a>` 就是走到它的 href,这是查得出来的事实。 - 按选择器点,不问模型:元素是我们自己刚采下来的,选择器也是自己生成的 (`data-test` → `id` → nth-of-type 路径)。把它翻译成一句自然语言再让一个看截图的模型 去屏幕上找回来是纯损失——而且它找不到 `data-test` 这种屏幕上不显示的名字时**不报错, 只是什么都不做**,实测连着四轮都被记成「没有新界面」。 模型在探索里现在只剩一处用途:登录(凭证写在页面上,需要看着页面读)。 最后一条是排序:**抽屉/菜单开关最后再试**。它在 DOM 里往往排最前,于是探索每到一个新页面 就先开菜单、再点「All Items」退回列表——抽屉把探索一次次拉回起点,7 屏里 3 屏只是 「某页 + 菜单打开」。先走完这一屏自己的东西,再看抽屉里有什么。 实测走势(SauceDemo): 1 屏 / 910 秒 → 2 屏 / 562 秒 → 3 屏 / 117 秒 → 7 屏 / 30 秒 → **14 屏 / 45 秒** 最后一次走到了 /checkout-step-one.html,路径是 登录 → 列表 → 购物车 → 商品详情 → 结账第一步 → 加购 → 购物车(1件) → 再加购 → 购物车(2件)。 还差结账第二三步:它们要填完表单才能推进,而填表单需要模型。那是另一类改动,单独做。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
无可见文案的控件用 title / data-test / href 兜底是对的——不兜底,SauceDemo 的购物车
(纯图标 `<a>`,innerText 为空)就整条丢失,而它是通往购物车与结账三步的唯一入口。
但兜底来的名字**不能冒充界面文案**。实测里 `data-test="login-button"` 就这样一路漏进
材料 → 规格 → 断言,产出了一条:
oracle {"kind":"text","value":"login-button"}
断言 页面存在一个标签为 'login-button' 的按钮
这条用例执行时必然失败,而且失败得毫无道理——屏幕上从来没有这几个字。
整个下游的设计基础是「逐字引用界面文案」(`COMPOSE_STABLE` 与 `CASES_STABLE` 都明写了
这一条),所以材料一旦把内部标识符写得像文案,后面每一层都会当真,最后变成一条
指着产品说它坏了的用例。现在兜底名字一律写成 `(无可见文案·<来源>)`。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
自托管的 oMLX 要把机器让出来。TokenHarbor 是 OpenAI 兼容网关,免费档三个模型, 实测下来只有一个同时满足我们两个消费者的要求: | 模型 | 文本 | guided decoding | 收图片 | |---|---|---|---| | deepseek-v4-flash:free | ✓ | ✓ 干净 | ✗ 明确拒绝 | | mimo-v2.5:free | ✓ 但带 reasoning_content | ✗ 不守 schema | ✓ | | **qwen3.8-27b:free** | ✓ 干净 | ⚠ 只是建议不是强制 | ✓ | 选它还有一个原因:和原来那颗同族,`MIDSCENE_USE_QWEN3_VL=1` 与全部提示词都不用改。 **为什么视觉是硬门槛**:这套东西有两个模型消费者,要求不一样——harness 自己的调用 (casegen/codegen/repair/critic)是纯文本,而 Midscene 驱动浏览器发的是**截图**。 免费档若不收图,用例还生成得出来,执行、探索的登录那一步、judge 断言会全部废掉。 接入点三处,都在配置里: - `server/.env`(已忽略,密钥不进仓库):base URL / key / 模型名,附一段切回本地的注释 - `harness.config.ts`:model-proxy 的 `MODEL_UPSTREAM` 指向 https —— 代理用 fetch 转发 并原样带 Authorization,https 上游可直接指,不用改代码 - `model.concurrency` 1 → 3:那个 1 是因为自托管那颗并发就 502。换成托管之后限制变成 **限流**(免费档 20 次/分钟)而不是「它会崩」。G1 那十几次串行文本调用是这条流水线 最长的一段,摊开是几分钟对半小时的差别。 顺带修掉一个会误导人的假阴性:`/api/model/test` 的探针图是 **1×1 像素**,托管网关直接 拒收("too large, corrupted, or an unsupported format"),于是自检把一个能看图的模型 报成 `notMultimodal`——而它的后果是让人以为执行链路用不了。换成 16×16,自检现在回 `ok · Reachable and multimodal`。探针要小到不花钱,也要真到不被当成坏文件。 `enable_thinking` / `chat_template_kwargs` 实测 TokenHarbor 照收不误,那部分不用动。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G2 第一次在真站点上跑完 5 条用例,结果 loose/strict 都是 0.4、零退化。两条通过的是
登录页那两条,另外三条全部报:
Replanning 10 times, which is more than the limit, please split the task into multiple steps
生成的代码本身是对的——前置真的变成了动作,凭证走了 `${env.*}`/`${secret.*}`,门禁②
score 1.00、0 拦截。坏在**第一句的海拔**:
await agent.aiAction('Navigate to the product list page');
这是一个**目标**,不是一个动作。在登录页上它无法达成,驱动模型 replan 十次后放弃。
和我在探索循环里犯的是同一个错,只是这次是提示词教模型犯的。
两处修:
**① 归因。** `Replanning …` 掉进兜底档变成了 `EXEC_ASSERT` / `assert`——一次**由用例
措辞造成**的失败,被记成「产品没通过断言」。失败分档机制存在的全部理由就是不让这种事
发生,而这一类正好漏在网外。新增 `EXEC_PLAN`,归到 `locate`:跟「找不到元素」一样是
可以靠改措辞解决的,修复循环该去改用例,而不是走「这是产品缺陷」的出口。
**② 提示词。** 明说每个 aiAction 必须是**一个人一秒钟能做完的具体交互**,不许是目标
('navigate to the product list page'、'complete the checkout'),也不许是等一个页面
('wait for the list to load and display the products');并且运行本来就会打开入口页,
不要再生成一步导航过去。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
此前探索只把走到的**点**留下来,走过的**边**全部丢掉。于是材料里只有一张张屏幕的静态 清单,`spec.compose` 整理出的 20 条规则全是「X 页显示 Y」——一条转移、一条守卫、一条 领域规则都没有。每一层都没坏,是材料里没有产品模型,后面每一层就都不可能有。 这个产物不是新发明:Crawljax(Mesbah 等,ICWE 2008 / TWEB 2012)把它叫 **state-flow graph**,是自动化 Web GUI 测试领域十五年的标准中间产物;「检查页面 → 状态抽象 → 并入状态转移图 → 选下一个动作 → 派发」这个循环就是我们现在的循环。 **状态抽象做成一族可替换的函数,名字随图记下来**(`route` / `route+controls` / `route+controls+title` / `url+controls`)。横比六种抽象的实证研究(Liu, Yang, Zhang, Xie 2026)把它认定为测试有效性的关键变量,而且严格细粒度的抽象正好配 model-based 策略——我们这一类。写死了既不能进消融清单,也没法和别人的结果比。 那篇论文里的两句话,正好是我在这一格上连撞的两个 bug: - 隐藏元素算进签名 → 菜单开关看不出区别 → **把新状态当旧的(漏测)** - 拿签名当 tried 的键 → 同一按钮反复点 → **把旧状态当新的(冗余)** 边记到 ScenGen(ASE 2026)的粒度:动作类型 / 目标控件 / 选择器 / 输入数据 / 结果。 只记 (from, to) 三元组在下游写不出用例——「怎么走过去的」正是用例的步骤。 **走不通的边也记。** 只留成功路径,材料看起来就像这个产品没有走不通的地方,而那些地方 正是下游「没有答案的地方」的来源之一。 图既进材料正文(先看结构再看正文——实证结论是「精简的功能级上下文」对 LLM 最有效, 原始屏幕转储不是),也作为结构化字段挂在 `SpecMaterial` 上,供后续按路径推流程、 按转移算结构覆盖率。**零额外模型调用**:全部来自循环已有的变量。 实测(SauceDemo,抽象 route+controls):12 个状态、21 条转移,含 `/checkout-step-one.html`,42 秒。 顺带修掉第一版一个标错:「回到已知状态」被标成「状态未变」。判据写成了「目标见过没见过」, 而它该是「from 与 to 相不相等」——前者是一条真实的转移(购物车点继续购物回到列表, 路由都换了),算结构覆盖率时要计入;后者才是原地不动。标错了下游读起来像什么都没发生。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 第一次用探索来的材料跑完整条流水线:80 条用例、门禁分 0.588,人工复核**基本 不可用**——大多是「X 页显示 Y」,且以扁平列表呈现。这份文档回答问题出在哪一层、怎么改、 以及每条改动的文献依据。 诊断:每一层都没坏。`spec.compose` 的第一条禁令就是「你整理,你不撰写」,材料里只有一张张 屏幕的静态清单,它就只能产出静态清单,而且它老老实实把缺的写进了 unknowns。 **问题在上游——探索走过了每一条边,却一条都没记。** 学术定位(这件事不必重新发明): - 状态转移图叫 state-flow graph,出自 Crawljax(Mesbah 等,ICWE 2008 / TWEB 2012), 是 AWGT 领域十五年的标准中间产物;「检查页面 → 状态抽象 → 并入图 → 选动作 → 派发」 这个循环就是我们的循环 - 状态抽象是实证认定的关键变量(Liu/Yang/Zhang/Xie 2026 横比六种),而论文里描述它失效 的两句话,正好是我们那天撞的两个 bug:把新状态当旧的(漏测)、把旧状态当新的(冗余) - 边的粒度参照 ScenGen(ASE 2026):只记 (from,to) 三元组在下游写不出用例 - 「精简的功能级上下文对 LLM 最有效」是喂结构不喂原文的实证支持 - GraphRAG 的两级索引(局部/社区摘要)就是「按模块 map、跨模块 reduce」的成熟形态 方案分五步,S1 已完成。其中两处刻意的自我修正记在文档里: - 曾把 0-switch 覆盖说成「度量困境的出口」——它解决的是分母从哪来,不解决「覆盖高是否 等于抓得到缺陷」。实证说代码覆盖与缺陷发现能力**弱相关**,所以三个数并排,不设单一北极星 - 「专业 spec」要的是海拔不是文件数;拆文件要改 kind 契约,收益只是观感 基准应用换成公开 AWGT 套件(Dimeshift / Pagekit / Petclinic / Phoenix / Retroboard / Splittypie)——同一套应用可直接对上已发表基线,且本地可起、不受限流。SauceDemo 降为冒烟靶子。 论文角度:多数 LLM 测试生成工作用模型判断的断言,且**不量化其中有多少本可以是确定性的**。 判据有据率(实测 49%)、确定性回查、修复循环作弊度量都是现成的贡献点;最有价值的可能是 把状态抽象的影响一路量到**用例质量**,而不是止步于代码覆盖。 三条限制写在前面:探索产不出意图(天花板是回归测试);有些边只有「做实验」才走得到; 这次重构改的是表示不是参数,配对评测裁决不了,验收只能靠三个提前记下的基线值。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
「从入口到终点有哪些路径」是图算法,不是模型推断。交给模型等于把一个确定性问题变成会漏、 会编、且不可复现的问题——而这条流水线反复用同一条原则:**能查出来的不要问模型** (出处用字符串定位、代码用正则解析不求值、判据用程序判定)。模型在这一层只做它无可替代 的那件事:给算出来的路径命名,说出它对用户意味着什么。那是意图,图上看不出来。 `computeFlows`:从入口枚举到各终点的路径,每条带着走过去的**动作序列**——那就是用例的步骤, 只记 (from, to) 在下游写不出用例。三条规则都是实测逼出来的: - **原地不动的边不推进流程**,否则同一条流程会出现无数个只差几次空点击的变体 - **深度与条数都有上限**,有环的图路径数是指数级的;截断了要说出来,不静默丢弃 - **同终点收敛**:到同一个地方常有好几条路(绕不绕商品详情页都能进结账)。全列出来, 骨架上就是一堆近似重复,人读不下去;全丢掉又等于说这个产品只有一条路。取最短的为主、 其余记个数——信息不丢,列表能看。真实图上这一条让 9 条变成 4 条 `inPage` 标记:终点若只是同一路由上的状态变化(抽屉打开),标出来并排在换了路由的后面。 打开一个抽屉不是「做完了一件事」。**不过滤掉**——它仍是可达状态、仍要算覆盖。 `computeModules`:按路由前缀确定性聚类。模块划分是路由结构的事实,命名才是判断。 流程归到**终点**所在的模块——流程是以「做完了什么」命名的。 实测(SauceDemo 的 12 状态 / 21 转移图):4 条流程、4 个模块,主线 `登录 → 购物车 → Checkout → /checkout-step-one.html` 排第一。零模型调用。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
此前 `spec.compose` 产出的 20 条规则**全是同一种句式**「X 页显示 Y」——没有一条转移、 一条守卫、一条领域规则。一份只有屏幕层规则的规格是**屏幕清单**,不是规格:由它推出的 用例只能检查「屏幕还是不是原来的样子」。 三处改动,都落在 schema 上而不是提示词上——这条流水线的形状一直是 schema 定的, 把要求写进提示词而不写进 schema,等于没有要求: **① 规则声明海拔**(`screen` / `flow` / `domain`)与它约束的对象(`about`)。提示词点明 每一条「改变了什么」的转移都是一条等着被写下来的 flow 规则,但仍然只写材料给得出的。 **② 流程进规格。** 路径由 `computeFlows` 从状态转移图**算**出来,模型只补 `name` 与 `purpose`——那是图上看不出来的意图。合并时**以算出来的为准**:模型多说的流程一律丢弃并 报一笔。反过来(以模型回复为准)会让它凭空多出或少掉几条流程,而那正是这一层要防的。 没被命名的路径也留着——一条没有名字的流程仍然是一条流程。 **③ 海拔分布报出来**,并且在「全部是屏幕层规则」时明说一句:由它推出的用例只能发现屏幕 变了。这件事从产出上看不出来——规则照样有 id、有原话、有出处,只是全在最低那一层。 测试钉的是最要紧的那条契约:模型自己加的流程被丢掉、没命名的路径仍保留、步骤永远来自图。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
实测产出里出现过 `US-10 页面底部显示社交链接和版权信息`。**没有人想要它**,它只是一条 关于屏幕的事实——由它衍生的用例做的也只能是「屏幕还是不是原来的样子」。而这类条目在扁平 列表里和真正的故事长得一模一样。 `StorySchema` 加四个字段,都进了 guided decoding 的 schema(写进提示词而不写进 schema 等于没有要求): - `role` / `benefit`:谁想要、能得到什么。说不出这两样的条目不是用户故事 - `flowId`:这条故事兑现的是规格里哪一条流程 - `activity`:骨架上的位置 后两个是**故事地图的横轴**。Patton 那套的横轴是用户活动按叙事顺序,而横轴必须来自数据—— 扁平数组画不出地图,界面只能硬凑一个分类,硬凑出来的横轴不是骨架。S5 的界面能不能画, 取决于这两个字段有没有值。 验收标准改成 Given / When / Then。提示词点明:**没有 When 的验收标准是一句描述,不是判据**。 门禁①加两条(都只标记不拒收,与这个门禁其余部分同一个理由——判断一条故事有没有价值 最终要人来看,门禁能做的是把该看的挑出来): - `story-no-actor`:说不出谁想要、能得到什么 - `acceptance-no-trigger`:验收标准里一条触发都没有 第二条的触发词判断改过一次:第一版只看动词,于是「结账页有三个**输入**框」里嵌着的 「输入」被当成了动作——名词里的动词。现在要求动词后面跟着「后 / 时 / 之后」,或出现显式的 when / 当…时 / 若 / 如果。 stats 里报出 `storiesAnchored / stories`:有多少故事挂在流程上。这是故事地图能不能画的 前提,也是「这批故事有多少是真的能力、多少是界面事实」的粗略读数。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
语义覆盖(黄金清单)问的是「测的是不是**该测的东西**」。那个数不可替代,但它永远回答 不了「**够不够**」——因为清单本身有多全,没有人说得清。这正是 M1 度量困境的形状。 状态转移图给出另一个分母:这个产品一共 N 个状态、M 条走得通的转移,你的用例覆盖了几条。 这是模型基测试的标准口径(状态覆盖 / 0-switch / 1-switch,源头是 Chow 1978 的 W-method)。 `scoreStructural` 的几条边界都写进了测试: - **走不通的边不进分母**。它们记在图里是为了说明「这条路走不过去」,拿来当分母会让覆盖率 永远到不了 1,而那个缺口说明不了用例的任何事 - **声称覆盖一条图上没有的转移不算数**。否则用例写一句「我覆盖了下单流程」就能把覆盖率刷满 - **没被覆盖的转移要列出来**——它们是这批用例的洞,而且是查得出来的洞 - 一条转移都说不出的用例单独计数 `TextCase` 加 `covers`,并进了 guided decoding 的 schema——没写进 schema 的字段模型产不 出来,而 `covers` 是结构覆盖率的全部来源。提示词点明:改变不了任何东西的用例没有转移可填, **而那本身值得注意**——它在检查一屏还是不是原来的样子,不是产品还做不做得到某件事。 门禁①报出 `casesCovering`,并对「声称是状态转移用例却说不出走了哪条转移」标一笔。 等价类用例说不出转移是正常的(「登录页显示六个账号」本来就不改变任何东西),不标。 **它不是北极星。** 同一份实证研究的结论之一是代码覆盖与缺陷发现能力**弱相关**,所以它与 语义覆盖、缺陷检出三个数并排出现,谁也不取代谁。这一条写在 structural.ts 的文件头上。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`covers` 把用例挂在产品模型上。复核队列的 regenerate 重建 `TextCase` 时没带这个字段, 于是重写出来的版本丢掉那根线——结构覆盖率上凭空少一条,而产出看起来完全正常。 和 `specText` 那次是同一个形状:一个后加的字段,在某一条支路上没接。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
S2b 的实测是这次重构最直接的证据:同一个站点、同一个模型、同一个 compose 节点, 只是材料里多了一张状态转移图,规格就从「屏幕清单」变成了有转移、有不变量的东西—— **非屏幕层规则 0 → 11**,外加 4 条命名过的流程。 另记两条实施期间撞到的、不属于设计但会咬人的事:`tsx watch` 会跟着 workspace 包重启 (一次运行因此被判 interrupted);后加的字段总有一条支路没接(`covers` 在 regenerate 那条路径上丢了)。两件合起来说明:这条流水线上最贵的不是写代码,是发现某处静默地少做了 一件事。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
加上 `flows`、`altitude`、`about` 之后,同一份材料的规格输出大了一截,3000 会把回复腰斩。 被腰斩的回复解析出来是「JSON 格式错误」,看起来像提示词的问题——截断本身有专门的报错说清 这是预算问题(那套机制在这次真跑里正常开火了),但更好的做法是不要撞上它。 同时把 `scoreStructural` 接进 `/api/evals/score`:探索来的材料带着状态转移图,于是同一次 评分里可以并排给出两个覆盖率——语义的(分母是人写的黄金清单,回答「测的是不是该测的东西」) 与结构的(分母是走出来的转移,回答「够不够」)。文档来的材料没有图,所以它是可选项。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`plan.stories` 的默认预算是 `maxStories × 260`,那是故事只有 id/title/acceptance 时的估算。 S3 加了 `role`、`benefit`、`flowId`、`activity`,验收标准又改成 Given/When/Then 三段—— 一条故事实际在 400–500 token,8 条就把 2080 的预算撑爆,回复被腰斩。 而腰斩的回复解析出来是「JSON 格式错误」,看起来像提示词的问题。截断有专门的报错说清这是 预算问题(这一轮它连着开火两次,spec 一次、stories 一次,都指对了地方),但更好的做法是 不要撞上它。 **加字段就要回头看预算,这是同一件事的两半。** 这一条和「后加的字段总有一条支路没接」 是同一类失误的两个面。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`parseJson` 原来的做法是「第一个 `{` 到最后一个 `}`」,它假设回复里**恰好一个**对象。
换到 TokenHarbor 之后这个假设不成立了:实测收到过 `{…}\n{…}`,切出来那段报
`Unexpected non-whitespace character after JSON` ——读起来像模型返回的格式坏了,
其实是**我们切错了**。前面带一段含花括号的解释,属于同一类。
改成扫描括号平衡的顶层对象,逐个用 schema 试,取第一个合形状的。要认字符串与转义,
否则一段带 `}` 的文案会把计数弄乱——而弄乱之后切出来的东西解析失败,报的却是
「模型返回的 JSON 不合法」,把人引向完全错的方向。
没闭合的那个也留作候选:它多半是被 maxTokens 腰斩的那一个,交给上层报「这是预算问题,
不是格式问题」——那条报错这一轮连着开火两次,都指对了地方,不能被这次改动挡掉。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/v1/models 对 8 个模型全写 modality:"llm",而 qwen3-8-27b 实测能看图 (三次随机纯色图全对,image_tokens=64)。照那个标签筛模型,会把唯一能用的那个划掉。 思考模式关不掉:enable_thinking 与 chat_template_kwargs 都不认。docs/spec/00 里 「必须关思考模式」那条边界在这个端点上做不到——不是不能用,但 max_tokens 要按 「推理 + 答案」给,而且它是付费的,跑长任务前先算花费。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
我在同一个文件里把同一个坑踩了第三次。 `CASES_SCHEMA` 是给模型的约束解码 schema——**没写进去的键,模型一个都产不出来**。 priority 和 postSteps 我加进了提示词、加进了 zod,独独漏了这里。 后果不是「少两个字段」,是 case-priority 那条配对评测**什么都没量到**:A 臂(提示词里 明明写着怎么定优先级)和 B 臂(写着不定)产出的用例优先级全是 undefined,两臂完全 相同,McNemar 检验的是两组一样的数。8 条用例、一次模型调用的钱、七分钟,全白花。 而且——这是最要命的一点——**那份报告看起来完全正常**:门禁 1.0、结构完整、 没有任何错误。是我去数了一下 priority 的分布才发现的。跑完就撞见 0/8。 同一个形状这个文件里已经记过两次(covers 一次、modules 一次),我还在 screens 旁边 亲手写下「提示词里加了字段、schema 里没加,模型会一个都产不出来……两处必须同时改」, 然后在下一个字段上又犯了一遍。 所以不再靠注释。加了三条测试把两份 schema 和 zod 钉在一起: · TextCase 的每个字段,要么在 CASES_SCHEMA 里,要么在「由 harness 填」的豁免名单里, 而豁免的每一条都得说出理由——一个没有理由的豁免就是把这条测试关掉; · 反向也查:schema 允许而 zod 会丢掉的字段是模型白写的 token; · SpecDoc 与 COMPOSE_SCHEMA 同样对齐。 反证过:把 priority 从 schema 里摘掉,测试立刻红并指名道姓;放回去就绿。 那次评测已取消(B 臂跑到一半),结果不入库、不写进文档。修完要重跑。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
第二发。把 priority / postSteps 加进 CASES_SCHEMA 的 properties 之后重跑, 8 条用例**仍然一条都没带 priority**。 诊断:直接拿真提示词 + 真 schema 打一次端点,回来的 JSON 里就是没有那个键。 加进 properties 只是「允许」,不是「要求」——而这个模型对可选键的默认答案是不写。 那为什么同样可选的 oracle 和 covers 产得出来?差别在提示词的语气: 那两个写的是「you MUST also give」「copy that id into … verbatim」,是命令; priority 那一段是描述。靠语气不如靠约束。 所以两件事一起做: · 把 priority / postSteps 放进 `required`; · 提示词里补一句命令式,和 oracle / covers 的写法对齐。 postSteps 空数组明说是「一个答案,不是留白」——只读用例本来就没什么要收拾的。 判据本身也支持这么做:我**要求**每条用例都带一个优先级判断。拒绝选择本身就是一种 选择,而它会把所有用例塞进同一个桶,等于没有优先级。 验证:同一发请求,现在回来 "priority": "P0" 与 "postSteps": [...],finish_reason=stop。 顺带记一个数:这次调用 4031 个输出 token 里 3549 是推理——**88% 花在思考上**。 这个端点关不掉思考,所以 design.cases 这种要产长 JSON 的节点会反复撞截断 (harness 有自动加预算重来的那一层,所以是多花钱不是出错)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
结果写进 evals/case-priority.json 的 `result` 字段,**expect 一个字没动**: 事后成形的预期永远正确,因而毫无价值。 A(问优先级) coverage 0.556 · 8 条 · 60063 token · 571s · P0/P1/P2 = 2/5/1 B(不问) coverage 0.111 · 5 条 · 40013 token · 266s McNemar aOnly=4 bOnly=0 p=0.125 —— 方向 a-better,**不显著** 通过的一条:P0 占比 25%,落在动手前写下的 5%–25% 判据上沿,而且判得对—— 两条 P0 是登录成功这条核心正常流,P2 是「首次进入不显示错误」这种装饰性断言。 推翻的一条:我预判的理由是「这一段不改变测什么,只改变先测什么」。实际 A 在测登录 流程,B 在测登录页渲染。推测的机制是那 9 行里写着「P0 — authentication, payment, checkout, and the core happy path」,它顺带告诉了模型什么重要——所以这一段不是纯粹的 优先级说明,夹带了对「该测什么」的引导,自变量比设计时以为的宽。 还有一个混淆是我修 bug 时自己引进的:把 priority 放进 required 之后,B 臂照样产出 P1/P2(约束解码强制),只是没人告诉它怎么定。所以 B 臂准确的名字是「要这个字段但 不说怎么定」,而不是文件里写的「不问优先级」——label 与 why 描述的是改之前的意图。 这条也写进了 result,不藏。 要真下结论:重复跑 3–5 轮取分布,并把「什么算 P0」的举例换成与被测产品无关的措辞, 看覆盖率差是否消失——那能分辨「优先级本身有用」和「例子在指路」。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
实测:同一份材料(三条用户故事、九条验收标准),`plan.stories` 每次只回**一条故事、 零条验收标准**,role / benefit / requirementId 一并缺席。上一个模型(mimo)在同样的 schema 下产得出来——所以「可选键会被产出」从来不是这套代码的保证,只是那个模型的性质。 换端点把它变成了回归。 后果一路往下,而且没有一处会报错: · 验收标准是设计用例唯一的对照物,没有它,design.cases 只能照着一句标题编; · 故事地图没有横轴、需求追溯没有依据、门禁的 story-no-actor 对每一条都报; · 模型随机挑中哪一条故事,就决定了下游全部覆盖率——四次运行里三次塌到 0.111, 唯一一次 0.556 只是因为它恰好挑中了「登录成功」那条。 **那个 0.556 我一度当成了提示词的效应**,还给它编了个机制(「P0 的例子在给模型指路」)。 它不是效应,是这个 bug 的一个表现。相应地 case-priority 的结论要作废重跑。 修法:acceptance / role / benefit 进 STORIES_SCHEMA 的 required。 role 与 benefit 要的是「回答这个问题」,不是「编一个答案」——提示词里明说材料没写就 留空,空字符串是诚实的回答,缺席不是;门禁的 story-no-actor 负责把留空的挑出来给人看。 验证:同一发请求,1 条故事 0 条验收 → **4 条故事、13 条验收标准、role 与 benefit 全有**。 测试也跟着改了形状:此前只查「字段在不在 properties 里」,现在还查「提示词点名要的 字段在不在 required 里」。这两件事在上一个模型上没区别,在这个模型上是全部区别。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
这三样是提示词点名要的,也是下游三件东西的唯一来源:故事挂流程的那根线、 故事地图的横轴、产品地图上的业务名。此前全是可选的。 这份 mock 材料是文档、没有状态转移图,所以这三样本来就该是空的(聚类是从图上算的), 从它身上看不出问题——但换成探索来的材料就会看出来,而那正是这三样唯一有用的场合。 要求它们不会逼模型编:数组为空同样满足 required。它只是不允许「算出来有五个模块、 模型一个名字都不给」这种情况装作没发生。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
三次配对评测的报告都看起来没问题:门禁 1.0、结构完整、零错误、McNemar 有数。 三次都是跑完之后去数了一下产物的分布才发现白跑。 三次是同一件事的三种形状——约束解码 schema 里可选的键,这个模型直接不写: 字段不在 properties(两臂都产不出来)→ 在 properties 但不在 required(照样不写) → acceptance 可选导致 plan.stories 只回一条故事,模型挑中哪条决定一切覆盖率。 第三条最阴险,它看起来像**方差**。我一度把 0.556 vs 0.111 当成提示词的效应, 还给它编了个机制。它不是效应。 所以 README 里加一节:跑任何配对评测之前,先看一眼这一臂的产物长什么样, 并给了两条现成的命令。判据很简单——一条产出全是同一个值或者全是空的字段, 说明这一臂根本没在回答那个问题,这时两臂比出来的任何数都不属于它声称的自变量。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
A(问优先级) coverage 0.889 · 留出 0.333 · 24 条 · 74.7k token · P0/P1/P2 = 7/12/5 B(不问) coverage 0.889 · 留出 0.333 · 29 条 · 93.1k token · P0/P1/P2 = 0/22/7 McNemar aOnly=1 bOnly=1 p=1 —— 两条分歧一正一反,没有方向 **动手前写的预判说中了**:「这一段不改变测什么,只改变先测什么,所以它不该动覆盖率」。 两臂 0.889 完全相同。上一次那个 0.556 vs 0.111 让我一度以为预判的推理错了,还给它编了 一个机制——那是 plan.stories 的 bug,不是效应;修完之后预判反而立住了。 这 9 行买到的东西写在优先级分布里:**B 臂一个 P0 都没有**,22 条 P1 + 7 条 P2。 不给判定口径,模型永远不说「这条是关键的」——那么「先跑 P0 冒烟」会跑到零条用例。 所以它买到的不是更好的覆盖,是让优先级这个字段有内容。 顺带一个反直觉的:A 反而更省(74.7k vs 93.1k)。B 多产的 5 条用例,钱花在了不区分 轻重的用例上。 如实记一处没达标的:P0 占比 7/24 = 29%,超出动手前写的 5%–25% 上界。不算敷衍, 但带宽是事先写的,超了就得说。**没有动判据**——事后改判据凑结果,比结果不好看糟得多。 result 里注明了它作废 10:15 那次的记录及原因。expect 仍然一个字未改。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
评测结果(eval-mtfqqa50): A(要求清理) coverage 0.889 · 36 条 · 125.7k token · 真清理 7 / 空转 18 / 无 11 B(不要求) coverage 0.778 · 36 条 · 103.0k token · 真清理 0 / 空转 0 / 无 36 McNemar aOnly=1 bOnly=0 p=1 —— 不显著 因果干净:**B 臂 36 条一条清理都没有**。这 5 行是清理步骤存在的唯一原因—— 不是「写得更好」,是「有没有」。 但有效率只有 28%:A 臂 25 条带清理里只有 7 条真在撤销,18 条是「重新打开 /testlogin」, 对一条只读用例来说那什么也没撤销。把 postSteps 放进 required 时我押的注是 「空数组是诚实回答」,押错了一半——模型会用无意义的内容把必填字段填满。 **顺带抓到我自己那条 no-cleanup 规则的漏报:两臂命中都是 0**,包括 B 臂那 36 条全无 清理的。判据要求「填过东西 + 写动作词」,而词表里只有对**数据**的写,没有「登录」。 登录不写数据,但它留下会话——下一条用例面对的是一个已登录的产品,而它自己不知道。 这跟留下一条 John Doe 是同一种毒。 判据的正确说法不是「有没有写动作」,是「跑完之后产品还在不在原来的状态」,会话也算。 加宽词表之后 B 臂从 0 命中变成 8 条,但 A 臂多了 3 条误报——都是「登录后点 Log out」 这种**在自己步骤里就还原了**的,而它们恰恰是唯一真在测清理路径的用例,报它们最伤人。 再加一条:步骤里已经有还原动作的跳过。最终 B 臂 5 条真命中、A 臂 0 条误报。 下一轮(单变量,值得再跑一次):提示词里「read-only cases leave it empty」不够硬, 模型没把「重新打开页面」算成只读。改成「必须撤销这条用例造成的改变;什么也没改就留空; 重新打开页面不是清理」,看那 18 条空转会不会降下来。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
三轮同一个根因:约束解码 schema 里可选的键,这个模型直接不写。第三轮最阴险—— 覆盖率在 0.111 和 0.556 之间跳,看起来像方差,实际是 plan.stories 每次只产一条故事 零条验收,模型挑中哪条决定一切。修完覆盖率跳到 0.889。 两条评测各自暴露了一个我自己的东西:一个是老代码在换端点后的回归, 一个是我上一轮刚加的门禁规则漏掉了「登录」。都记进去了,包括加宽过头又收窄的过程。 没达标的一处也记着:P0 占比 29% 超出事先写的上界,没有事后改判据。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
拿 wf-mtd7gcdk(PetClinic)跑 /api/evals/detection,5 个变异体全部「活下来」, mutationScore: 0,falseAlarmRate: 0。看起来像「这套用例什么都抓不到」。 curl 一比就知道不是: http://localhost:8080/ md5 261de9d6… http://localhost:8080/?defect=no-error md5 261de9d6… ← 逐字节相同 那套 `?defect=` 注入只有内置 fixture(server/src/index.ts 的 /testlogin)实现。 指向任何真实产品,参数被忽略,于是每一轮跑的都是健康版、每条用例都通过、 每个变异体都「活下来」。**这次实验根本没发生,而它报出了一个干净的 0 分。** 这正是 mutate/score.ts 里那个 notApplied 要防的事——但那是工作流里的那条路径, 这条老的 detection 评测没有同样的保护。 判据是确定性的、不问模型:健康版取两遍 + 注入版取一遍。 两遍健康版就不一样 → 页面本身每次都在变,判据用不了,报 unknown(照常跑,宁可多花) 健康 == 注入 → 没注进去,applied=no 健康 != 注入 → 注进去了 先比两遍健康版这一步不能省:少了它,一个带时间戳的页面会被判成「注进去了」, 它的存活会被当成盲区——那正是这个检查要防的错误方向。 注不进去的**不进分母**,也不跑那一轮用例(在健康版上再跑一整轮除了烧钱什么也说明不了), 并单独报 notApplied 条数。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
模块齐全(算子、注入、判决、计分),报告能读、复核界面能显示、缺口能标在故事图上, 可是 `saveMutationReport` **全仓库零调用**。盘上那两份报告(0.6 / 0.5)的生产者已经不在 代码里了——多半是某次一次性脚本,没提交。也就是说这个产品最强的一处能力**不可重跑**, 我在评审报告里引用的那个「变异得分 0.600」是一次性的、没人能验证的数字。 补齐的是管道,不是算法:ExecSpec.opts 收 mutation、ExecOutcome 带回 mutationApplied、 executeCaseDirect 透传,然后一个编排器 + 两个路由。 编排器里三件事都有前车之鉴,六条测试各钉一条: ① **先跑一遍干净的。** 没有基线就分不清「被变异体搞挂的」和「它本来就挂」。 测试里让 c1 在干净跑也挂,它就不能构成 kill。 ② **「没生效」和「活下来」分开,且不进分母。** 注入脚本自己记账 (window.__tpMutation.applied),0 处就是 notApplied。混为一谈会把工具自己的失败 伪装成用例集的盲区,而虚低的那部分看起来像真发现——今天刚在 /api/evals/detection 上撞过一次一模一样的(PetClinic 不认 ?defect=,5 个变异体全「活下来」,报 0 分)。 ③ **变异体从产品模型与规格生成,用例集不参与。** 从用例生成变异体,量的是「用例能不能 抓到它自己」,一个必然为真的循环。生成不出来时如实报错,不给一份 0 分的报告—— 一份 0 分的报告和一句「这次没生成出变异体」在读者那里是两件完全不同的事。 变异注在**浏览器会话**里,被测应用一个字节都不改、容器不重建。这既是黑盒变异测试成立的 前提,也是它能对任何产品用的原因——detection 那条老路径靠被测应用自己实现 ?defect=, 于是指向任何真实产品都会静悄悄地退化成「跑了一整轮健康版」。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
用新入口重跑基线,拿到 killed 0 / survived 7 / notApplied 2 / score 0 / cases 8。 七条「盲区」,看起来像这套用例什么都抓不到。 去 events.db 里复原干净跑那一轮:**8 条挂了 4 条**(其中 2 条是 infra,模型或浏览器故障, 连产品都没碰到)。那 4 条进了基线失败集,永远不可能构成 kill——一条本来就挂的用例 再挂一次,说明不了任何关于变异体的事。真正有机会叫的只有 4 条。 所以那个 0 的真实含义是「一半的用例集根本没上场」,而报告只写了一个 0。 跟今天早些时候 /api/evals/detection 那个 0 分是同一种病:一个干净的数字盖住了 「这次实验有一半是坏的」。我刚建的入口自己也犯了。 报告加三个字段:baselineFailed / baselineInfra / usableCases。 并且干净跑全挂时**直接报错**,不出报告——那时后面每个变异体必然「活下来」, 不是因为用例集有盲区,是因为没有一条用例上得了场,每一条「盲区」都是假的。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
这条界线本项目早有定论:infra 可重试,判决不可。但在变异测试的基线里,它的代价 比别处大一个量级——基线挂掉的用例会进 baselineFailures,此后**每一个变异体那一轮 都不再算它**。一次模型抖动不是让这条用例这次不算,是让它整场都不算。 实测:8 条基线挂了 4 条,其中 2 条是 infra——一次抖动就把 25% 的用例集移出了场, 而报告只显示「score 0,七条盲区」。 只重试 infra,只重试一次:判决失败重试一次多半还是同样的判决,重试它只是把同一个 问题再问一遍,还掩盖了它。重试次数写进报告——基线抖得越多,这份报告越不可信, 那是读者该知道的事。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
`startRun` 里写着「Re-running one node of an existing run must use that run's target,
not today's default」,而接口层一行代码把它废掉了:
target: body.target ?? { projectId: body.projectId, envRef: body.envRef, url: body.url }
那个兜底对象**永远为真**,哪怕三个字段全是 undefined。于是
`input.target ?? previous.target` 永远走第一支,拿到一个空对象。
实测(就在修它之前,为了验证截图落库而重跑 wf-mtd7gcdk 的一个节点):
目标被静默擦成 `{}` → 三条用例在 12 毫秒内以 infra 失败 → record() 因为没有 projectId
直接返回 → 一条执行记录都没写 → 而运行状态是 `done`、事件里 wf.run.finished status=done。
**一次什么都没跑的运行,报告说它成功了。**
存量数据也被改坏:那次运行原来的 target 没了。已从同项目的兄弟运行里抄回来
(不手打——手打等于又编一次),mode 与 paramOverrides 一并还原。改之前按红线用
VACUUM INTO 备了 workflows / graphs / testpilot 三个库到 .data/backup/。
修法:请求真的说了才拼 target,什么都没说就不传,让 startRun 沿用原来的。
显式传一个空 target 仍然照收——那是「我就是要清空」,和「没说」不是一回事。
四条测试钉住这个区分,因为它太容易在重构里被写回原样。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
第 4 项完成,而且多验出一件事:logs 是跟截图一起接通的,失败详情从「一行 EXEC_ASSERT」 变成了完整执行轨迹。第一次尝试还撞出接口层擦掉 target 的 bug(一次什么都没跑的运行 报告说它成功了),已修并把被擦掉的存量数据抄回来。 第 3 项:入口建好了——在此之前变异那条路径是只读的,那个 0.600 不可重跑。 对齐参数跑出来 8 个 inconclusive、4 个 notApplied、0 个定级,因为 161 次执行里 80 次是 infra(50%)。这一轮真正的价值是它没撒谎:没有 inconclusive 这一档的话, 报告会写成「score 0.000,发现 8 处盲区」,八条全是编的。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
评审报告 F-07 剩下的两条:「三层目录还没真正生成」「复用 1 个片段覆盖 3 条」。 产品自己在代码线上也挂着这句 TODO。 导出现在按 cases / actions / flows 三层生成:至少两条用例共用的前置进 flows, 重复的单步进 actions,其余留在用例里。拿真实的 38 条批次量: 步骤共 80 步 → flows 1 个覆盖 4 步、actions 7 个覆盖 19 步 复用率 29%(评审报告里是 3/38 ≈ 8%) 复用关系是在导出时**重新推**的:批准进看板那一步把共享前置展开了(看板没有片段这个 概念,一条用例要能独立跑起来),所以到导出时手上只剩扁平步骤。 三条判据各有理由,八条测试各钉一条: · **只在真的复用时抽**(门槛两条用例)。一个只有一个调用方的公共函数比内联更糟: 读的人多跳一次,却什么也没省。 · **名字由内容决定,不由模型决定。** 导出必须可重复——同一批用例导两次要得到逐字节 相同的工程,否则它进不了版本库,每次导出都是一个巨大的假 diff。模型起名做不到。 · **抽取只改组织,不改语义。** `expandPlan` 存在的唯一理由就是让这条能被逐字验证: 任何一条用例展开之后必须和原步骤序列完全相同。这一层要是能改变用例做什么, 它就不是「组织方式」,是一次没人复核过的重写。 只认前缀不认任意子序列:复用在这条流水线上几乎总是「到达某一屏要走的那几步」, 而任意子序列会把两条不相干的用例中间偶然相同的两步也抽成「流程」——那种抽取 读起来毫无道理,也没人敢改它。 顺带量到一件事:**复用率被措辞一致性卡住**。同一个目标写成两种话就收敛不成一个动作 (`click the "FIND OWNERS" link in the navigation bar` 与 `click the FIND OWNERS link in the top navigation bar`)。这正是今天加的 wording-drift 规则在管的事——两条发现是 同一个根。代码线上那句 TODO 文案也照实改了。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011b79SCHX1RFJWaoKvjqo1E
在这之前,测试数据只有两个去处,两个都不对:写死在步骤里(换个环境跑不了,
跑第二遍撞唯一约束,而且它进了版本库),或者塞进环境变量的数组里(能跑,
但没有名字、没有列、没有界面,只能手改 JSON,还和「这个环境的配置」混在
同一口袋里)。
给它一个一等的位置。四条设计取自 E2E 测试数据的通行做法,每一条对着一个
这个仓库真吃过的亏:
① 先预览再落库。一份列名解析错的数据集,症状不是报错,是二十分钟后一批
「断言没通过」——而错的是数据不是产品。所以预览是必经的一步。
CSV 解析认引号:按逗号硬切会把「Springfield, IL」切成两列,且不报错。
② 像凭证的列当场警告,但不拦。数据集会跟着导出的工程进版本库,凭证不能;
而判断一列是不是凭证最终要人来看——一个会自作主张拒收的导入,遇到一个
叫 password_hint 的合法列就把人挡死了。
③ 会落库的列能标唯一,运行时加一个这次运行独有的短后缀(-r7k2)。
「跑第二遍撞已存在」是 E2E 最常复发的一种失败,而它每次看起来都像产品坏了。
这不是替代清理,是它的另一半:清理管「跑完还原」,唯一管「清理失败时
也别互相踩」。邮箱插在 @ 前面,否则域名被破坏、产品当格式错误拒收。
④ 引用的列必须校验。原来用例抽屉里 dataKey 是个自由输入框:名字打错、列名
打错都没有任何提示,而后果是 ${row.emial} 原样进了表单——一个看起来像
占位符的字符串被当成字面量输进去,三层里没有一层会喊。改成从已有数据集里
选,并把 checkBinding 的结果接到界面上:缺的列标红,没用到的列只说一声。
导出这一半:数据随工程走(tests/data/<name>.json),不是运行时去平台上取——
导出的工程要能离开平台自己跑,一份留在服务器上的数据集会让它在别人的 CI 上
第一天就挂。一行一个 test,不是一个 test 里跑循环:差别在失败的时候,循环
只说「这条用例挂了」,一行一个 test 直接说「第 2 行挂了」。
最后修掉自己犯的一个:第一版是拿正则去改写**已经生成好的代码**,把
${row.x} 换成 ${r.x},可 lit() 早就把步骤包进了 JSON 双引号,导出的是
aiAction("在 firstName 填入 ${r.firstName}")——双引号不插值。改成在渲染
每一步时就决定形态,用字符串拼接(模板字符串要转义反引号和 ${,而步骤原文
里这两样都可能有)。加了会变红的测试钉住它。
包含:docs/spec 与 refactor 的归档移动、harness-core 评测/配置改动、apps/agent 与 runner、README 三语更新。 这是一次「先把现状钉住」的提交,不代表每一处改动都已验收;下一步在新分支上按第四档六项逐项优化。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013WvGpm9RQRpu9ooeMNHiMe
和上一条 wip 一样,这是一次「先把现状钉住」的提交,不是逐项验收。446 个新文件、 152 个修改、39 个移动;跑动证据(docs/v3/evidence,105MB,6 个录屏就占 73MB) 加进了 .gitignore 留在本地——进了 git 历史就只能靠 filter-repo + force push 拿掉, 而结论、数字和引用都已经写进 docs/v3/*.md。 这段时间里撞到的、值得单独记住的几件(详见 docs/v3/24 的 §37–§40): **判决在屏幕上,但屏幕得先画完。** 执行日志里一句 `page ready after 2117ms (16 controls, 309 text characters)`——一个交易页不可能只有 309 个字符。照着真页面量:骨架期 DOM 连续两轮一模一样,而 `Order Book` 要到 5309ms 才出现。`settleOn` 的「两轮没长就算稳」正好被骗过去,随后视觉模型对着骨架说 「页面上不存在 Order Book 标签」——一条记在产品头上的失败。分得开这两件事的是 资源计数:骨架期每 500ms 涨四到六条,画完之后掉到 0–1 条。 **一次限流报废一整批。** 第 2 条用例撞上共享端点的 429 (`hosted_shared_model_concurrency_limit`,16 个并发位被别的租户占满), `if (result.infraError) break` 当场停批,剩下 79 条记成 not_run。而 `classifyFailure` 早就标了 `retryable: true`,从来没人读它。补重试的时候发现补错了层: 规划侧的 openai.ts 一直在退避重试 429,执行侧走的是 role-proxy,而它没有。 **预算按「一次运行」发,工作却被拆成了 N 份。** 81 条用例拿着一份 10 分钟 / 100 次调用的预算,而一条就跑了 186 秒。和单元循环那次(§31)是同一条。 第一版我把每条的份额写成 150 秒,注释里还说那是「实测的两倍」——实测是 186 秒。 **用例为什么那么短:acRefs 是改写,不是引用。** 85 条 acRefs 里 21 条是模型自己 编的一句话,16 条连 When 都换了,方向高度一致:「When 用户勾选 Reduce Only 并提交」 被改成「When 用户查看面板」。故事里那个有业务含量的动作被换成了「看一眼」, 之后整条链闭合:用例不需要动作(81 条里 24 条只有导航和看)→ 判据退化成 「页面上有这个字面量」→ 那种判据在初始页面上就成立 → 通过时什么都没证明。 给验收准则编号、acRefs 只能填编号之后,三轮对照:自己编的 24% → 0%, 动作型准则被真做过 55% → 98%,平均步数 3.1 → 3.7。 **动作动词表是拿真语料调出来的,不是想出来的。** 第一版只认「点击」,而模型写的是 「用户点「Limit」」——12 条故事被误报,而契约正拿这份错标注在教模型写用例, 只好当场停掉重调。拿三轮运行的 258 条去重验收准则当靶子,调了三遍才准。 **一份生成出来的测试工程里有两种文件,混在一起它就没法用。** 导出此前每次全量覆盖, 用户在 playwright.config.ts 里加的东西下一次导出就没了,还不会收到任何提示。 划出「工具拥有 / 人拥有」这条线,判据只有一个函数,文件头标注、清单的 ownership、 README 三处都从它来。顺带修掉:路径里写着会变的优先级;242 条用例全塌在 tests/_ (分组只看 activity,而它们 242 条里 0 条有 activity、242 条有 storyId); 每条用例导航两次,第二次还交给视觉模型跑。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 调研宿主接入时发现:`plugins/testpilot-claude` 与 `-codex` 停在 09-12, 落后四个文件——`.claude-plugin/testpilot.json`、`testpilot-design/REFERENCE.md`、 `REFERENCE-domain-perp.md`,以及当天刚改过的 `testpilot-stories/SKILL.md`。 也就是说「长故事覆盖短故事」那条新规则,宿主 agent 整整两天没收到过。 检查本来是有的。`build-claude-plugin.mjs --check` / `build-codex-plugin.mjs --check` 一直在 `pnpm check:drift` 那个三步串里。问题出在 CLAUDE.md 的红线写的是 `node scripts/check-drift.mjs`——**只有第一步**。照红线跑,它一直是绿的, 后两步从没跑过。一个有两种拼法、而其中一种会漏的检查,等于没有这个检查。 所以修的不是那行字,是入口:三步收进 `check-drift.mjs` 一个入口, `package.json` 的 `check:drift` 退回成单条命令,两种拼法从此等价。 `--write` 也顺带重新生成宿主副本——改完 skill 跳版本的那一下,本来就该把副本一起带上。 验过它真能抓到:故意在 `testpilot-claude/skills/testpilot-stories/SKILL.md` 末尾加一行, 窄跑法当场报「与真源不一致(1 处)」并列出文件名;改回去就绿。 顺带把两个宿主副本重新生成到与真源一致。CLAUDE.md 那条红线也改了, 并在旁边写清楚为什么值得记着这件事。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
九个节点里只有探索是「中途挂 = 全丢」的,而它同时是最长的一个:这次的材料 187,669 字,跑满 20 屏要几十分钟加一次钱包会话。整份结果只在函数返回时才组装、 才交给服务端落账本,所以第 18 屏上崩掉,前 17 屏花掉的十几次模型调用全部作废, resume 从第 1 屏重来。2026-09-12 还因为收尾时 runner 被心跳看门狗 SIGKILL, 一晚上丢过四次完整探索——那次探索其实全跑完了。 **探索器每采到一屏落一次半成品**(`snapshotPartial`),只落材料不落状态图: `graph` 要循环跑完才建得出来,而材料就是钱花在的地方。写法是先写临时文件再 rename, 崩在写一半上会留下半个 JSON,那比没有更糟。跑完了就把半成品删掉——留着它, 下一次失败会捡到上一次的材料。 **服务端在失败路径上把它捡回来**:有材料就带着已采到的屏继续走,没有才如实抛原来的错, 并落一条 `report/exploration-partial` 说清楚停在第几屏、为什么停。 材料里也写着「这份只到第 N 屏」——下游读到的是一份 17 屏的材料,不是 20 屏的。 这**不是**断点续跑:不会从第 18 屏接着探(那要动 `interactive.ts` 的状态机, 而且探索有随机性,「接着走」的语义得先定义清楚)。它保证的只是已经花掉的钱不白花。 落点用同一个函数算(`partialObservationPath`),两侧共用。这是个典型的会悄悄错开的 接缝:两处各写一遍模板字符串,哪天改了目录名,写的一侧和读的一侧都不会报错—— 只是永远捡不到东西,而「捡不到」和「本来就没有」长得一模一样。 顺带修 `workflowCheckpoint` 漏掉的两个节点。原来的清单是 `["instructions","stories","cases","gate","finalize"]`,`source` 与 `modules` 不在里面: 一次在 modules 上失败的运行,`next` 会指向 `stories`——resume 把模块节点整个跳过去, 而下游所有单元都按模块树切。 但这两个是**有条件的**:宿主注册的运行材料在注册时就交了,根本没有 source 节点。 第一版我写成「没 done 就回到它」,`run-routes` 那条测试当场红了——它会把每个宿主运行 送回一个它从来没有过的节点。改成「走过、而且没走完才回到它」,必经节点仍按原判据。 测试 +7(431)。`pnpm check:drift` 在这条分支上是红的,红的是 PR #1 要修的那 4 处 宿主 plugin 副本——这条分支基于 opt/tier4,还带着过期的副本,不在这里顺手修。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude Code 作为宿主此前只有六条接缝单测,没有一条会真的 spawn 一个 `claude` 进程—— 「参数拼对了」「hook 真的被加载了」「skills 真的被识别了」三件事只有文档没有证据。 Codex 在 09-10 有过一次实测(19),两个宿主的证据强度长期不对等。 `verify-host-stages.ts --real --claude` 这个脚本一直存在,只是没人跑过。第一次跑, 脚本判 `passed: false`「agent 中途停下了」。看 trace 才知道它停在哪: begin_stage(modules) → load_run_instructions → retrieve_spec → plan_modules → module_plan_state ×3 → 停 session 本身是**正常结束**的(`is_error: false, subtype: success`)。它在等人冻结模块树, 而脚本里没有这个人。**不是适配器坏了,是这个脚本比流水线旧**——它写在模块节点与那道 人工闸引入之前。 照 `drive-modules-and-stories.ts` 的先例加 `--freeze-as-operator`:默认不按, 带这个开关才按,并把「谁按的、什么时候按的」写进证据。这条闸的意义是「有人为这棵树 负责」,不是「必须有个人坐在那儿」——脚本调用者显式声明自己就是那个人,是成立的; 悄悄替他按,不成立。 带上它重跑,全链通了:17 次 MCP 阶段调用(内部 run_pipeline 0 次)、3 分 10 秒、$1.52、 11 份修订,最终 `finalize`。实测记录写进 docs/v3/25。 顺带验到两件: - schema 拒收之后 agent 会自己修。第一次 write_cases 被 `transitionIds` 打回, 改完重写就过了;门禁出结果后又写一次再 gate,修复回路按设计走通。 - **2026-09-14 新加的门禁规则在英文材料上也生效。** `acceptance-uncovered` 点到 「S-01/AC-1 要求用户动手(When the visitor clicks the visible Inc)」——验收准则编号 与「要不要用户动手」的判定当时是对着中文语料调的,这次在纯英文规格上, `When` 子句解析与英文动作动词都命中了。 还没验到的三件写在文档里:hook 有没有真被执行、执行节点没走、用的是本地 counter 规格不是重前端页面。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上一轮用的是脚本自带的本地 counter 规格。这一轮换成真东西:新建项目、目标 `app.hyperliquid-testnet.xyz/trade`,走完探索 → 宿主规划 → 人复核 → g2。 分工是被迫的也是对的:MCP 里没有探索工具(`register_run` 收的是材料文本), 而且探索要浏览器、宿主 agent 没有。所以服务端探索(modules 前设断点停住), 材料交给宿主规划,人复核,再编译执行。驱动脚本 `drive-claude-host.ts`。 6 屏 / 32,656 字材料 → 9 模块 / 4 故事 / 11 用例,门禁 score 1;复核 10 批准 1 驳回; g2 ready_to_execute,代码门禁 score 1。 用例质量与 24 §38 之前那 81 条完全不同:11 条全是 tier1 + 机器判据 + ready, acRefs 全部按编号引用,步骤是真实动作,大多带回退,没有一条是「打开页面 + 看一眼」。 **驳回的那条,缺陷不在用例。** C-12 的判据是 `Account value must be <$2500万 to use portfolio margin in beta mode.`—— 一个英文 dapp 不会渲染「万」。查探索材料,原文就是这句:用例逐字抄的,没编。 缺陷在上游,材料里这句的数字被本地化了($25M → $2500万),而同一份材料里 `No open positions yet`、`Trailing Stop` 都是逐字英文。这是 outputLanguage: 'zh' 的运行都可能中招的一类。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
全流程最后一段跑完了:探索 → 宿主规划 → 人复核 → g2 → execution, 10 条批准的用例真跑到 app.hyperliquid-testnet.xyz/trade 上,**零基础设施错误**。 唯一一条失败是定位失败不是产品缺陷:C-10 第 2 步找不到账户面板里的「Positions」标签, 而页面就绪读数是 30 controls / 1849 text characters——页面是画完的,未连接钱包时 那一行标签本来就可能不在。归 locate 而不是 assert,分档是对的。 9 条通过里只有 4 条是挣来的,另 5 条的判据在动作之前就已经成立 (`heldBefore`)。判据是真的机器判据、通过也是真的通过,但那 5 条这次没有证明 它们那几步做成了什么。复核时我已经标出 C-01 与 C-11 两条,执行把另外三条也找出来了。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(drift): 漂移检查漏掉了两个宿主 plugin
fix(explore): 探索中途挂掉,已采到的屏不再全丢
CI 从来没绿过。三条 PR 分支各红了一次,红的都是同一条,而且和各自改的东西
毫无关系:
Error: penguin_node_24_required: set TP_PENGUIN_NODE to a Node 24+ executable
❯ src/penguin.ts:144 ❯ test/managed-penguin.test.ts:22
`managed-penguin.test.ts` 的四条都是 `it.runIf(installed)`——没装就跳过,机制是齐的。
但 `installed` 这一行在模块顶层调用 `node24Path()`,而它在找不到 Node 24 时**抛异常**。
于是在一台没有 Node 24 的机器上,整个文件加载失败,四条连「跳过」都到不了,
整个 server 包的测试红。
CI 用的是 Node 22。本机也是 22.17.0,只是 PATH 上另有一个 Node 24,
`node24Path()` 的 `execFileSync("node", …)` 兜底找得到它——所以本地一直是绿的,
CI 一直是红的,而两边跑的是同一条命令。
探测的答案本来就该是「没有」,不是「出错」。包一层 try/catch。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test(claude-code): 宿主端到端第一次真跑通
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
把
opt/tier4上 2026-09-07 以来的全部工作合入main,包含已合并的 #1 #2 #3。这条分支上都有什么
一次 WIP 式整体落盘(
e0e1dfc,637 文件)加三个针对性修复。跑动证据(
docs/v3/evidence/,105MB,其中 6 个录屏占 73MB)在.gitignore里留在本地——进了 git 历史只能靠 filter-repo + force push 拿掉,而结论、数字和引用都写在
docs/v3/*.md。三个已合并的 PR
fix(drift)— 漂移检查漏掉了两个宿主 plugin,而 CLAUDE.md 的红线正好指着漏的那一半。三步收进一个入口,两种拼法从此等价。fix(explore)— 探索是九个节点里唯一「中途挂 = 全丢」的,而它最长。每采一屏落一次半成品,失败时捡回来带着已采到的屏继续走。顺带补workflowCheckpoint漏掉的source/modules。test(claude-code)— 宿主端到端第一次真跑通,两轮:本地 counter 规格,以及真实的app.hyperliquid-testnet.xyz/trade(探索 → 宿主规划 → 人复核 → g2 → 执行,10 条真跑测试网,9 过 1 败,零基础设施错误)。顺带修好的 CI
CI 从来没绿过。三条 PR 分支各红一次,红的都是同一条、且与各自改动无关:
managed-penguin.test.ts顶层调用node24Path(),它在找不到 Node 24 时抛异常,整个文件加载失败——四条
it.runIf(installed)连「跳过」都到不了。CI 用 Node 22。探测的答案本来就该是「没有」不是「出错」。修完 CI 第一次绿。
检查
pnpm typecheck/pnpm test(431)/pnpm check:drift本地全绿;CI 在 #3 上 success。e0e1dfc是一次整体落盘,不代表每一处都已逐项验收,中间没有可 bisect 的粒度。tip 是绿的。
🤖 Generated with Claude Code