find-untested-sources
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFind Untested Sources
查找未测试源码
Purpose
用途
Coverage tools answer "which lines were executed?" — they require a green build
and a passing test run, which is minutes-to-tens-of-minutes on a real repo. The
question this skill answers is different and much cheaper:
Which source files have no test file referencing any of their declared types/symbols?
That's the question an agent asks before writing a new test — and it can be
answered statically in a few seconds by parsing source files, with no build,
no dependency resolution, and no compilation. The output is a deterministic
test-pairing map that lets the agent pick the next file to test without reading
the entire codebase first.
覆盖率工具回答的是“哪些代码行被执行了?”——它们需要成功构建和通过测试运行,在真实仓库中这通常需要数分钟到数十分钟。而本工具解决的问题不同,且成本低得多:
哪些源文件没有任何测试文件引用其声明的类型/符号?
这是智能体在编写新测试之前会提出的问题——通过解析源文件即可静态地在几秒内得到答案,无需构建、无需依赖解析、无需编译。输出结果是确定性的测试配对映射,让智能体无需先通读整个代码库就能选择下一个要测试的文件。
Two engines — pick one
两种引擎——二选一
This skill ships two interchangeable analyzers with a compatible JSON contract:
| Engine | Script | Use when |
|---|---|---|
| Roslyn (C#) | | The repo is .NET-only. Parses every |
| tree-sitter (polyglot) | | The repo is not exclusively C#, or you want one tool across Python, TypeScript/JavaScript, Go, Java, Rust, Ruby, and C#. |
For a .NET-only repository, prefer the Roslyn engine — its namespace-aware
pairing beats the polyglot engine's identifier overlap.
本工具提供两个可互换的分析器,它们的JSON输出格式兼容:
| 引擎 | 脚本 | 使用场景 |
|---|---|---|
| Roslyn (C#) | | 仓库为纯.NET项目。使用Roslyn语法API解析所有 |
| tree-sitter(多语言) | | 仓库并非仅包含C#,或者需要一款支持Python、TypeScript/JavaScript、Go、Java、Rust、Ruby和C#的通用工具。 |
对于纯.NET仓库,优先选择Roslyn引擎——它基于命名空间的配对能力优于多语言引擎的标识符匹配方式。
Required workflow
必选工作流程
- Use the narrowest repository or package root named by the caller. Do not scan a parent workspace when the request identifies a subdirectory.
- Execute the appropriate analyzer once. Do not replace analyzer execution with
manual globbing, filename matching, or visual inspection.
For polyglot analysis, pass when the answer must distinguish paired sources from unpaired sources.
--include-tested - Base the result on the analyzer's JSON. Preserve its paired/unpaired classification and suggested relative path; do not guess a different path.
- When the caller named a subdirectory, prefix analyzer-relative paths with that subdirectory so reported paths are workspace-relative.
- Report the requested result plus the static-pairing coverage caveat. Do not append build, package-install, test-run, or coverage commands. When paired sources exist, name their covering test files so the unpaired classification is auditable.
- 使用调用者指定的最窄范围仓库或包根目录。当请求明确子目录时,请勿扫描父级工作区。
- 执行一次对应的分析器。请勿用手动通配符匹配、文件名匹配或视觉检查替代分析器执行。若进行多语言分析,当需要区分已配对和未配对源码时,需传入参数。
--include-tested - 基于分析器输出的JSON生成结果。保留其配对/未配对分类及建议的相对路径,请勿自行猜测其他路径。
- 当调用者指定子目录时,在分析器返回的相对路径前添加该子目录,使报告路径为工作区相对路径。
- 报告请求的结果及静态配对覆盖的说明。请勿附加构建、包安装、测试运行或覆盖率命令。当存在已配对源码时,列出对应的测试文件,以便审核未配对分类的准确性。
When to Use
使用场景
- User asks "where should I add tests?", "which files have no tests?", "find untested code", "give me a test gap list", "what's the next file to test".
- Before invoking a test-generation agent, to produce a prioritized worklist.
- After generating tests, to verify each new test file pairs to a source file.
- To enumerate "weakly paired" source files (only one referring test) for follow-up depth checks.
- 用户询问“我应该在哪里添加测试?”、“哪些文件没有测试?”、“找出未测试的代码”、“给我测试缺口清单”、“下一个要测试的文件是什么”。
- 在调用测试生成智能体之前,生成优先级任务清单。
- 生成测试后,验证每个新测试文件是否与对应源文件配对。
- 枚举“弱配对”源文件(仅被一个测试文件引用)以进行后续深度检查。
When Not to Use
禁用场景
- Line/branch coverage — use .
coverage-analysis - CRAP-score / risk hotspots — use .
coverage-analysis - Are existing tests strong? — use (mutation reasoning) or
test-gap-analysis.assertion-quality
- 行/分支覆盖率分析——请使用。
coverage-analysis - CRAP评分/风险热点——请使用。
coverage-analysis - 现有测试是否足够完善?——请使用(变异推理)或
test-gap-analysis。assertion-quality
Roslyn engine (C#)
Roslyn引擎(C#)
Prerequisites
前置条件
- .NET SDK that supports file-based apps (). Pinned in the repo's
dotnet run script.cs(SDK 11 preview or later).global.json - No internet access required beyond the initial NuGet restore of
on first run.
Microsoft.CodeAnalysis.CSharp
- 支持基于文件的应用的.NET SDK(可通过运行)。仓库的
dotnet run script.cs中已固定版本(SDK 11预览版或更高)。global.json - 首次运行时仅需从NuGet还原,之后无需联网。
Microsoft.CodeAnalysis.CSharp
Usage
使用方法
powershell
undefinedpowershell
undefinedFrom the skill folder
从工具目录执行
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> [--top N]
dotnet run scripts/Find-UntestedSources.cs -- <仓库根目录> [--top N]
Save the report
保存报告
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> > pairing.json
dotnet run scripts/Find-UntestedSources.cs -- <仓库根目录> > pairing.json
Iterate the untested list, highest-API-surface first
按API规模从高到低遍历未测试文件列表
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested | Select-Object -First 10 source, decl_count, suggested_test_path
Diagnostics go to stderr; JSON goes to stdout.$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested | Select-Object -First 10 source, decl_count, suggested_test_path
诊断信息输出到stderr;JSON结果输出到stdout。Output schema
输出 schema
jsonc
{
"repo": "<absolute path>",
"elapsed_ms": 8883,
"counts": {
"source_files": 3036,
"test_files": 867,
"untested_files": 1852,
"paired_files": 1184
},
"untested": [
{
"source": "src/Foo/Bar.cs",
"decl_count": 8, // # of type declarations in the file
"suggested_test_path": // mirror of source under a discovered test project
"tests/Foo.Tests/Bar/BarTests.cs"
}
],
"source_to_tests": {
"src/Foo/Baz.cs": [
"tests/Foo.Tests/BazTests.cs",
"tests/Foo.IntegrationTests/Scenarios/BazScenarios.cs"
]
}
}jsonc
{
"repo": "<绝对路径>",
"elapsed_ms": 8883,
"counts": {
"source_files": 3036,
"test_files": 867,
"untested_files": 1852,
"paired_files": 1184
},
"untested": [
{
"source": "src/Foo/Bar.cs",
"decl_count": 8, // 文件中的类型声明数量
"suggested_test_path": // 在已发现的测试项目下镜像源文件路径
"tests/Foo.Tests/Bar/BarTests.cs"
}
],
"source_to_tests": {
"src/Foo/Baz.cs": [
"tests/Foo.Tests/BazTests.cs",
"tests/Foo.IntegrationTests/Scenarios/BazScenarios.cs"
]
}
}How it works
工作原理
- File discovery — recursive walk pruning ,
bin/,obj/,node_modules/,.git/,.vs/, and any dotted subdir. Skips generated files (packages/,.g.cs,.Designer.cs)..AssemblyInfo.cs - Test vs source classification — walks up to the nearest and marks it a test project if the project name ends in
.csproj,.Tests,.Test,.UnitTests,.IntegrationTests,.E2E,.EndToEnd,.Spec, or the content references.Specs,Microsoft.NET.Test.Sdk,MSTest.Sdk,Microsoft.Testing.Platform,xunit,NUnit, orTUnit.<IsTestProject>true</IsTestProject> - Source index (parallel) — parse each source file with
(syntax only, no compilation); record every
CSharpSyntaxTree.ParseText/BaseTypeDeclarationSyntaxasDelegateDeclarationSyntax.(ShortName, EnclosingNamespace, FilePath) - Test scan (parallel) — parse each test file, collect directives + enclosing namespace, walk every
using, look it up in the short-name index, and disambiguate strictly: an identifier is attributed only if the declaration's namespace matches one of the test file'sIdentifierTokendirectives, the enclosing namespace, or a prefix of them. This avoids noise where common names likeusingorSettingsmatch every project.Context - Pairing & suggestion — invert into . Build a production-to-test project map from
source → [tests]entries; for each untested source, mirror its in-project relative path under the referencing test project to suggest a path.<ProjectReference> - JSON emit — ordered by declaration count desc, then alphabetical.
- 文件发现——递归遍历目录,排除、
bin/、obj/、node_modules/、.git/、.vs/及所有点开头的子目录。跳过生成文件(packages/、.g.cs、.Designer.cs)。.AssemblyInfo.cs - 测试文件与源文件分类——向上遍历至最近的文件,若项目名称以
.csproj、.Tests、.Test、.UnitTests、.IntegrationTests、.E2E、.EndToEnd、.Spec结尾,或项目内容引用.Specs、Microsoft.NET.Test.Sdk、MSTest.Sdk、Microsoft.Testing.Platform、xunit、NUnit,或包含TUnit,则标记为测试项目。<IsTestProject>true</IsTestProject> - 源码索引(并行)——使用解析每个源文件(仅语法分析,无需编译);记录每个
CSharpSyntaxTree.ParseText/BaseTypeDeclarationSyntax为DelegateDeclarationSyntax。(短名称, 所属命名空间, 文件路径) - 测试扫描(并行)——解析每个测试文件,收集指令+所属命名空间,遍历每个
using,在短名称索引中查找,并严格消歧:仅当声明的命名空间与测试文件的IdentifierToken指令、所属命名空间或其前缀匹配时,才将标识符关联到对应源文件。这避免了using或Settings这类通用名称导致的跨项目误匹配。Context - 配对与路径建议——将结果转换为的映射。从
源文件 → [测试文件]条目构建生产项目到测试项目的映射;对于每个未测试源文件,在引用它的测试项目下镜像其项目内相对路径,生成建议的测试文件路径。<ProjectReference> - JSON输出——按声明数量降序排列,再按字母顺序排列。
Polyglot engine (tree-sitter)
多语言引擎(tree-sitter)
Prerequisites
前置条件
- Python 3.10+.
- (single self-contained wheel that bundles parsers for 300+ languages and the high-level
pip install tree-sitter-language-packAPI). No native build, no per-language grammar install.process()
- Python 3.10+。
- 安装(单个独立包,包含300+语言的解析器和高级
pip install tree-sitter-language-packAPI,无需原生构建,无需单独安装各语言语法)。process()
Usage
使用方法
powershell
undefinedpowershell
undefinedFrom the skill folder
从工具目录执行
python scripts/find_untested_sources.py <repo-root>
python scripts/find_untested_sources.py <仓库根目录>
Restrict to a language (repeatable)
限制语言(可重复指定)
python scripts/find_untested_sources.py <repo-root> --lang python --lang typescript
python scripts/find_untested_sources.py <仓库根目录> --lang python --lang typescript
Truncate the report (top 20 by declared API surface)
截断报告(按API规模取前20个未测试文件)
python scripts/find_untested_sources.py <repo-root> --limit-untested 20 > pairing.json
python scripts/find_untested_sources.py <仓库根目录> --limit-untested 20 > pairing.json
Iterate, highest-API-surface first
按API规模从高到低遍历
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested_sources | Select-Object -First 10 path, declaration_count, suggested_test_path
Pass `--include-tested` to additionally emit `tested_sources` (omitted by
default to keep the payload small for LLM consumption). Diagnostics go to
stderr; JSON goes to stdout.$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested_sources | Select-Object -First 10 path, declaration_count, suggested_test_path
传入`--include-tested`参数可额外输出`tested_sources`(默认省略以减少LLM处理的 payload 大小)。诊断信息输出到stderr;JSON结果输出到stdout。Output schema
输出 schema
jsonc
{
"repo_root": "<absolute path>",
"summary": {
"source_files": 3138,
"test_files": 761,
"tested_source_files": 1419,
"untested_source_files": 1719,
"orphan_test_files": 15,
"languages": ["csharp"]
},
"untested_sources": [
{
"path": "src/Foo/Bar.cs",
"language": "csharp",
"declaration_count": 8,
"declarations": ["Bar", "BarOptions", "IBar", "..."],
"suggested_test_path": "src/Foo/BarTests.cs"
}
],
"orphan_tests": [
{ "path": "tests/SomeIntegrationTest.cs", "language": "csharp" }
]
}jsonc
{
"repo_root": "<绝对路径>",
"summary": {
"source_files": 3138,
"test_files": 761,
"tested_source_files": 1419,
"untested_source_files": 1719,
"orphan_test_files": 15,
"languages": ["csharp"]
},
"untested_sources": [
{
"path": "src/Foo/Bar.cs",
"language": "csharp",
"declaration_count": 8,
"declarations": ["Bar", "BarOptions", "IBar", "..."],
"suggested_test_path": "src/Foo/BarTests.cs"
}
],
"orphan_tests": [
{ "path": "tests/SomeIntegrationTest.cs", "language": "csharp" }
]
}How it works
工作原理
-
File discovery — recursive walk pruning common build/vendor dirs (,
bin,obj,node_modules,target,dist,build,vendor,__pycache__,.venv, …) and generated files (.git,.d.ts,.g.cs,.Designer.cs,_pb2.py,*.min.js, …).AssemblyInfo.cs -
Language detection —maps the extension to a supported language; unknown extensions are skipped.
detect_language_from_path -
Test-vs-source classification — per-language path heuristics:
Language Test rule Python path contains /tests/; or filename starts withtest/or endstest_; or_test.py.conftest.pyJS/TS/TSX path contains ,__tests__,tests,test,spec; or filename containse2e/.test...spec.Go filename ends ._test.goJava path contains /test; or filename endstests/Test.java.Tests.javaRust path contains /tests/.benches/C# path contains ; or project segment endstests//.Tests/.Test/.UnitTests; or filename ends.IntegrationTests/Tests.TestRuby path contains /spec/; or filename endstest//_spec.rb._test.rb -
Per-file extraction —returns declared items, raw import statements, and a flat declared -name list.
process(text, ProcessConfig(structure, imports, symbols)) -
Pairing — for each test file, union import resolution (per language, e.g. Python→
from pkg.mod import x; Javapkg/mod.py→import a.b.C;; C#a/b/C.javais namespace-not-file, so a no-op) with identifier overlap (word-like tokens, length ≥ 4, matched against declared names).using -
JSON emit —ordered by declaration count descending.
untested_sources
-
文件发现——递归遍历目录,排除常见构建/依赖目录(、
bin、obj、node_modules、target、dist、build、vendor、__pycache__、.venv等)及生成文件(.git、.d.ts、.g.cs、.Designer.cs、_pb2.py、*.min.js等)。AssemblyInfo.cs -
语言检测——根据文件扩展名映射到支持的语言;未知扩展名的文件会被跳过。
detect_language_from_path -
测试文件与源文件分类——基于语言的路径规则:
语言 测试文件判定规则 Python 路径包含 /tests/;或文件名以test/开头或test_结尾;或为_test.py。conftest.pyJS/TS/TSX 路径包含 、__tests__、tests、test、spec;或文件名包含e2e/.test.。.spec.Go 文件名以 结尾。_test.goJava 路径包含 /test;或文件名以tests/Test.java结尾。Tests.javaRust 路径包含 /tests/。benches/C# 路径包含 ;或项目段以tests//.Tests/.Test/.UnitTests结尾;或文件名以.IntegrationTests/Tests结尾。TestRuby 路径包含 /spec/;或文件名以test//_spec.rb结尾。_test.rb -
单文件提取——返回声明项、原始导入语句和平坦化的声明名称列表。
process(text, ProcessConfig(structure, imports, symbols)) -
配对逻辑——对于每个测试文件,结合导入解析(各语言不同,例如Python→
from pkg.mod import x;Javapkg/mod.py→import a.b.C;;C#a/b/C.java是命名空间而非文件,因此不处理)和标识符匹配(类单词令牌,长度≥4,与声明名称匹配)。using -
JSON输出——按声明数量降序排列。
untested_sources
Limitations (be honest with the agent)
局限性(需如实告知智能体)
Both engines are static, parse-only heuristics that trade a little accuracy for
orders-of-magnitude lower cost than coverage. Known gaps:
- Reflection / DI-resolved types referenced only via a string name or container resolution won't be detected — the type's short name never appears in the test source.
- Extension methods invoked as instance methods (C#): the declaring static class is not named, so its file is not credited.
- , target-typed
var, pattern matching lose the type token; the file-level union usually still catches it through other references.new() - Short identifier names (polyglot, < 4 chars) are dropped to avoid noisy
pairings on names like ,
id,db.Tag - Monorepo path aliases (TS path mapping, Java module-info) are not resolved; a suffix-match fallback may pick the wrong source if two files share a trailing path segment.
For these cases, run actual coverage () on the unpaired
candidates the agent has already triaged.
coverage-analysisAlways label the final result as a static pairing heuristic, not evidence of
line or branch coverage. Include that caveat even when every requested source
file has an obvious matching or missing test.
两个引擎均为静态、仅解析的启发式工具,以少量准确性为代价换取比覆盖率分析低几个数量级的成本。已知的局限性包括:
- 反射/依赖注入解析的类型:仅通过字符串名称或容器解析引用的类型无法被检测到——因为测试源码中不会出现该类型的短名称。
- 扩展方法(C#):当作为实例方法调用时,声明该方法的静态类不会被命名,因此其所在文件不会被标记为已配对。
- 、目标类型
var、模式匹配:会丢失类型令牌;通常文件级的联合匹配仍会通过其他引用捕获到对应类型。new() - 短标识符名称(多语言引擎,<4个字符):会被忽略,以避免、
id、db这类名称导致的误配对。Tag - 单仓库路径别名(TS路径映射、Java module-info):不会被解析;若两个文件共享末尾路径段,后缀匹配的回退逻辑可能会选错源文件。
对于这些情况,可对智能体已筛选出的未配对候选文件运行实际的覆盖率分析()。
coverage-analysis请始终将最终结果标记为静态配对启发式分析,而非行或分支覆盖率的证据。即使所有请求的源文件都有明显的匹配或缺失测试,也需包含此说明。
Outputs the agent should consume
智能体应使用的输出内容
- /
untested[*].source— pick the next source file to test (highest declaration count first).untested_sources[*].path - — drop-in target for the new test file; the Roslyn engine honors the test project that already
*.suggested_test_paths the source's project, so<ProjectReference>is not needed. The polyglot engine may suggest a co-located test when no test root is discoverable; that is a valid fallback, but prefer an established repository test directory when one exists.dotnet sln add - (Roslyn) /
source_to_tests--include-tested(polyglot) — verify a newly written test file lands in the list for the intended source.tested_sources - (polyglot) — tests that don't reference any same-language source file; useful for triaging stale or integration-only tests.
orphan_tests
- /
untested[*].source——选择下一个要测试的源文件(优先选择声明数量最多的)。untested_sources[*].path - ——新测试文件的直接目标路径;Roslyn引擎会遵循已通过
*.suggested_test_path引用源项目的测试项目,因此无需执行<ProjectReference>。当未发现测试根目录时,多语言引擎可能会建议同目录的测试文件,这是有效的回退方案,但如果仓库已有既定的测试目录,优先使用该目录。dotnet sln add - (Roslyn)/
source_to_tests参数下的--include-tested(多语言)——验证新编写的测试文件是否被列入对应源文件的测试列表中。tested_sources - (多语言)——未引用任何同语言源文件的测试;可用于筛选过时或仅用于集成测试的文件。",
orphan_tests