captain-obvious
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCaptain Obvious
Captain Obvious
Deletes tests that assert what is already guaranteed — by the compiler, by the
mock framework, or by the laws of logic. These tests burn CI time, inflate
coverage confidence, and can never catch a regression. They are the signature
of AI-generated test suites (empirical studies find test smells in 38–100% of
LLM-generated tests).
The heavy lifting is done by two deterministic scripts in . Your job
is orchestration: run them, interpret the report, clean up the residue, and
verify nothing broke. Do not hand-scan test files or spawn subagents per
file — one script invocation scans the whole project.
scripts/该工具会删除那些断言已被编译器、模拟框架或逻辑定律所保证内容的测试。这类测试会消耗CI时间、虚增覆盖率可信度,且永远无法捕获回归问题。它们是AI生成测试套件的典型特征(实证研究发现,38%-100%的大语言模型生成测试存在测试异味)。
核心工作由目录下的两个确定性脚本完成。你的任务是协调流程:运行脚本、解读报告、清理残留内容,并验证未出现问题。请勿手动扫描测试文件或为每个文件启动子代理——一次脚本调用即可扫描整个项目。
scripts/Workflow
工作流程
1. Detect the stack(s)
1. 检测技术栈
- TypeScript: a and
tsconfig.json/*.test.ts/*.spec.tsfiles.__tests__ - Python: /
test_*.pyfiles (pytest).*_test.py - A repo can have both; run both detectors.
- TypeScript:项目需包含以及
tsconfig.json/*.test.ts/*.spec.ts文件。__tests__ - Python:项目需包含/
test_*.py文件(基于pytest)。*_test.py - 仓库可同时包含两种技术栈;需运行对应的两个检测器。
2. Safety first
2. 安全优先
The fix step edits test files in place. Both scripts enforce this themselves:
exits 2 unless the target is a git repository with a clean working
tree (untracked files are fine). If it refuses, stash or commit rather than
reaching for — removes the only undo path there is
(), so use it only when the user has explicitly
accepted that.
--fix--force--forcegit checkout -- <files>Trust boundary: scanning executes the project's own toolchain. mypy loads
from the repo's config as in-process Python;
/ resolve (and can run) the
repo's dependencies; the TS side loads the repo's own
package. Run the scan only on repositories you would be willing to run
/ in yourself.
[tool.mypy] plugins--mypy "uv run mypy""poetry run mypy"typescriptmypytscNote: when installed as a plugin, a write-time PreToolUse hook may also be
active — if a test-file Write/Edit is denied with a "captain-obvious:"
reason during cleanup rewrites, fix the flagged assertions instead of
re-trying the same content (see ). Both scanners
also support for a syntactic-only single-file
scan (JSON to stdout; no mypy/tsc, no side effects).
references/prevention.md--file <path> [--stdin]修复步骤会直接编辑测试文件。两个脚本自身都有安全限制:参数仅当目标是git仓库且工作树干净时才会执行(未跟踪文件不受影响),否则会以状态码2退出。如果脚本拒绝执行,请先暂存或提交更改,而非使用参数——会移除唯一的撤销路径(),仅当用户明确接受该风险时才可使用。
--fix--force--forcegit checkout -- <files>信任边界:扫描过程会执行项目自身的工具链。mypy会从仓库配置中加载并作为进程内Python代码运行; / 会解析(并可能运行)仓库的依赖;TypeScript端会加载仓库自身的包。仅在你愿意自行运行/的仓库中执行扫描。
[tool.mypy] plugins--mypy "uv run mypy""poetry run mypy"typescriptmypytsc注意:当作为插件安装时,写入时的PreToolUse钩子可能会生效——如果在清理重写过程中,测试文件的写入/编辑操作因「captain-obvious:」原因被拒绝,请修复标记的断言,而非重试相同内容(详见)。两个检测器还支持参数,用于仅做语法分析的单文件扫描(结果以JSON输出到标准输出;不调用mypy/tsc,无副作用)。
references/prevention.md--file <path> [--stdin]3. Scan (report-only)
3. 扫描(仅生成报告)
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --json /tmp/co-ts.json
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --json /tmp/co-py.json- The Python detector shells out to mypy for the type-guaranteed category. Use
the project's own environment: pass for uv projects,
--mypy "uv run mypy"for poetry, etc. If mypy isn't available it degrades gracefully to the syntactic categories.--mypy "poetry run mypy" - Note: the mypy pass briefly writes copies next to test files (removed when the run ends) — so a "report-only" scan does touch the working tree. Pass
_cap_obv_shadow_*for a strictly read-only scan; if the tree is not writable the scan degrades to syntactic categories and says so.--no-types - The TS detector resolves the project's own package; without a tsconfig it degrades to syntactic categories.
typescript - If the project already produces coverage (or you can cheaply run it),
pass (lcov / istanbul
--coverage <file>/ coverage.pycoverage-final.json). This is the dynamic half of the ICSE'19 rotten-green analysis: acoverage jsonwhose line never ran is promoted to proven rotten, and one that did run is dropped as a confirmed false positive. It turns the noisiest advisory category into a trustworthy one — use it whenever coverage is available.conditional-assert
Show the user the summary table and the findings before deleting anything.
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --json /tmp/co-ts.json
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --json /tmp/co-py.json- Python检测器会调用mypy来检测类型已保证的断言类别。请使用项目自身的环境:对于uv项目,传递;对于poetry项目,传递
--mypy "uv run mypy"等。如果mypy不可用,会降级为仅检测语法类别。--mypy "poetry run mypy" - 注意:mypy扫描阶段会在测试文件旁临时写入副本(扫描结束后会删除)——因此「仅生成报告」的扫描也会触及工作树。传递
_cap_obv_shadow_*参数可进行严格的只读扫描;如果工作树不可写,扫描会降级为仅检测语法类别并告知用户。--no-types - TypeScript检测器会解析项目自身的包;如果没有tsconfig,会降级为仅检测语法类别。
typescript - 如果项目已生成覆盖率报告(或你可以低成本生成),请传递参数(支持lcov / istanbul
--coverage <file>/ coverage.pycoverage-final.json格式)。这是ICSE'19烂绿分析的动态部分:从未执行过的coverage json会被标记为已证实无用,而执行过的会被判定为假阳性并排除。这会将最嘈杂的建议类别转化为可信类别——只要有覆盖率报告就请使用该参数。conditional-assert
在删除任何内容之前,向用户展示汇总表格和检测结果。
4. Understand the two levels
4. 理解两个检测层级
- proven — cannot fail, by construction. The scripts guard the known
escape hatches (/
any,unknowncasts,as, index signatures, unchecked index access, structural!, custom assertion helpers). Safe to auto-delete.instanceof - advisory — almost certainly useless but not provable (assertion-free
tests, structural instanceof, mock-echo variants, index-signature-backed
checks, rotten-green conditional asserts, unawaited async assertions). The
script never auto-deletes these, but it records exactly why each is
uncertain, plus a hint (
deletable= usually a deletion,aggressive= usually needs a rewrite). That reason is a question you are equipped to answer against the surrounding code — so advisories are adjudicated by you (step 6), not dumped on the user.report-only
See for the full category catalog and the reasoning
behind each guard.
references/detectors.md- 已证实——本质上不可能失败。脚本会处理已知的例外情况(/
any类型、unknown类型转换、as非空断言、索引签名、未检查的索引访问、结构化!、自定义断言助手)。可安全自动删除。instanceof - 建议性——几乎可以肯定无用,但无法被证实(无断言测试、结构化instanceof、模拟重复变体、基于索引签名的检查、烂绿条件断言、未等待的异步断言)。脚本永远不会自动删除这类测试,但会记录每个测试被标记的确切原因,以及一个提示(
deletable=通常可删除,aggressive=通常需要重写)。该原因是一个需要你结合周边代码来回答的问题——因此建议性检测结果由你裁决(步骤6),而非直接推给用户。report-only
详见获取完整的类别目录及每个限制背后的推理。
references/detectors.md5. Fix the proven tier (deterministic)
5. 修复已证实层级(确定性操作)
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --fix
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --fixPlain removes only the proven findings — no judgment required, no
LLM. This is the safe deterministic core; run it first.
--fixbash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --fix
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --fix单纯使用参数只会移除已证实的检测结果——无需判断,无需大语言模型。这是安全的确定性核心;请先执行此步骤。
--fix6. Adjudicate the advisory tier (you decide, then confirm)
6. 裁决建议性层级(由你决定,然后确认)
Advisories are the cases determinism can't settle — and that's your job, not
a report line for the user. Do not just forward the list. For each advisory
finding:
- Read the test and the code it exercises. The finding's field is a pointed question — e.g. "structural instanceof — a shaped non-instance could sneak in" → check whether anything actually constructs a non-instance of that type; "mock-echo, indirect" → check whether a real code path runs between stub and assert.
reason - Decide one of: delete (the doubt doesn't hold — it really is useless),
keep (the doubt holds — it's a real check), or rewrite (the intent
is valid but the assertion is broken). Rewrite is the advisory tier's real
value: fix the unawaited (
.rejectsit), narrow aawaitto the specific type, repair a rotten-greenpytest.raises(Exception)so it actually runs. Noteconditional-assertfindings are smoke tests — legitimate by design (ICSE'19); default to keep unless the test clearly meant to assert something and forgot.no-assert - Propose before acting. Present a compact per-item table — finding, verdict, one-line rationale, and the exact edit for rewrites — and apply only what the user approves. Never auto-delete or auto-rewrite an advisory.
For a large advisory set, delegate the per-item code reads to a Sonnet
subagent (batch the findings; have it return verdict + rationale + proposed
edit per item) and keep the final proposal/synthesis here — don't burn the main
loop reading files one by one. The proven tier is never handed to a subagent;
it's already decided.
建议性检测结果是确定性无法解决的情况——这是你的工作,而非给用户的报告条目。请勿直接转发列表。对于每个建议性检测结果:
- 阅读测试及其测试的代码。检测结果的字段是一个针对性问题——例如「结构化instanceof——符合结构的非实例对象可能混入」→检查是否真的存在构造该类型非实例对象的情况;「间接模拟重复」→检查在存根和断言之间是否存在实际的代码路径。
reason - 决定以下操作之一:删除(疑虑不成立——测试确实无用)、保留(疑虑成立——这是有效的检查)或重写(意图合理但断言存在问题)。重写是建议性层级的核心价值:修复未等待的(添加
.rejects)、将await缩小到特定类型、修复烂绿pytest.raises(Exception)使其实际执行。注意conditional-assert检测结果是冒烟测试——设计上是合法的(ICSE'19);除非测试明显本应包含断言却遗漏,否则默认选择保留。no-assert - 先提议再操作。呈现一个简洁的逐项表格——检测结果、裁决、一行理由,以及重写的具体编辑内容——仅应用用户批准的操作。切勿自动删除或重写建议性检测结果。
如果建议性检测结果数量较多,可将逐项代码阅读任务委托给Sonnet子代理(批量处理检测结果;让其返回每个条目的裁决+理由+提议编辑内容),并在此处保留最终的提议/综合结果——不要在主循环中逐个阅读文件浪费时间。已证实层级永远不会交给子代理——其结果已确定。
7. Clean the residue
7. 清理残留内容
The scripts delete whole test blocks or individual assertion lines. That can
leave behind: unused imports/variables ( will flag them),
empty blocks, empty test classes, orphaned fixtures/mocks. Fix
those by hand — the typechecker output is your worklist.
noUnusedLocalsdescribe()脚本会删除整个测试块或单个断言行。这可能会留下:未使用的导入/变量(会标记它们)、空的块、空测试类、孤立的fixture/模拟对象。手动修复这些问题——类型检查器的输出就是你的任务清单。
noUnusedLocalsdescribe()8. Verify
8. 验证
Run the project's typecheck AND full test suite ( + the test
command from package.json / ). Everything must pass with the same
result as before (minus the deleted tests). If anything regresses,
and report what happened instead of pushing through.
tsc --noEmitpytestgit checkout -- <files>运行项目的类型检查和完整测试套件( + package.json中的测试命令 / )。所有内容必须通过,结果与之前一致(除了已删除的测试)。如果出现任何回归问题,执行并报告问题,而非强行推进。
tsc --noEmitpytestgit checkout -- <files>9. Report
9. 报告
Tell the user: proven tests/assertions removed (per-category counts, lines
saved), the advisory verdicts you applied (deleted / rewritten, with the fix),
and anything you chose to keep with the reason the doubt held — that last
group is the tool earning trust, not failing.
告知用户:已移除的已证实测试/断言数量(按类别统计,节省的代码行数)、你应用的建议性结果裁决(删除/重写及修复内容),以及你选择保留的测试及其疑虑成立的理由——最后一组内容是工具建立信任的关键,而非失败的表现。
What NOT to flag (the scripts already know, but so should you)
无需标记的情况(脚本已处理,但你也应了解)
- on
toBeDefined()/.find()results — the type isMap.get(), the check is real.T | undefined - Enum/constant contract locks () — they catch renumbering.
expect(ExitCode.OK).toBe(0) - Assertions on values read from files/APIs at test time — real regression tests.
- Tests asserting via custom helpers (,
expectAllow(x)).self._check(...) - "Must not raise" contract tests for fail-open code paths.
- 对/
.find()结果使用Map.get()——其类型为toBeDefined(),该检查是有效的。T | undefined - 枚举/常量契约锁定()——它们能捕获编号变更。
expect(ExitCode.OK).toBe(0) - 对测试时从文件/API读取的值进行断言——这是有效的回归测试。
- 通过自定义助手进行断言(、
expectAllow(x))。self._check(...) - 针对故障开放代码路径的「不得抛出异常」契约测试。
When NOT to run this at all
绝对不要运行此工具的场景
- Mid red-green. During TDD a test is supposed to be failing, and a freshly-written test may not have its assertion yet. This is post-hoc cleanup — run it once the suite is green, never between red and green.
- On a branch under review. Scan () is fine;
--jsonis not. Rewriting test files while a reviewer or a merge gate is reading the diff invalidates what they reviewed.--fix - As a coverage or CI-time optimizer. It deletes tests that cannot fail, which is a correctness argument, not a speed one. "CI is slow" is not a reason to reach for it — a slow suite full of real tests stays slow.
- 红绿测试中期。在TDD过程中,测试本应失败,刚编写的测试可能还没有断言。这是事后清理工具——仅当测试套件全部通过后再运行,切勿在红态和绿态之间运行。
- 在待审核的分支上。扫描()是可行的;
--json不可行。在审核者或合并网关查看差异时重写测试文件会使他们的审核内容失效。--fix - 作为覆盖率或CI时间优化工具。它删除的是不会失败的测试,这是正确性层面的考量,而非速度层面。「CI速度慢」不是使用该工具的理由——充满有效测试的慢套件依然会很慢。",