Skip to content

test-arg-flag-values.sh listed its scripts by hand, so a new tool was untested and nothing said so (#188) - #192

Merged
fdaviddpt merged 2 commits into
mainfrom
fix/188
Aug 18, 2026
Merged

test-arg-flag-values.sh listed its scripts by hand, so a new tool was untested and nothing said so (#188)#192
fdaviddpt merged 2 commits into
mainfrom
fix/188

Conversation

@fdaviddpt

@fdaviddpt fdaviddpt commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #188.

What was wrong

tests/test-arg-flag-values.sh sweeps every tool for the missing-flag-value hang of #114. Its header said a thing added later was covered without anyone having to remember it. That was true of flags — each script's own argument loop is parsed for arms carrying shift 2 — and false of scripts, which were four hand-written drive_script calls at the bottom of the file.

So a new tool under scripts/ was silently untested by the suite whose job is to sweep every tool, and untested in the way this repository is named after: nothing errored, the totals went up as assertions were added elsewhere, and the coverage that was lost appeared in no number the suite printed. #183 added the jit-doctor.sh line and a comment naming the gap; the gap itself stayed open.

What changed

The script list is now git ls-files -- scripts, the same enumeration tests/test-dogfood-entries.sh already ran for rule coverage.

Enumeration turns "not in the list" into "must be driven", so a tool that legitimately takes no flag arguments needs an answer of its own. classify_script() gives every tracked file one of five verdicts, read out of that file's own bytes, and each of the five prints a line naming the script:

Verdict Meaning Result
drive the canonical loop, with shift 2 arms every flag driven
no-flags no loop, no sign of flag parsing anywhere named PASS
boolean-flags-only a loop, but no arm reaches for $2 named PASS
loop-no-flags a loop reaching for $2, no shift 2 arm to read FAIL
flags-elsewhere no loop, but getopts / shift 2 / a dash case arm FAIL

Today's tree: 4 driven, 6 with no flags.

There is no skip list, deliberately. The issue named the choice between an explicit skip list and a sweep that can tell "no flags to test" from "never ran". A skip list is the hand-written list again, one indirection further out, stale in the same silence — so the second was built. What it costs, stated rather than hidden: a tool parsing flags in a shape this suite cannot read is red, not skipped, and there is no opt-out short of changing the classifier.

The header comment no longer overstates itself. Both halves are read from the repository now, and it says which one was typed until when.

Proving it catches an unlisted script

Observing that the sweep passes on today's tree says nothing — today's tree is exactly what the old hand-written list already covered. So the enumeration and the classifier are driven on every run against a throwaway git repository holding five scripts this suite has never heard of, one per verdict. The enumeration finds them, the classifier sorts them, and the unlisted tool actually goes red for want of a positive control — paired with an assertion that the same fixture's refusal path passed on the same run, so the red cannot be a broken fixture reading as a working guard.

The sweep's own coverage is guarded two ways that fail differently: two floors (at least one drive and at least one no-flag verdict in the real tree) catch a classifier stuck on one answer, and an independently written cross-check — every file mentioning shift 2 anywhere must be one the sweep drove — catches a classifier that quietly loses a single script.

When the fixture cannot be built at all, the suite exits 2 and its summary line counts the skipped section. A tally identical to a clean run would be this issue's own defect one level up, inside its own fix.

Mutation evidence

Every mutation applied to a copy outside the tree:

Mutation Result
tracked_scripts() reduced to the old four-script hand list 60/64, 4 failed
has_arg_loop() always false 14/24, 10 failed
parses_flags_somehow() always false 69/70, 1 failed
loop_consumes_value() inverted either way 73/74, 1 failed
meta fixture git init forced to fail exit 2, 62/62 passed, 0 failed, 1 section(s) SKIPPED

The first row is the one that matters: reverting to the arrangement this issue is about reddens the suite.

Also in the diff

  • .claude/jit-context/paths/00-manual/tooling.md — the consequence for whoever adds the sixth tool: while [ $# -gt 0 ] is now a shape, not a habit. Body-only edit, no frontmatter change, so no index rebuild.
  • changelog.d/188.fixed.md.

Nothing under scripts/ is touched.

Checks

  • bash tests/run-all.sh — all suites passed; this suite 74/74 (was 65/65).
  • shellcheck -S warning scripts/*.sh tests/*.sh — clean.
  • python3 .oss/assemble_changelog.py --check — ok.
  • awk, gawk and mawk all agree on the has_arg_loop idiom (observed); a CRLF line still matches the loop pattern (observed). Windows and Linux legs are reasoned, not observed.

Reviewed by an Explore reviewer (no findings) and oss:auditor (two findings, both accepted and fixed in the second commit — a skipped section that could not reach the tally, and a loop-no-flags verdict that had no fixture and was wrong for an all-boolean tool).

Verified by the maintainer

The load-bearing mutation, re-run by me in a fresh worktree at origin/main with only this suite file copied in: unmutated it is 74/74 passed, 0 failed, 0 SKIPPED, exit 0. Reverting tracked_scripts() to the four-script hand list this issue is about gives 58/63 passed, 5 failed, exit 1. That is the claim that matters — a suite whose enumeration cannot be reverted without reddening is a suite that is actually enumerating.

And one accident worth reporting, because it exercised a guard nobody wrote a fixture for. My first attempt staged the tree with git archive rather than a worktree, so there was no .git and git ls-files returned nothing. The suite did not pass over an empty sweep — it failed and said so in as many words: "and this suite would have reported a clean run over an empty sweep." That is the floor doing exactly its job against a case I produced by mistake, which is better evidence than a fixture I would have built to agree with it.

The design call is right, and it is the one I could not settle when I briefed it. A skip list is the hand-written list one indirection further out, stale in the same silence. Five verdicts read from each file's own bytes is the harder build and the correct one. The cost is stated plainly in the body rather than buried: a tool parsing flags in a shape the classifier cannot read goes red, not skipped, with no opt-out short of changing the classifier. I accept that trade — a false red is debuggable in one run, and the alternative failure is a tool with flags reading as a tool with none, which is this issue verbatim.

loop-no-flags was a real bug, not just an untested arm. The auditor found the missing fixture; acting on it turned up that the verdict would have failed any all-boolean tool with the message "the parser rotted". That is the second time today an audit finding has been the thread that pulled a defect out of a diff, and it is the argument for the two-reviewer arrangement over one.

Filed rather than carried: #193 — the classifier reads bash argument-loop shapes, so a tool written in another language under scripts/ classifies as no-flags and goes quiet rather than red. Correct today only because CLAUDE.md forbids anything but bash, awk and perl there, and nothing connects the two rules.

Florian DAVID and others added 2 commits August 18, 2026 14:38
… untested and nothing said so (#188)

Its header claimed a tool added later was covered without anyone remembering.
That was true of flags -- each script's own argument loop is parsed for arms
carrying `shift 2` -- and false of scripts, which were four hand-written
drive_script() calls. A new tool under scripts/ was silently untested by the
suite that exists to sweep every tool: nothing errored, the totals went up as
assertions were added elsewhere, and the lost coverage appeared in no number the
suite printed. #183 added the jit-doctor.sh line and a comment naming the gap.

The list is now `git ls-files -- scripts`, the enumeration
tests/test-dogfood-entries.sh already ran for rule coverage. Enumeration turns
"not in the list" into "must be driven", so classify_script() gives every tracked
file one of four verdicts read out of its own bytes:

  drive            the canonical loop, with valued flags in it
  no-flags         no loop and no sign of flag parsing -- printed BY NAME, so
                   "nothing to drive here" and "never ran" are different lines
  loop-no-flags    the loop is there and nothing could be read out of it -- red
  flags-elsewhere  no loop, but getopts / shift 2 / a dash case arm -- red

No skip list, deliberately: a skip list is the hand-written list again one
indirection further out, stale in the same silence. The cost is that a tool
taking flags in an unfamiliar shape is red rather than quietly skipped.

The sweep own guarantee is driven rather than assumed, against a throwaway git
repository holding three scripts this suite has never heard of, one per verdict:
the enumeration finds them, the classifier sorts them, and the unlisted tool
actually goes red for want of a positive control -- paired with an assertion that
the same fixture refusal path PASSED, so the red cannot be a broken fixture
reading as a working guard. Two floors (at least one driven and at least one
no-flags in the real tree) and an independently written cross-check (every file
mentioning `shift 2` is one the sweep drove) catch a classifier stuck on one
answer or quietly dropping a single script.

The header no longer overstates itself: both halves are now read from the
repository, and it says which one was typed until when.

paths/00-manual/tooling.md gains the consequence for whoever adds the sixth tool
-- `while [ $# -gt 0 ]` is now a shape, not a habit. Body-only edit, no
frontmatter change, so no index rebuild.

70/70 in the suite, all suites green, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ached the tally (#188)

Two findings from the audit of the previous commit, both real.

1. `loop-no-flags` -- "the loop is there and yielded nothing" -- had no fixture
   anywhere, and worse, it was wrong for a shape that is perfectly ordinary: a
   tool whose flags are all booleans has a loop, no `shift 2` arm, and nothing
   for #114 to happen to. It would have been told its parser had rotted. Split in
   two on evidence read from the same loop -- does any arm reach for `$2`? -- so
   `boolean-flags-only` is a named pass and `loop-no-flags` keeps its FAIL for the
   case where a value IS consumed in a shape valued_flags() cannot see. Both now
   have a fixture; all five verdicts do.

2. When the throwaway git repository could not be built, the section printed a
   SKIPPED block and left PASS/FAIL untouched -- so the tally at the bottom, and
   the exit code run-all.sh reads, were byte-identical to a run where the sweep
   self-test passed. That is this issue own defect one level up, inside its own
   fix. The summary line now counts skipped sections and the suite exits 2, which
   run-all.sh already renders as "NOT a clean result".

Driven, not reasoned: forcing the fixture build to fail gives exit 2 and
"62/62 passed, 0 failed, 1 section(s) SKIPPED"; inverting loop_consumes_value()
either way reddens exactly one of the two new assertions.

74/74, all suites green, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdaviddpt
fdaviddpt merged commit eb9cf71 into main Aug 18, 2026
5 checks passed
@fdaviddpt
fdaviddpt deleted the fix/188 branch August 18, 2026 13:29
fdaviddpt pushed a commit that referenced this pull request Aug 18, 2026
…ching (#191)

A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next
PR that inserts a line above it. It rots silently: a rotted citation reads
exactly like a live one, so the reader follows it and lands on a plausible
comment rather than on an error.

Counted on main at 98386f1, outside the assembled changelog: nine citations of
that shape. Six pointed at the wrong thing, one had drifted off the block it
named, two were still right. #191 names three of the six; the sweep added here
found the other three. #185 added one of them three hours before #190 moved it.

Every citation in an enforced file now points at something greppable -- a
function name, a distinctive literal, or the issue number, which additionally
says why rather than where.

tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a
citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING
over every other tracked file; and it names the assembled changelog as
deliberately not swept, because that file is generated and a finding in it is
unactionable. The advisory half is advisory rather than enforced for one stated
reason: its only remaining finding lives in an entry held by PR #192, and
reddening a file this change may not edit is how a check gets disabled in its
first week. Widening it is one awk pattern.

The false-positive surface was costed before the check was written: over all 96
tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a
shellcheck line, an awk error and a longer basename are driven as controls that
must not match, alongside a planted citation that must.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt pushed a commit that referenced this pull request Aug 18, 2026
A reason that has been spent reads exactly like a reason that still holds --
which is the defect class this whole branch is about, in the file that exists
to refuse it. The suite printed "advisory only while #192 holds one of the
files it would flag" on every run, and #192 merged at eb9cf71.

Replaced with what is true: widening is a scope decision nobody has taken, not
a blocked one. Deliberately NOT widened here. It is unrequested scope on a pull
request already in review, and the question it settles -- whether every future
doc, template, example and jit-context entry is bound by this rule, in every
installed project's contributor path -- is a design decision this branch was
not briefed to make. Filed for the maintainer with the exact cost instead: two
awk alternations in the selectors, one line in the entry currently flagged,
and zero measured false positives on that surface across two commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt added a commit that referenced this pull request Aug 18, 2026
…ses new ones (#191) (#197)

* Cross-file line-number citations rotted silently, and nothing was watching (#191)

A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next
PR that inserts a line above it. It rots silently: a rotted citation reads
exactly like a live one, so the reader follows it and lands on a plausible
comment rather than on an error.

Counted on main at 98386f1, outside the assembled changelog: nine citations of
that shape. Six pointed at the wrong thing, one had drifted off the block it
named, two were still right. #191 names three of the six; the sweep added here
found the other three. #185 added one of them three hours before #190 moved it.

Every citation in an enforced file now points at something greppable -- a
function name, a distinctive literal, or the issue number, which additionally
says why rather than where.

tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a
citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING
over every other tracked file; and it names the assembled changelog as
deliberately not swept, because that file is generated and a finding in it is
unactionable. The advisory half is advisory rather than enforced for one stated
reason: its only remaining finding lives in an entry held by PR #192, and
reddening a file this change may not edit is how a check gets disabled in its
first week. Widening it is one awk pattern.

The false-positive surface was costed before the check was written: over all 96
tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a
shellcheck line, an awk error and a longer basename are driven as controls that
must not match, alongside a planted citation that must.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address the audit: an advisory sweep that could report coverage it never had (#191)

Four findings from the self-review, all in tests/test-line-citations.sh unless
noted:

- The advisory sweep had no floor guard on its file count, so a broken selector
  would print `Clean across 0 file(s)` and exit 0 -- byte-identical, to
  run-all.sh which reads only the exit code, to a genuinely clean tree. It now
  takes the same `< 10 -> exit 2` floor the enforced sweep already had.
- The enforced selector was `[^/]*`, so a shell file landing in a subdirectory
  one day would fall silently into the report-only bucket and stay there. Now
  `.*`, which closes the shape rather than half of it.
- A tracked path that is not a readable file was skipped in silence in both
  loops, while the header above claimed a count the sweep never reached. Both
  are counted now; an unreadable path in the enforced set fails the suite.
- The needle accepts an optional directory prefix, and no enforced file
  exercises that branch -- every real citation was bare-basename -- so a dead
  prefix branch would have read as clean. It has its own positive control now.

scripts/jit-dry-run.sh named `jit_log_name()` and `jit_row_id()` as being "in
pre-tool-hook.sh". They are defined in common.sh and only called there, which
is an anchor wrong in the same way the line numbers were. Reworded to what is
true: pre-tool-hook.sh routes every refused row through them.

The 96-basename and 10-hit figures are now pinned to 98386f1 in all three
places that carry them, rather than stated in the present tense. They were
already stale by two the moment this branch added a file, which is the exact
failure mode the change is about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Two prose findings from the re-review (#191)

- CLAUDE.md read as attributing the move to #191 itself, because the issue
  citation sat immediately after "the PR that moved it". #185 added it and #190
  moved it three hours later; the sentence now says so.
- scripts/jit-dry-run.sh: "which #7 is the reason for" -> "which exists because
  of #7". Accurate either way, clunky one way.

A third finding was argued down rather than applied. The reviewer recounted the
audit as 3 right / 1 drifted / 5 wrong and read the tooling.md citation as
correct, on the strength of that entry own sentence "The truncation is
pre-tool-hook.sh:127-144 and it is correct" -- where "it" is the truncation
BEHAVIOUR being deliberate, not the line range being accurate. The same range
in scripts/jit-dry-run.sh was judged wrong in the same review, which is the
contradiction: at 98386f1 those lines are the #182 subagent_type comment, and
the truncation is at 162-185. The count stands at 6 wrong / 1 drifted / 2 right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* README said "Two suites are the exception" and this change made it three (#191)

The test section enumerates the suites that are about this repository rather
than the hooks, and the enumeration was a count. Adding tests/test-line-citations.sh
made the sentence wrong in the quietest possible way -- a reader counting two
and finding three has no reason to think anything is missing.

Named rather than counted where it can be: the third one is called out by name
and by what it refuses, and the "assembler they are about" clause now says
which two it means.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Rebase onto 5b46095: two citations #194 wrote while this branch was open (#191)

CI caught these, and the check that caught them is the one this branch adds.
Neither is a defect in this diff -- they are in tests/test-session-markers.sh,
section J, which #194 (issue #177) added to main after this branch was cut.

- `session-start-hook.sh:32 parses the payload with jit_json_fields +
  jit_session_key` -- the line number was ALREADY WRONG on the day it was
  written: line 32 is mid-sentence in the LC_ALL paragraph, and the parse claim
  is three lines earlier. The comment names its own functions, so the number
  was carrying nothing the prose did not already carry. Dropped.
- `the comment at session-start-hook.sh:25-29` -- accurate today, and a pointer
  to a comment rather than to code, which is the shape most likely to move.
  This one wanted more than a rename: it is a claim about TWO hooks agreeing,
  so it now names the pair of functions the claim is about and says which half
  of the claim this leg proves.

The counts pinned to 98386f1 are re-pinned to 5b46095 in all three places that
carry them, and the second measurement is kept beside the first rather than
replacing it. Nine citations at 98386f1, eleven at 5b46095; 96 basenames / 10
hits / 0 false, then 98 / 12 / 0. Pinning was already the right call and the
rebase is what proved it: the pin went stale in under a day and SAID SO,
where a present-tense number would have read true and been wrong.

That interval is now the argument the suite header leads with. The citations
this check first went red on in CI were not the ones it was written for, they
were written by somebody else while it was in review, and one of the two was
born broken. A cleanup would not have caught either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The advisory half said it was blocked on #192, which has landed (#191)

A reason that has been spent reads exactly like a reason that still holds --
which is the defect class this whole branch is about, in the file that exists
to refuse it. The suite printed "advisory only while #192 holds one of the
files it would flag" on every run, and #192 merged at eb9cf71.

Replaced with what is true: widening is a scope decision nobody has taken, not
a blocked one. Deliberately NOT widened here. It is unrequested scope on a pull
request already in review, and the question it settles -- whether every future
doc, template, example and jit-context entry is bound by this rule, in every
installed project's contributor path -- is a design decision this branch was
not briefed to make. Filed for the maintainer with the exact cost instead: two
awk alternations in the selectors, one line in the entry currently flagged,
and zero measured false positives on that surface across two commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Florian DAVID <fdavid@digital-village.fr>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test-arg-flag-values.sh enumerates its scripts by hand, so a new tool is untested there and nothing says so

1 participant