ADLC
Toolkit

hollow-test

Diff-scoped mutation gate. Mutates only the lines your change touched and fails if any mutant survives your test suite, which proves the coverage is load-bearing.

hollow-test

ADLC phase: P3 Rail (C4) · Gate: every changed line is constrained by an assertion, so no mutant survives.

The ADLC lifecycle, with P3 highlighted. Read the theory for P3

What it defends against

F5: Reward hacking (theory ↗)

Executed lines are not tested lines. A suite can run every changed statement and still assert nothing about its behavior. That is hollow coverage, and a reward-seeking model will happily produce it. hollow-test mutates only the lines in your diff (invert a comparison, flip a boolean, null a return), reruns the suite against each mutant, and fails if any survives. A survivor is proof the line's behavior is unconstrained. Diff-scoping keeps the run in seconds-to-minutes instead of the hours whole-codebase mutation testing costs.

Usage

adlc hollow-test --test-cmd "node --test test/" [--base <ref>] [--max <n>] [--timeout-ms <n>] [--target <file>] [--rails <ticket-file>] [--json]
FlagDescription
--test-cmd <cmd>(required) Shell command to run the suite; must exit non-zero on failure.
--base <ref>Git base ref for the diff. Defaults to the merge-base with a trunk; fails closed if none resolves.
--max <n>Max mutants across all files (default 20), spread round-robin.
--timeout-ms <n>Per-mutant test-command timeout (default 120000).
--target <file>Mutate this file whole, independent of the diff (repeatable). For rails-authoring / characterization tickets where the behavior file itself did not change.
--rails <ticket-file>Expand a ticket's declared rails globs against tracked files and mutate them (repeatable).
--jsonMachine-readable output for orchestrators.
--helpShow usage and exit 0.

Refuses to run on a dirty working tree, and requires a green baseline before mutating so a failing suite cannot masquerade as killing every mutant. If the baseline suite fails, hollow-test emits the test command's captured stdout and stderr to stderr before exiting, ensuring failure reasons and failing tests are diagnosable.

Exit codes

0: gate passes. Every generated mutant was killed by the suite.1: operational error. Dirty tree, not a git repo, bad arguments, non-green baseline, nothing eligible to mutate with no --target/--rails, or a selected file received no mutation budget (see below).2: gate fails. One or more mutants survived (hollow coverage).

--max too small for the diff: a starved file fails closed

--max spreads the mutant budget round-robin across every selected file, and a small --max against a wide diff can leave one or more diff-derived files with zero quota — never mutated at all. That file's changed code is then unverified, so hollow-test refuses to report a pass: it exits 1 and names every starved file on stderr rather than silently passing on whatever other files did get budget. Raise --max to cover every selected file (one per file changed, at minimum), or narrow the diff. This does not apply to a file that received budget but has no mutable line at all (a comment-only or import-only change) — that case still only warns, since the file genuinely has nothing to mutate rather than having been skipped for lack of budget.

Comment-only changes are reported as not covered

A changed file whose change leaves its program untouched — only comments or blank lines added, reworded or removed — has no behaviour to mutate. It is not failed; it is listed on stdout as not covered, and when it is the only change the run exits 0 with comment-only diff — no changed behaviour to mutate. With --json the same files appear under "skipped": { "commentOnly": [...] }, on that early exit and in the normal report alike.

Both sides of the diff are judged, so these are not comment-only and stay mutation targets (failing closed when nothing mutable remains):

  • a code line deleted and replaced by a comment or a blank line;
  • live code wrapped in an added /* … */;
  • a deleted */ that turns the code after it into comment text;
  • a // ... line inside a multi-line template literal, which is string data.

Any doubt — an unreadable file, an old side that cannot be read from --base, a scan that loses its place — keeps the file a target.

Example

$ adlc hollow-test --test-cmd "node --test test/" --base main
src/calc.mjs:7  null-return   survived   return a + b;  →  return null;
hollow coverage — 1 mutation(s) pass your tests
exit 2

Go deeper

On this page