captain-obvious

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Captain 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
scripts/
. 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.
该工具会删除那些断言已被编译器、模拟框架或逻辑定律所保证内容的测试。这类测试会消耗CI时间、虚增覆盖率可信度,且永远无法捕获回归问题。它们是AI生成测试套件的典型特征(实证研究发现,38%-100%的大语言模型生成测试存在测试异味)。
核心工作由
scripts/
目录下的两个确定性脚本完成。你的任务是协调流程:运行脚本、解读报告、清理残留内容,并验证未出现问题。请勿手动扫描测试文件或为每个文件启动子代理——一次脚本调用即可扫描整个项目。

Workflow

工作流程

1. Detect the stack(s)

1. 检测技术栈

  • TypeScript: a
    tsconfig.json
    and
    *.test.ts
    /
    *.spec.ts
    /
    __tests__
    files.
  • Python:
    test_*.py
    /
    *_test.py
    files (pytest).
  • A repo can have both; run both detectors.
  • TypeScript:项目需包含
    tsconfig.json
    以及
    *.test.ts
    /
    *.spec.ts
    /
    __tests__
    文件。
  • Python:项目需包含
    test_*.py
    /
    *_test.py
    文件(基于pytest)。
  • 仓库可同时包含两种技术栈;需运行对应的两个检测器。

2. Safety first

2. 安全优先

The fix step edits test files in place. Both scripts enforce this themselves:
--fix
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
--force
--force
removes the only undo path there is (
git checkout -- <files>
), so use it only when the user has explicitly accepted that.
Trust boundary: scanning executes the project's own toolchain. mypy loads
[tool.mypy] plugins
from the repo's config as in-process Python;
--mypy "uv run mypy"
/
"poetry run mypy"
resolve (and can run) the repo's dependencies; the TS side loads the repo's own
typescript
package. Run the scan only on repositories you would be willing to run
mypy
/
tsc
in yourself.
Note: 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
references/prevention.md
). Both scanners also support
--file <path> [--stdin]
for a syntactic-only single-file scan (JSON to stdout; no mypy/tsc, no side effects).
修复步骤会直接编辑测试文件。两个脚本自身都有安全限制:
--fix
参数仅当目标是git仓库且工作树干净时才会执行(未跟踪文件不受影响),否则会以状态码2退出。如果脚本拒绝执行,请先暂存或提交更改,而非使用
--force
参数——
--force
会移除唯一的撤销路径(
git checkout -- <files>
),仅当用户明确接受该风险时才可使用。
信任边界:扫描过程会执行项目自身的工具链。mypy会从仓库配置中加载
[tool.mypy] plugins
并作为进程内Python代码运行;
--mypy "uv run mypy"
/
"poetry run mypy"
会解析(并可能运行)仓库的依赖;TypeScript端会加载仓库自身的
typescript
包。仅在你愿意自行运行
mypy
/
tsc
的仓库中执行扫描。
注意:当作为插件安装时,写入时的PreToolUse钩子可能会生效——如果在清理重写过程中,测试文件的写入/编辑操作因「captain-obvious:」原因被拒绝,请修复标记的断言,而非重试相同内容(详见
references/prevention.md
)。两个检测器还支持
--file <path> [--stdin]
参数,用于仅做语法分析的单文件扫描(结果以JSON输出到标准输出;不调用mypy/tsc,无副作用)。

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
    --mypy "uv run mypy"
    for uv projects,
    --mypy "poetry run mypy"
    for poetry, etc. If mypy isn't available it degrades gracefully to the syntactic categories.
  • Note: the mypy pass briefly writes
    _cap_obv_shadow_*
    copies next to test files (removed when the run ends) — so a "report-only" scan does touch the working tree. Pass
    --no-types
    for a strictly read-only scan; if the tree is not writable the scan degrades to syntactic categories and says so.
  • The TS detector resolves the project's own
    typescript
    package; without a tsconfig it degrades to syntactic categories.
  • If the project already produces coverage (or you can cheaply run it), pass
    --coverage <file>
    (lcov / istanbul
    coverage-final.json
    / coverage.py
    coverage json
    ). This is the dynamic half of the ICSE'19 rotten-green analysis: a
    conditional-assert
    whose 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.
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项目,传递
    --mypy "uv run mypy"
    ;对于poetry项目,传递
    --mypy "poetry run mypy"
    等。如果mypy不可用,会降级为仅检测语法类别。
  • 注意:mypy扫描阶段会在测试文件旁临时写入
    _cap_obv_shadow_*
    副本(扫描结束后会删除)——因此「仅生成报告」的扫描也会触及工作树。传递
    --no-types
    参数可进行严格的只读扫描;如果工作树不可写,扫描会降级为仅检测语法类别并告知用户。
  • TypeScript检测器会解析项目自身的
    typescript
    包;如果没有tsconfig,会降级为仅检测语法类别。
  • 如果项目已生成覆盖率报告(或你可以低成本生成),请传递
    --coverage <file>
    参数(支持lcov / istanbul
    coverage-final.json
    / coverage.py
    coverage json
    格式)。这是ICSE'19烂绿分析的动态部分:从未执行过的
    conditional-assert
    会被标记为已证实无用,而执行过的会被判定为假阳性并排除。这会将最嘈杂的建议类别转化为可信类别——只要有覆盖率报告就请使用该参数。
