`TEST.md` 与测试组织:它到底测了什么

2026-08-10

判断一个 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.md 113 到 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 countsHARNESS.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 没有:3MFbrowsergodotintelwatchlive2dmacroclinovitarekordboxsiyuansketchunimol_toolsvideocaptioner。其中 browserHARNESS.md 里被当作 MCP 后端范例反复引用的那一个。规范写的是 MUST,仓库里有 12 处没有,这是两个可以各自核对的事实,我们只陈述到这里。

另外还有三个 harness 把 TEST.md 放在 agent-harness/ 顶层而不是 tests/ 下:openscreenquietshrinkshotcut。你按规范路径去找文件找不到时,记得往上一层看看。

对这 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 InventoryTest 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 里的测试类名。

blendertest_full_e2e.py 有 9 个测试类:TestSceneLifecycleTestBPYScriptGenerationTestWorkflowsTestCLISubprocessTestPreviewE2ETestScriptValidityTestBlenderBackendTestBlenderRenderE2ETestBlenderRenderScriptE2E。类名本身就把「脚本生成 / 子进程 / 后端 / 渲染」几档分开了。

和这一点配套的还有两个机制,都值得单独记一下。

一个是 _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.skipifgrep -rl 'pytest.mark.skipif' --include='test_*.py' . | wc -l)。宿主软件缺失时,这类用例是跳过而不是失败。所以一份绿的 pytest 输出里,可能有相当一部分用例根本没执行。

这里必须说清楚一件事:这类 harness 的测试本身就会在本机起子进程、执行外部程序与脚本,真要跑”调起真软件”那一档,还得你自己先把宿主软件装上。跑不跑、在什么机器上跑,请结合自身环境评估。

声明的测试数与代码里的函数定义数对不上

前面说过 Phase 4 那一段是预估。落到具体文件上,我们采集时数到三处偏差:

harnessTEST.md 声明我们数到的 def test_
blendertest_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 个)
audacitytest_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 个
adguardhometest_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/

二是多出一份未登记文件的。 blendertest_bpy_gen.py 只含一个类 TestGeneratedScriptSyntax,而它的 TEST.md 里完全没提这个文件。你按 TEST.md 的清单去理解覆盖面,会漏掉这一份。

三是加了规范之外的评测层。 audacityrekordbox 两个 harness 有 eval/ 目录(全仓只有这 2 个)。audacity/.../eval/runner.py 是 299 行,提供 discover_tasksrun_evalload_baselinecompare_baseline(回归基线对比)与 _report_to_markdown,任务放在 eval/tasks/ 下,我们采集时是 4 个:effects_registryexport_wavproject_roundtriptrack_clip_flow。配套的 tests/test_eval.py 里是 3 个模块级测试函数:test_discover_taskstest_run_eval_creates_reportstest_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.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。