`TEST.md` 与测试组织:它到底测了什么
判断一个 harness 值不值得用,最省事的做法是先翻它的 TEST.md——毕竟里面通常贴着一整块 pytest 输出,末尾还写着「N passed」。但这份文件读起来有个陷阱:它不是一份测完之后写的报告,而是两段在不同时间写的东西拼在一起。前半段写在代码之前,后半段才是跑完粘上去的。不知道这一点,很容易把一个计划数字当成验收结论。
本文只讲 TEST.md 这一份和它背后的测试组织。harness 的整棵目录树、四份文档各自写给谁看,另有一篇专门讲,这里不重复。
规范把它切成了两段
方法论主文档在 cli-anything-plugin/HARNESS.md,我们采集时(2026-08-10,对应仓库快照 39634a6)这份文件是 747 行。TEST.md 在里面出现了两次,分属两个阶段:
- Phase 4 Test Planning (TEST.md - Part 1),位置在
HARNESS.md113 到 139 行,要求写四块:Test Inventory Plan、Unit Test Plan、E2E Test Plan、Realistic Workflow Scenarios。这一阶段排在 Phase 5 Test Implementation 之前。 - Phase 6 Test Documentation (TEST.md - Part 2),位置在 221 到 227 行,要求在测试跑完之后追加三块:Test Results(贴
pytest -v --tb=no的全量输出)、Summary Statistics、Coverage Notes。
也就是说,同一份文件的上半截是「打算测什么」,下半截是「跑出来什么」。Phase 4 那一段的原文用词是 estimated test counts(HARNESS.md 第 117 行),清单里的数字在规范定义上就是预估值。
这个切法本身是合理的——测试计划先行,实现之后补结果。会出问题的是读者这一侧:文件名叫 TEST.md,正文里数字成排,很难第一眼分清哪个数是预估、哪个数是输出。
57 份实际长什么样
我们采集时数了一遍:
find . -path '*/agent-harness/*' -name TEST.md | wc -l
结果是 57 份,而同期 tests/ 目录有 68 个、test_core.py 66 个、test_full_e2e.py 63 个。规范原文写的是 “Every cli_anything/<software>/tests/ directory MUST contain a TEST.md”(HARNESS.md 552 到 553 行),我们采集时有 12 个 harness 没有:3MF、browser、godot、intelwatch、live2d、macrocli、novita、rekordbox、siyuan、sketch、unimol_tools、videocaptioner。其中 browser 是 HARNESS.md 里被当作 MCP 后端范例反复引用的那一个。规范写的是 MUST,仓库里有 12 处没有,这是两个可以各自核对的事实,我们只陈述到这里。
另外还有三个 harness 把 TEST.md 放在 agent-harness/ 顶层而不是 tests/ 下:openscreen、quietshrink、shotcut。你按规范路径去找文件找不到时,记得往上一层看看。
对这 57 份汇总二级标题,出现频次前列是:## Test Results 35、## Test Inventory Plan 14、## Test Inventory 14、## Unit Tests (`test_core.py`) 12、## Unit Test Plan 10、## Realistic Workflow Scenarios 10、## Coverage Notes 10、## Running Tests 9、## E2E Test Plan 9、## End-to-End Tests (`test_full_e2e.py`) 8、## Test Plan 7。
这张频次表里有件事值得注意:同一个含义存在两套叫法——Test Inventory 与 Test Inventory Plan 各 14 份。你要写脚本批量抽这一节,两个标题都得匹配。
blender 那份是结构最完整的样本之一,小节序列是:## Test Inventory(File / Test Classes / Test Count / Focus 四列表)→ ## Unit Tests (test_core.py)(每个测试类一个三级标题,标题里带 “(N tests)“,正文逐条列被测行为)→ ## End-to-End Tests (test_full_e2e.py)(同格式)→ ## Test Results(贴 pytest 输出块),对应 blender/agent-harness/cli_anything/blender/tests/TEST.md 的第 3、11、111、160 行。
顺带一个细节:多份 TEST.md 里贴的 pytest 头部是同一套环境串——含 pytest-9.0.2 的 14 份、含 rootdir: /root/cli-anything 的 6 份。这两个数是我们用 grep -rl ... --include=TEST.md . | wc -l 数出来的,你可以自己复现。
反直觉的那一处:这里的 E2E 不等于「真的驱动了宿主软件」
这是本篇最要紧的一段。
规范侧对 E2E 的要求写得非常硬(HARNESS.md 538 到 547 行),三条原文都是 MUST:
- “E2E tests MUST invoke the real software and produce real output files”
- “Every export/render function MUST be verified with programmatic output analysis. ‘It ran without errors’ is not sufficient.”
- “E2E tests MUST include subprocess tests that invoke the installed
cli-anything-<software>command via_resolve_cli()”
但 blender 那份 TEST.md 里,两类测试的自述口径是这样的:
- 讲
test_core.py的那一节:"All unit tests use synthetic/in-memory data only. No Blender installation required."(这句在该TEST.md的第 13 行,不是.py源码里的行号) - 讲
test_full_e2e.py的那一节:"No Blender binary required"(同一份TEST.md第 113 行),只验证 bpy 脚本生成、工程往返与 CLI 子进程调用
单元测试不需要装宿主软件,这不奇怪。奇怪的是标着 end-to-end 的那一份也写「不需要 Blender 二进制」——规范那条 MUST 说的是必须调起真软件并产出真文件。规范写的是一种口径,样本 harness 的 TEST.md 写的是另一种,这是两处可以各自核对的文本,我们只陈述差异。
对读者来说,实际影响是很具体的:当你在某个 harness 的 TEST.md 底部看到「200 passed」,这个数字覆盖的很可能是命令行这一侧——参数解析、工程 JSON 的读写往返、生成脚本的语法是否成立、以及子进程调起 CLI 本身——而不是「Blender 真的按这份脚本渲染出了一张图」。要判断某一份到底属于哪种,可以直接看它的 ## Unit Tests / ## End-to-End Tests 小节里有没有写”required”那句话,再翻一眼 test_full_e2e.py 里的测试类名。
blender 的 test_full_e2e.py 有 9 个测试类:TestSceneLifecycle、TestBPYScriptGeneration、TestWorkflows、TestCLISubprocess、TestPreviewE2E、TestScriptValidity、TestBlenderBackend、TestBlenderRenderE2E、TestBlenderRenderScriptE2E。类名本身就把「脚本生成 / 子进程 / 后端 / 渲染」几档分开了。
和这一点配套的还有两个机制,都值得单独记一下。
一个是 _resolve_cli,共享的子进程测试助手(blender/.../tests/test_full_e2e.py 593 到 605 行):优先 shutil.which(name) 用已安装的命令,找不到就回退 python -m;设环境变量 CLI_ANYTHING_FORCE_INSTALLED=1 则强制要求必须是已安装的那个。全仓 44 个文件出现了 _resolve_cli。这意味着同一份测试在「装了 CLI」和「没装」两种环境下走的是不同路径。
另一个是跳过策略:我们采集时有 32 个测试文件用了 pytest.mark.skipif(grep -rl 'pytest.mark.skipif' --include='test_*.py' . | wc -l)。宿主软件缺失时,这类用例是跳过而不是失败。所以一份绿的 pytest 输出里,可能有相当一部分用例根本没执行。
这里必须说清楚一件事:这类 harness 的测试本身就会在本机起子进程、执行外部程序与脚本,真要跑”调起真软件”那一档,还得你自己先把宿主软件装上。跑不跑、在什么机器上跑,请结合自身环境评估。
声明的测试数与代码里的函数定义数对不上
前面说过 Phase 4 那一段是预估。落到具体文件上,我们采集时数到三处偏差:
| harness | TEST.md 声明 | 我们数到的 def test_ |
|---|---|---|
| blender | test_core.py 8 类 156 个、test_full_e2e.py 5 类 44 个、Total 13 类 200 个(该文件 7 到 9 行),底部 pytest 块写 “200 passed”(167 到 172 行) | test_core.py 165 个 / 9 类,test_full_e2e.py 63 个 / 9 类,另有 TEST.md 完全未提及的第三个文件 test_bpy_gen.py(6 个) |
| audacity | test_core.py 10 类 109 个、test_full_e2e.py 7 类 45 个、Total 154,末尾 pytest 块写 “154 passed”(该文件 7 到 9 行) | test_core.py 107 个、test_full_e2e.py 56 个,另有未登记的 test_eval.py 3 个 |
| adguardhome | ”test_core.py: 20 unit tests”、“test_full_e2e.py: 12 E2E + subprocess tests”(该文件 5 到 6 行) | test_core.py 24 个、test_full_e2e.py 12 个 |
audacity 这一项还有第三个口径:audacity/agent-harness/AUDACITY.md 第 150 行与第 159 行写的是 “Unit tests (test_core.py): 60+ tests” 与 “E2E tests (test_full_e2e.py): 40+ tests”,与同一个 harness 里 TEST.md 的 109/45、以及我们数到的 107/56 三方各不相同。三处文本各自可核对,我们只陈述到这里。
我们的计数口径也得交代清楚,否则这张表没法复现:数的是正则 ^\s*def test_ 匹配到的函数定义数。这个口径会少计参数化用例(@pytest.mark.parametrize 展开后的实际用例数更多),也会多计被 skip 掉的用例。所以上面这张表的准确说法是「声明数与函数定义数不符」,不是「实际通过数不符」。我们没有运行过任何一份测试,TEST.md 里的 “N passed” 一律按文本记录,没有验证过其真实性。
各家的测试组织并不一样
规范给的分工是两个文件:test_core.py 管单元、test_full_e2e.py 管端到端。实际落地时有几种明显的扩展形态,读一个陌生 harness 时先看它属于哪种,比逐行读测试快得多。
一是拆得更细的。 browser 把测试拆成四个文件,我们采集时各自的 def test_ 数是:test_core.py 33 个、test_domshell_backend.py 113 个、test_full_e2e.py 11 个、test_security.py 49 个。后端适配那一份的用例数是核心的三倍多,安全那一份也单独占了 49 个——它对应的是 browser/.../utils/security.py 里的 URL scheme 白名单校验、内网拦截开关与文本长度截断那一层。这个 harness 的测试重心明显不在 core/。
二是多出一份未登记文件的。 blender 的 test_bpy_gen.py 只含一个类 TestGeneratedScriptSyntax,而它的 TEST.md 里完全没提这个文件。你按 TEST.md 的清单去理解覆盖面,会漏掉这一份。
三是加了规范之外的评测层。 audacity 和 rekordbox 两个 harness 有 eval/ 目录(全仓只有这 2 个)。audacity/.../eval/runner.py 是 299 行,提供 discover_tasks、run_eval、load_baseline、compare_baseline(回归基线对比)与 _report_to_markdown,任务放在 eval/tasks/ 下,我们采集时是 4 个:effects_registry、export_wav、project_roundtrip、track_clip_flow。配套的 tests/test_eval.py 里是 3 个模块级测试函数:test_discover_tasks、test_run_eval_creates_reports、test_compare_baseline_detects_regression。这一层做的事和 pytest 那一层不同——它是把任务跑出来的结果和一份基线比对,看有没有回归。
规模上给个背景数:我们采集时 harness 内的测试文件共 159 个,def test_ 总数 5479(同样是 ^\s*def test_ 口径)。这个量分摊到 69 个 harness 上,平均每个几十条。
你可以自己跑一遍的核对动作
读一个陌生 harness 的 TEST.md 时,按下面这个顺序走,能在几分钟内判断它测的是哪一档:
# 1. 这个 harness 有没有 TEST.md,在 tests/ 下还是顶层
find . -path '*/agent-harness/*' -name TEST.md | wc -l
# 2. 声明数:TEST.md 的 Test Inventory 表里写了多少
# 3. 定义数:代码里实际有多少个测试函数
grep -cE '^\s*def test_' <harness>/agent-harness/cli_anything/<app>/tests/*.py
# 4. 有多少用例会在缺宿主软件时被跳过
grep -rl 'pytest.mark.skipif' --include='test_*.py' .
# 5. 成熟度声明
grep -rh 'Development Status' */agent-harness/setup.py */agent-harness/pyproject.toml | sort | uniq -c
第 2 步和第 3 步的差值,就是这篇文章的主题。第 5 步的结果我们采集时是:45 处标 4 - Beta、2 处标 3 - Alpha,全仓没有任何一个 harness 标 5 - Production/Stable——这是仓库自己给的口径,和上面那些测试数字放在一起看更合适。
以上命令为按仓库中的文件组织方式组合的核查示例,未经实测,以你本地 clone 的实际内容为准。数字会随上游更新变动,重要的不是记住 57 和 5479,而是记住 TEST.md 上半截是计划、下半截是输出,以及这个仓库里的「E2E」不必然意味着宿主软件真的被调起来过。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。