Understand-Anything tested_by测试覆盖链接机制揭秘:两遍归一化如何规范测试边
【免费下载链接】Understand-AnythingGraphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.项目地址: https://gitcode.com/GitHub_Trending/un/Understand-Anything
Understand-Anything 能把任意代码仓库变成可探索、可搜索、可提问的交互式知识图谱,而图谱质量的关键一环,就是tested_by 测试覆盖链接机制:在合并分析批次的merge-batch-graphs.py中,一个确定性的"两遍归一化"链接器(linker)会把散落在各批次里的测试边统一规范为production → test方向,让知识图谱里的测试覆盖关系清晰、可审计、不会重复。本文面向新手,带你看懂这套机制的工作原理。
一、问题背景:LLM 生成的测试边为什么会"乱"
在/understand技能的多智能体流水线中,文件分析智能体会把每个批次的分析结果写成batch-*.json,其中包含节点(文件、函数、类)和边(imports、calls、tested_by等关系类型,完整枚举见 schema.ts)。
其中tested_by表示"某生产代码文件被某个测试文件覆盖"。但 LLM 生成的这类边有三个通病:
| 通病 | 原因 |
|---|---|
| 方向系统性反了 | LLM 只有在分析测试文件时才会看到import 生产代码的关系,于是它习惯把测试文件写在 source 一侧(test → production) |
| 出现无意义的边 | test↔test、production↔production、端点节点缺失的"孤儿边" |
| 覆盖不全 | 很多测试文件根本没被 LLM 关联到任何生产文件 |
简单地把这些边"删掉重算"会丢掉 LLM 真实看到的配对证据(比如一个 Go 的_test.go覆盖同包多个.go文件)。Understand-Anything 的方案是:保留配对证据,纠正方向,补齐遗漏——这正是两遍归一化的核心思想(见 SKILL.md 的说明)。
二、第一遍:保留 LLM 语义,纠正方向(Pass 1)
核心逻辑位于 merge-batch-graphs.py 的link_tests函数中。Pass 1 遍历所有type == "tested_by"的边,对每条边按端点分类做三选一处理:
- 方向正确(production → test):原样保留;
- 方向反了(test → production):原地交换 source/target,把方向纠正为
forward,并在 description 中追加[direction corrected]审计标记——配对证据被完整保留,只是方向修正了; - 语义损坏(test↔test、prod↔prod、端点缺失):直接丢弃,因为已无可恢复的含义。
此外还有两个工程细节:
- 权重感知去重:同一对 (production, test) 出现多条边时,保留 weight 更大的那条(规则与后续 Step 6 的边去重保持一致),避免低置信度边"占位";
tested标签:凡是最终成为某条tested_by边 source 侧的生产文件节点,都会被打上"tested"标签,方便在面板里一眼看出哪些代码有测试覆盖。
三、第二遍:按路径约定补齐遗漏(Pass 2)
Pass 1 之后,还有一些测试文件没有被任何边覆盖。Pass 2 就登场了:对每个未配对的测试文件,调用 production_candidates 按各语言的路径约定生成候选生产文件路径列表,取第一个在图中真实存在的候选,生成一条新的production → test边(weight 0.5,description 标记为 "Path-based pairing (deterministic)")。
候选规则覆盖了主流语言布局,配置集中在文件顶部的 tested_by linker 配置区:
- JS/TS 系:
foo.test.ts→ 同目录foo.ts/tsx/js/...;支持__tests__/、test/、spec/子目录"走出"匹配;支持tests/foo/X.test.ts→src|app|lib|根目录/foo/X.ts的镜像树; - Go:
foo_test.go→ 同目录foo.go; - Python:
test_bar.py/bar_test.py→ 同目录或镜像树bar.py,支持 Django 式mypkg/tests/布局; - Java/Kotlin:Maven/Gradle 布局
src/test/java/...→src/main/java/...; - C#:支持
<svc>/tests/→<svc>/src/以及My.App.Tests/→My.App/的镜像项目; - Swift/Rust/PHP:
tests/目录内的文件一律视为测试源。
判断"某个路径是否算测试文件"由 is_test_path 统一完成——注意 JS/TS 系要求文件名必须带.test/.spec中缀,所以__tests__/helpers.ts这种工具文件不会被误判成测试。
四、结果在哪里看:合并报告自审计
整个链接器在merge_and_normalize的Step 5b阶段运行(位于节点去重之后、边去重之前),并在合并报告中输出独立小节:
- Fixed区统计"翻转了多少条测试边、丢弃了多少条损坏边";
- Tested-by linker区统计"新增了多少条路径约定边、给多少生产节点打上 tested 标签"。
这样每次合并都是可审计的:哪些边是 LLM 证据、哪些是确定性规则补齐、哪些被纠正了方向,一目了然。最终产物写入<ua-dir>/intermediate/assembled-graph.json,供后续面板渲染与评审阶段消费。
五、测试覆盖:91 个单元测试守住行为契约
这套机制本身也有完善的测试。test_merge_batch_graphs.py 包含 91 个测试用例,重点验证:
IsTestPathTests:13 种语言的测试文件识别与生产文件排除;ProductionCandidatesTests:各语言候选路径的生成顺序与优先级(同目录优先于镜像树);LinkTestsTests:反向边"交换而非删除"、test↔test 与孤儿边丢弃、重复边保留高权重、补齐不重复已有配对、幂等性等;- 典型用例 test_inverted_llm_edge_is_swapped_not_stripped 直接演示了
src/foo.test.ts → src/foo.ts的反向边被纠正为src/foo.ts → src/foo.test.ts,且 description 中留痕 "direction corrected"。
运行方式:
python -m unittest tests.skill.understand.test_merge_batch_graphs -v六、小结:为什么"交换"优于"删除重算"
| 方案 | 优点 | 缺点 |
|---|---|---|
| 删除全部 LLM 测试边,纯路径约定重算 | 规则简单 | 丢失真实项目的非标准配对(如一对多测试覆盖),覆盖率下降 |
| 两遍归一化(本项目方案) | 保留 LLM 真实证据 + 纠正方向 + 补齐遗漏 + 全程留痕可审计 | 规则较多,但均有单测约束 |
对新手而言,记住一句话即可:tested_by 边最终永远是"生产文件 → 测试文件",方向反的会被翻转、坏掉的会被丢弃、缺掉的会按路径约定补上,而且每一步都会在合并报告里留下数字账目。这也是 Understand-Anything "图谱是用来教你的,不是用来炫技的"这一理念的落地细节。
💡 延伸阅读:SKILL.md 中 Phase 2 合并脚本的完整说明,以及 graph-reviewer.md 中 27 种边类型(含
tested_by)的评审约定。
【免费下载链接】Understand-AnythingGraphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.项目地址: https://gitcode.com/GitHub_Trending/un/Understand-Anything
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考