在删除任何内容之前,向用户展示汇总表格和检测结果。

4. Understand the two levels

4. 理解两个检测层级

  • proven — cannot fail, by construction. The scripts guard the known escape hatches (
    any
    /
    unknown
    ,
    as
    casts,
    !
    , index signatures, unchecked index access, structural
    instanceof
    , custom assertion helpers). Safe to auto-delete.
  • 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
    deletable
    hint (
    aggressive
    = usually a deletion,
    report-only
    = 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.
See
references/detectors.md
for the full category catalog and the reasoning behind each guard.
  • 已证实——本质上不可能失败。脚本会处理已知的例外情况(
    any
    /
    unknown
    类型、
    as
    类型转换、
    !
    非空断言、索引签名、未检查的索引访问、结构化
    instanceof
    、自定义断言助手)。可安全自动删除。
  • 建议性——几乎可以肯定无用,但无法被证实(无断言测试、结构化instanceof、模拟重复变体、基于索引签名的检查、烂绿条件断言、未等待的异步断言)。脚本永远不会自动删除这类测试,但会记录每个测试被标记的确切原因,以及一个
    deletable
    提示(
    aggressive
    =通常可删除,
    report-only
    =通常需要重写)。该原因是一个需要你结合周边代码来回答的问题——因此建议性检测结果由你裁决(步骤6),而非直接推给用户。
详见
references/detectors.md
获取完整的类别目录及每个限制背后的推理。

5. 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> --fix
Plain
--fix
removes only the proven findings — no judgment required, no LLM. This is the safe deterministic core; run it first.
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --fix
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --fix
单纯使用
--fix
参数只会移除已证实的检测结果——无需判断,无需大语言模型。这是安全的确定性核心;请先执行此步骤。

6. 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:
  1. Read the test and the code it exercises. The finding's
    reason
    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.
  2. 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
    .rejects
    (
    await
    it), narrow a
    pytest.raises(Exception)
    to the specific type, repair a rotten-green
    conditional-assert
    so it actually runs. Note
    no-assert
    findings are smoke tests — legitimate by design (ICSE'19); default to keep unless the test clearly meant to assert something and forgot.
  3. 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.
建议性检测结果是确定性无法解决的情况——这是你的工作,而非给用户的报告条目。请勿直接转发列表。对于每个建议性检测结果:
  1. 阅读测试及其测试的代码。检测结果的
    reason
    字段是一个针对性问题——例如「结构化instanceof——符合结构的非实例对象可能混入」→检查是否真的存在构造该类型非实例对象的情况;「间接模拟重复」→检查在存根和断言之间是否存在实际的代码路径。
  2. 决定以下操作之一:删除(疑虑不成立——测试确实无用)、保留(疑虑成立——这是有效的检查)或重写(意图合理但断言存在问题)。重写是建议性层级的核心价值:修复未等待的
    .rejects
    (添加
    await
    )、将
    pytest.raises(Exception)
    缩小到特定类型、修复烂绿
    conditional-assert
    使其实际执行。注意
    no-assert
    检测结果是冒烟测试——设计上是合法的(ICSE'19);除非测试明显本应包含断言却遗漏,否则默认选择保留
  3. 先提议再操作。呈现一个简洁的逐项表格——检测结果、裁决、一行理由,以及重写的具体编辑内容——仅应用用户批准的操作。切勿自动删除或重写建议性检测结果。
如果建议性检测结果数量较多,可将逐项代码阅读任务委托给Sonnet子代理(批量处理检测结果;让其返回每个条目的裁决+理由+提议编辑内容),并在此处保留最终的提议/综合结果——不要在主循环中逐个阅读文件浪费时间。已证实层级永远不会交给子代理——其结果已确定。

7. Clean the residue

7. 清理残留内容

The scripts delete whole test blocks or individual assertion lines. That can leave behind: unused imports/variables (
noUnusedLocals
will flag them), empty
describe()
blocks, empty test classes, orphaned fixtures/mocks. Fix those by hand — the typechecker output is your worklist.
脚本会删除整个测试块或单个断言行。这可能会留下:未使用的导入/变量(
noUnusedLocals
会标记它们)、空的
describe()
块、空测试类、孤立的fixture/模拟对象。手动修复这些问题——类型检查器的输出就是你的任务清单。

8. Verify

8. 验证

Run the project's typecheck AND full test suite (
tsc --noEmit
+ the test command from package.json /
pytest
). Everything must pass with the same result as before (minus the deleted tests). If anything regresses,
git checkout -- <files>
and report what happened instead of pushing through.
运行项目的类型检查和完整测试套件(
tsc --noEmit
+ package.json中的测试命令 /
pytest
)。所有内容必须通过,结果与之前一致(除了已删除的测试)。如果出现任何回归问题,执行
git 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)

无需标记的情况(脚本已处理,但你也应了解)

  • toBeDefined()
    on
    .find()
    /
    Map.get()
    results — the type is
    T | undefined
    , the check is real.
  • Enum/constant contract locks (
    expect(ExitCode.OK).toBe(0)
    ) — they catch renumbering.
  • 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 (
    --json
    ) is fine;
    --fix
    is not. Rewriting test files while a reviewer or a merge gate is reading the diff invalidates what they reviewed.
  • 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速度慢」不是使用该工具的理由——充满有效测试的慢套件依然会很慢。",