Conversation
…id-merge) `git ls-files <pattern>` lists an UNMERGED path once per index stage (1=base, 2=ours, 3=theirs). verify_test_suite_count.py counted output LINES, so during an in-progress merge a single conflicted `tests/test_*.py` was counted two or three times, `--regenerate` wrote that inflated number into tests/CLAUDE.md, and only the pre-push gate caught it. The conflict sweep had to correct it twice (PRs #710, #711). Repro (hermetic temp repo, one conflicted + one clean python suite): raw `git ls-files 'tests/test_*.py'` emits 4 lines for 2 files, and the unfixed `--regenerate` wrote `**Python (4 suites)**:`. Fix: list_git_files() collects paths into a set. Deduplicating in Python rather than via `git ls-files --deduplicate` (git >= 2.31; verified supported on the local git 2.54 but not portable to every builder) keeps this correct on any git AND additionally fixes cross-pattern double counting -- the shell family is derived from three globs, and `tests/test_x.test.sh` matches both `tests/*.test.sh` and `tests/test_*.sh`. Merge-in-progress decision: WARN, do not refuse. A refusal was considered, but conflict resolution is exactly when counts drift and exactly when the sweep needs `--regenerate` (the merge automation calls it there), so refusing would break the workflow that needs the tool most. The actual hazard was a SILENT wrong number; per-unique-path derivation removes the wrongness, and a loud stderr `[WARN]` naming MERGE_HEAD removes the silence. Fail-closed guards are untouched: the exactly-one count-line assertion and the per-family vacuous-zero guard both still fail closed (covered by a regression test). Also regenerates a pre-existing Python count drift on main (238 -> 239) so the gate is clean. Tests: 8 new cases in TestUnmergedStageDeduplication. Red first -- 4 failed / 4 passed before the fix; 48/48 pass after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The finding
git ls-files <pattern>lists an unmerged path once per index stage (1=base, 2=ours, 3=theirs).tools/verify_test_suite_count.pycounted git's output lines, so during an in-progress merge a single conflictedtests/test_*.pywas counted two or three times.--regeneratethen wrote that inflated number intotests/CLAUDE.md, and only the pre-push gate caught it — the conflict sweep had to correct it twice (PRs #710, #711).Repro (red first)
Hermetic temp repo, one conflicted + one clean Python suite, merge left in progress:
Against the unfixed tool:
--regeneratewrote**Python (4 suites)**:and--checkreportedactual is 4.TestUnmergedStageDeduplicationbefore the fix: 4 failed / 4 passed. After: 48/48 pass across both suite-count test files.Fix
list_git_files()collects paths into a set;count_git_files()returnslen()of it.Dedupe mechanism chosen: Python-side set, not
git ls-files --deduplicate.--deduplicatewas verified working on the git here (2.54.0) but it needs git >= 2.31, and — more importantly — it only fixes the stage case. The set form also fixes a second latent double-count: the shell family is derived from three globs and one path can match two of them (tests/test_x.test.shmatches bothtests/*.test.shandtests/test_*.sh). That is covered by its own test.Line parsing moved from
stdout.strip().split("\n")tosplitlines()so a path is never mangled by stripping.Should
--regeneraterefuse while a merge is in progress?Decision: WARN loudly, do not refuse.
Refusal was the tempting answer — a derived count from a half-merged tree is arguably meaningless. But conflict resolution is precisely when suite counts drift and precisely when the sweep must run
--regenerate; the merge automation calls the tool in exactly that window, so a refusal would break the workflow that needs this tool most, and would push operators toward editing the count by hand.The actual failure mode was not "ran during a merge" — it was a silently wrong number. Per-unique-path derivation removes the wrongness, and a loud stderr
[WARN]namingMERGE_HEADremoves the silence, telling the operator the tree is only half resolved and to re-run after the merge concludes. Both--checkand--regeneratewarn; exit codes are unchanged.Guards preserved
No weakening. The exactly-one count-line assertion, the homoglyph/fence/HTML-comment masking, the not-a-git-work-tree check, the git-failure fail-closed, and the per-family vacuous-zero guard all still fail closed — the last has an explicit regression test in the new class (
test_vacuous_zero_guard_still_fails_closed). The merge probe itself can never break the gate: it ischeck=Falseand swallows toFalse, sinceensure_git_repo()already fails closed on an unusable git.Sibling tools checked (no action needed)
tools/verify_test_coverage.pyandtools/ci_shard_runner.pyboth already collectgit ls-filesoutput into aset(), so neither is exposed to this.verify_test_suite_count.pywas the only one counting raw lines.Also in this PR
Regenerates a pre-existing Python count drift on
main(238 -> 239) so the gate is clean, plus theINDEX:docstring line and the resultingtools/INDEX.mdregeneration.Verification
pytest tests/test_verify_test_suite_count.py tests/test_verify_test_suite_count_prepush.py-> 48 passedtest_ci_workflow_lint,test_test_hygiene,test_no_bare_test_functions,test_cli_help_hygiene,test_subprocess_guard,test_traps) -> 131 passedtools/ci_shard_runner.py 0 1secret_scan.py --staged-> exit 0 (CLEAN: 4 files scanned)encoding_lint.py --check,import_resolution_check.py,claudemd_lint.py,claudemd_sync_gate.py --check,verify_test_coverage.py --check-> all exit 0bash hooks/pre-push-policy.sh --test-> 18/18 passedverify_test_suite_count.py --check->[OK] Test suite counts match🤖 Generated with Claude Code