oh-my-openagent 的 Prompt 契约测试移除工程:AST 扫描器、fail-first 验证与 seam 保留策略
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
本篇技术指南围绕 oh-my-openagent 仓库中一次完整的分支级工程实践展开:系统性移除对作者创作的 prompt、指令、SKILL.md、规则、AGENTS.md与 Markdown 指令文本的自动化断言(即 "prompt 契约测试"),同时用自研 AST 扫描器 + SHA-256 指纹分类索引建立"fail-first(先在基线失败、再在最终树上变绿)"的可复现证据链,并明确保留 6 类机器消费的测试 seam。读完本文,你将掌握这套扫描器管线的原理与命令、证据文件的组织方式,以及如何在大体量 Agent 项目中安全地裁剪"纯文本契约类"测试而不损失运行时、安全与用户可见行为覆盖率。
背景:为什么 prompt 契约测试必须被移除
在 oh-my-openagent 这类深度依赖 prompt 编排的 Agent 工程中,测试代码很容易出现一种"过度契约化"倾向:直接对 agent 系统提示词、SKILL.md正文、规则文件、AGENTS.md的措辞、标题、章节顺序、文本片段、快照乃至作者创作文本的长度做断言。这类断言的共同问题是:纯 prose 没有稳定的自动化测试 seam——它描述的是"写给人(或模型)读的指导文字",而不是"机器消费的结构化值"。任何措辞润色都会让测试无意义地红掉,而真正的行为回归却无法被这类测试捕获。
该分支的最终结论(见 FINAL-REPORT.md)非常明确:移除对 authored prompt、directive、SKILL.md、rule、AGENTS.md与 markdown-instruction 文本的自动化契约,同时完整保留:
- 机器消费的值(machine-consumed values);
- 真实工件之间的字节级/副本级相等性(real artifact equality);
- 解析、路由、分发、状态、安全、运行时投递行为;
- 用户可见的 UI 与错误行为。
这一边界随后被固化进仓库策略,写入了 AGENTS.md 与 tests/AGENTS.md(完整 diff 见 agents-policy-diff.txt):
Prompt/prose contract tests are forbidden. Never assert authored agent prompt,
SKILL.md, rule,AGENTS.md, or markdown-instruction wording, headings, section order, fragments, snapshots, negative past wording, or authored text length. Test only machine-consumed fields/sentinels/tool names, byte or shipped-copy equality between real artifacts, or observable runtime behavior such as parsing, routing, dispatch, state, security, and dynamic input propagation. Pure prose has no automated-test seam; review and QA-by-read are the correct verification.
扫描器管线:从 git 跟踪文件到分类索引
移除了哪些、保留了哪些,不能靠人工判断,必须由确定性的 AST 扫描器给出全量清单。整套工具位于 .omo/evidence/20260817-remove-prompt-contract-tests/ 目录,入口说明见 README.md。
1. 枚举:直接来自git ls-files,无 allowlist
audit_prompt_contracts.py用正则(?:^|/).+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$过滤git ls-files -z的输出(audit_prompt_contracts.py),枚举仓库内所有被跟踪的JavaScript/TypeScript 测试与 spec 文件。这里有两个关键设计:
- 没有任何路径 manifest 或 allowlist 控制发现范围,凡是
git跟踪的测试文件都在扫描范围内; - 若某个被跟踪的路径在工作树中被删除,它不会被静默忽略,而是单独汇报为
tracked-missing,确保"删文件"本身也成为证据的一部分。
2. 解析:TypeScript 编译器 API
扫描器把文件清单写入临时 JSON,再调用node prompt_contract_ast.mjs(prompt_contract_ast.mjs)。该脚本使用仓库内安装的 TypeScript 6.0.3 编译器 API 解析每个测试文件,输出{ parser: "typescript-6.0.3", candidates }结构。扫描覆盖的断言模式非常广(详见 prompt_contract_scan.mjs、prompt_contract_values.mjs、prompt_contract_derived.mjs、prompt_contract_node_assert.mjs):
- 断言字面量穿过变量引用、数组、
for...of绑定、布尔.includes()表达式、matcher 链被持续追踪; - 顺序助手调用(order-helper)、派生标题/token 数组、
indexOf顺序断言; startsWith别名、正则派生的展示类检查(presentation checks);- Node 原生
assert的各种变体、可调用断言(callable assertions)以及快照断言(snapshot assertions)。
也就是说,即便测试作者把断言的期望值藏在变量、数组或派生函数里,扫描器也能还原出"实际断言了哪段文本"。
3. 指纹:不含行号的 SHA-256
每个 candidate 按path + candidate kind + matcher + actual expression + expected value计算 SHA-256 指纹,并与 prompt-contract-classification-index.json(或补充分类 prompt-contract-classification-supplemental.json)中的 disposition 做联接。指纹刻意不含行号,因此代码行移动不会破坏指纹稳定性;同时该索引记录了全部 scanner 脚本的哈希(audit_prompt_contracts.py、classification_bundle.py、prompt_contract_ast.mjs 等 15 个文件的 SHA-256),保证"用的就是这份扫描器"这一事实本身可被校验。
4. 判定:allowed / unclassified / forbidden / stale
分类提案(classification-proposals/core.json 及其余 codex/infra/opencode/senpi-shared/scripts-tests 提案)中,每条 allowed 或 forbidden 记录必须同时满足两个条件:
- 属于扫描器定义的显式 seam 类别(machine-sentinel、parser-fixture、runtime-behavior、security-boundary、shipped-copy-equality、user-ui-error);
- 带有非空的 rationale(例如"AGENTS.md 是机器发现的指令文件名常量"、"用 fixture body 证明 root 上下文排除逻辑")。
audit_prompt_contracts.py的退出码逻辑(audit_prompt_contracts.py)为:
return int( bool( payload.unclassified_count or payload.forbidden_count or payload.stale_classification_count ) )即:只要存在未分类、被禁止或**过期(stale)**的分类条目,命令就以非零退出——这是整个 fail-first 门禁的落点。
Fail-first 证明:先红后绿的可复现证据
"移除测试"最大的风险是偷偷放水:用缩小范围或一刀切放行来把红变绿。该分支的做法相反,用同一份最终扫描器+分类索引,分别在两个时间点跑出相反的退出码,把过程性证据全部落盘:
| 维度 | 原始基线(RED) | 最终 rebase 树(GREEN) |
|---|---|---|
| 基线/状态 | commit3dd88267f87bd47795d3eea7782e676bb40e2f9b | rebase 到当前 dev 的最终工作树 |
| 退出码 | 1 | 0 |
| 跟踪测试数 | 2,291 | 2,297 |
| 候选断言数 | 2,421 | 1,910 |
| 已分类 allowed | 1,674 | 1,910 |
| 未分类 | 747 | 0 |
| forbidden | 0 | 0 |
| 过期分类 | 94 | 0 |
| 缺失跟踪文件 | 0 | 0 |
证据文件逐一对应:
- 原始基线完整违规清单:red-prompt-contract-scan-post-quality.txt(含 747 条 unclassified 明细);
- 编辑后、rebased 前摘要:green-prompt-contract-scan-post-quality.txt;
- 最终 rebase 后摘要:green-prompt-contract-scan-rebased.txt。
三者使用完全相同的命令:
python3 .omo/evidence/20260817-remove-prompt-contract-tests/audit_prompt_contracts.py \ --classification .omo/evidence/20260817-remove-prompt-contract-tests/prompt-contract-classification-index.json \ --compact--compact模式下,摘要以 JSON 输出,随后逐行打印每个候选的位置、分类与指纹;tracked-missing与stale也各自独立成行,杜绝"删路径来消灭违规"的作弊路径。
保留的 seam:六类仍可断言的测试边界
移除的不是"所有文本断言",而是"对作者 prose 的契约"。保留边界被汇总在 preserved-prompt-test-seams.json 中(该文件本身由 7 份分类提案汇总而来,绑定 1,476 个活跃指纹):
| seam 类别 | 含义 | 出现次数 | 唯一指纹 |
|---|---|---|---|
machine-sentinel | 机器消费的哨兵值/字段名/工具名(如AGENTS.md常量、task_create、$SESSION_ID) | 718 | 594 |
parser-fixture | 解析器 fixture 往返(JSON/JSONC/Markdown 配置加载后值不变) | 213 | 187 |
runtime-behavior | 可观察的运行时行为(注入、路由、去重、重试、状态机) | 473 | 416 |
security-boundary | 安全边界(URL 校验、密钥脱敏、指令注入拦截) | 67 | 51 |
shipped-copy-equality | 真实工件之间的字节/副本相等(源码 vs 打包产物) | 121 | 109 |
user-ui-error | 用户可见的 UI 与错误消息 | 135 | 119 |
一个典型的分界示例:packages/memory-core/src/compile/cache.test.ts中toBe("second")、toContain("- AGENT_ID: other-agent")等属于机器行为断言被保留;而packages/memory-core/src/reflection/assets/assets.test.ts中断言Phase 1: Investigate等作者创作文本顺序的候选,则被移除或重构为运行时行为断言。分类提案中的每条 rationale 都解释了"为什么这条断言不落在 prose 上"。
自动化验证矩阵:五条独立红线
测试裁剪之外,整个仓库必须仍处于绿色状态。FINAL-REPORT 记录的自动化验证结果如下:
bun run typecheck:根目录、scripts 与每个 package 全部 exit 0;bun test:15,613 passed、0 failed,7 个既有平台/TUI skip,131,764 个断言,2,048 个文件;bun run build:exit 0;bun run test:codex:exit 0,最终 Node 段492/492 passed;- 扫描器回归套件:在声明的 uv 环境下15 项全部通过;
- 独立评审阻塞项解决记录:review-fixes.txt;
- rebase 后命令与结果台账:post-rebase-validation.txt;
- 聚焦域汇总:focused-domain-verification.txt;
- LSP 调用与干净的 changed source/helper 结果:lsp-diagnostics.txt;
- 扫描器自身的严格检查:pytest、Ruff、basedpyright strict、no-excuse 检查、Node 语法检查、单文件 ≤250 行纯逻辑(LOC)上限,全部通过。
扫描器回归套件与审计入口的复现命令(来自 README.md):
uv run --script .omo/evidence/20260817-remove-prompt-contract-tests/test_audit_prompt_contracts.py -v uv run --script .omo/evidence/20260817-remove-prompt-contract-tests/test_node_assert_scanner.py -q uv run --script .omo/evidence/20260817-remove-prompt-contract-tests/test_snapshot_scanner.py -q真实表面 QA:四个宿主环境的现场验证
自动化测试不能证明"真实插件还能跑"。该分支对四个真实宿主环境做了现场 QA(汇总见 manual-qa-summary.txt):
1. OpenCode 真实本地插件(opencode-serve-wake-rebased/ 目录)
RESULT=FIXED;- 一次终端停止(one terminal stop)、一个子任务会话(one child task session);
- live-route 分发(live-route dispatch)正常;
- 真实数据库计数不变(精确计数按卫生规则脱敏)。
2. Codex 真实 app-server
- 隔离的本地插件安装;
- mock-model turn 完整走完;
hook/completed事件序列sessionStart、userPromptSubmit、stop均被观测到;- 真实 Codex 配置哈希保持不变。
3. Codex 隔离安装器
- 插件缓存为
5.0.0-beta.8; omo@sisyphuslabs已启用;- 10 个组件 bin 与 agent TOML 完成链接;
- 真实 Codex 配置哈希同样不变。
4. Senpi 本地扩展(经 xterm.js)与 CLI
Senpi 终端证据位于 senpi-web-terminal/ 目录:截图、文本、ANSI 与 metadata 齐备,tipsJSON 合法,help命令 exit 0、非法选项 exit 1——用户可见 CLI 行为未受裁剪影响。下图为该次 xterm.js 终端现场运行结果:
Senpi post-rebase CLI 断言:result=PASS、ultraworkInjected=true、commentChecker=PASS、realSenpiUntouched=true。
清理与范围控制:不留残余
裁剪不是"删了就跑",还包括对称的收尾:
- 移除了有引用佐证的孤儿导出(orphaned exports)、测试助手、一个死模块(dead module)与过期的 scoped 文档;死代码引用证据见 dead-code-reference-audit.txt;
- 生成的 CodeGraph/Senpi 产物最终 diff 为零;
- 临时 detached 扫描器 worktree、XDG/CODEX/Senpi 隔离 home、fake server、PTY 以及 LSP bridge 全部被清理;
- 脏的共享主 checkout 中不留任何任务编辑残留。
证据卫生:可复核但不可泄露
所有证据制品遵循同一契约(见 README.md):记录精确命令、观测输出、二进制 PASS/FAIL 条件与清理回执。原始密钥、环境 dump、凭据、cookie、授权头与私有日志一律省略;分类来源通过 prompt-contract-classification-index.json 中的sources数组做哈希绑定(每份提案文件都带有 SHA-256),保证"被允许的就是这些、且只有这些"。
策略侧同样留痕:根规则 AGENTS.md 与仓库测试规则 tests/AGENTS.md 的 RED/GREEN 审计输出分别为 red-agents-policy-audit.txt 与 green-agents-policy-audit.txt,评审者可读的差异在 agents-policy-diff.txt 中展示(git diff --check零空白错误)。
可复用的方法论要点
- 把"裁剪"做成门禁而非人工决策:任何测试断言候选都必须落到 6 类 machine seam 之一并给出非空理由,否则 CI 拒绝——这防止未来重新长出 prompt 契约测试;
- fail-first 证据三件套:同一命令在基线(RED)与最终树(GREEN)各跑一次,中间态存档,让"确实删了该删的"可审计;
- 指纹不含行号:用
path+kind+matcher+actual+expected的 SHA-256 做稳定标识,代码挪动不会制造假 diff; - 真实宿主 QA 与单测解耦:裁剪文本契约测试后,用 OpenCode/Codex/Senpi 的真实插件安装、hook 事件序列与终端 exit code 补足"行为未变"的置信度。
这套从"AST 全量扫描 → 哈希分类 → RED/GREEN 门禁 → 多宿主 QA"的完整证据链,位于 .omo/evidence/20260817-remove-prompt-contract-tests/,可作为大型 Agent 仓库做测试治理时的直接参考范本。
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考