Skip to content

check-step-collectors cannot see .claude/hooks/*.selftest.sh collectors — a second self-test family it is structurally blind to #11801

Description

@os-steve

Found while wiring #11514 (PR #11799). Filing rather than widening the gate in that PR: its declared surface is .github/workflows/lint.yml alone, and this is a scripts/ change.

Measured

check-step-collectors.mjs decides what to judge from two regexes:

/** A repo script path carrying `--self-test` somewhere after it on the same command. */
const SELF_TEST_TARGET = /(?:^|\s)((?:\.\/)?(?:scripts|packages)\/[\w./-]+\.(?:mjs|mts|cjs|js|sh|ts))(?=\s)[^\n]*?\s--self-test\b/;

A step is only considered once selfTestTargets(step.run).length >= 2, so its population is scripts/|packages/ paths carrying a --self-test flag. The matrices in .claude/hooks/ are neither: they live under .claude/, and they are invoked as bare executables (.claude/hooks/guard-shared-stash.selftest.sh), with no flag.

Confirmed on the PR #11799 tree, which adds a real third collector to lint.yml:

✓ check-step-collectors: 329 `run:` steps across 26 workflow(s); 2 step(s) run 2+ independent self-tests, all of them through a collector.

Still 2, with three collectors in the file.

Why this is the gate's own defect class

#10814 exists because a run: block is bash -e, so a bare sequence of independent self-tests aborts at the first failure and leaves the rest unrun — neither green nor red. The new step runs two independent hermetic matrices and is written as a tolerate-and-collect block for exactly that reason; the measurement is in PR #11799 (with the first matrix mutated red, the second still runs). But nothing enforces it. Someone "simplifying" that block into

for t in "${selftests[@]}"; do "$t"; done

restores the #10814 defect verbatim, and every gate in the repo stays green. The step therefore carries a ⛔ comment asking reviewers not to do that — prose enforcement, which is the state #10814 was filed to leave behind.

That the gate's own header says its sweep found "exactly two" matches is not a defence: it was true when written and it is what makes the blind spot invisible now — a count that stays right while its subject grows.

Options

  1. Widen SELF_TEST_TARGET's path alternation to include .claude/ and make the --self-test flag optional for a path matching *.selftest.sh (self-testing is in the filename there). Note driveBlock's SCRIPT_TOKEN stubbing needs the same widening, or a .claude/ collector reaches it and throws no repo script path in command.
  2. Key the population on "≥2 independent self-tests" in a spelling-independent way — a path ending .selftest.sh, or a --self-test flag, either counting.
  3. Accept the blind spot and record it. Not recommended: this is the gate whose whole argument is that a static shape cannot vouch for itself.

Option 1 or 2 should keep the gate's --self-test harness driving the real block, since a collector that swallows its exit code is worse than the masking it replaces.

Note on --self-test reach

The .claude/hooks/ matrices are not the only family outside this gate's population — scripts/bump-objectui.selftest.sh uses the same *.selftest.sh naming and is likewise unmatched by the --self-test flag requirement. Whether that one is run anywhere is a separate question and is not asserted here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions