fix(git): push-guard — resolve re-review blockers; mutation table now generated
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Authored by installer-7; committed by mos-claude (no credential for this remote). BLOCKER 1 — verify-clean-clone.sh had B1's own defect: it copied WORKING-TREE files into a scratch repo and asserted the SCRATCH index, so a stale local exec bit was laundered in and it reported success while the committed artifact was still 100644. v2 reads mode from 'git ls-tree' of the SOURCE COMMIT and runs from 'git clone --no-local --no-hardlinks' of that commit; there is no 'cp' in the file. Proven by A/B against one laundered repo: v1 EXIT=0, v2 EXIT=1 (both observed, not asserted). New test-verify-clean-clone.sh 6/6, incl. a fixture that first proves it really is git=100644 disk=755 before testing it. BLOCKER 2 — empty-merge needle + non-empty-merge control, both asserting on OUTPUT; the fixture first proves it IS a merge with a tree identical to both parents. The 'non-blocking' README item was not documentation: regenerating its mutation table (now generated by mutate-push-guard.sh, not hand-numbered) found TWO genuinely surviving mutants against a 46/46 green suite — staged-but-uncommitted opt-out honoured, and unparseable committed config ignored. Both still exit 6 under mutation because control falls to a SIBLING refusal, so an exit-code-only assertion would have been satisfied by the wrong branch. It also found a live instance of the substring-anchor defect already in the suite: three branches print 'OPT-OUT IS NOT REVIEWABLE', so deleting one let the needle RE-POINT to a sibling instead of failing. Re-anchored. Suite 44 -> 46. Verifier 6/6. 13 mutants, 0 survived. Also: README reformatted to satisfy 'pnpm format:check' (prettier), which was failing CI at both prior heads — a gate neither author nor reviewer had exercised. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YKj59Qadrb2WBLaePvkM7H
This commit is contained in:
co-authored by
Claude Opus 5
parent
b0fb208b89
commit
8310075d33
@@ -508,8 +508,44 @@ R="$(new_repo e2)"
|
||||
write_config_untracked "$R" '{"json_check": "none", "reason": "local unreviewed bypass"}'
|
||||
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
||||
git -C "$R" add -f data/bad.json
|
||||
# The anchor is the DISTINGUISHING clause, not the shared headline. Three
|
||||
# separate branches print "OPT-OUT IS NOT REVIEWABLE" — untracked, staged-not-
|
||||
# committed, and symlink. Anchoring on the headline means this needle stays green
|
||||
# if the untracked branch is deleted and control falls through to a SIBLING that
|
||||
# prints the same words: a substring anchor does not fail when its subject is
|
||||
# removed, IT RE-POINTS. Anchor on the clause only this branch can produce.
|
||||
expect NEEDLE 6 "an UNTRACKED opt-out is refused as unreviewable" \
|
||||
--out "OPT-OUT IS NOT REVIEWABLE" -- \
|
||||
--out "is not tracked in git" -- \
|
||||
bash -c "cd '$R' && '$GUARD' check-staged"
|
||||
|
||||
# STAGED BUT NOT COMMITTED. Found by mutation, not by review: `if [[ -z "$cmode" ]]`
|
||||
# survived `if false` against a 44/44 green suite, because no case ever built this
|
||||
# state. Note the mutant still exits 6 — control falls to the LOCAL-ONLY branch
|
||||
# below and refuses for a different reason. An exit-code-only assertion here would
|
||||
# be satisfied by the wrong branch, which is why --out carries the distinguishing
|
||||
# clause. `git add` puts the file in the index, so ls-files matches while HEAD
|
||||
# does not: staged review is not review.
|
||||
R="$(new_repo e2b)"
|
||||
write_config_untracked "$R" '{"json_check": "none", "reason": "staged, never committed"}'
|
||||
git -C "$R" add .push-guard.json
|
||||
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
||||
git -C "$R" add -f data/bad.json
|
||||
expect NEEDLE 6 "a STAGED-but-uncommitted opt-out is refused" \
|
||||
--out "staged but not yet in HEAD" -- \
|
||||
bash -c "cd '$R' && '$GUARD' check-staged"
|
||||
|
||||
# COMMITTED CONFIG DOES NOT PARSE while the working tree opts out cleanly. Also a
|
||||
# mutation survivor. The working-tree config is valid, so the early config check
|
||||
# passes and we reach the re-read; the committed object is what fails. Without
|
||||
# this branch an opt-out could be honoured on the strength of a HEAD blob nobody
|
||||
# can actually read a decision out of.
|
||||
R="$(new_repo e2c)"
|
||||
write_config "$R" '{ this is not json'
|
||||
printf '%s\n' '{"json_check": "none", "reason": "valid here, broken in HEAD"}' > "$R/.push-guard.json"
|
||||
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
||||
git -C "$R" add -f data/bad.json
|
||||
expect NEEDLE 6 "an UNPARSEABLE committed config cannot authorise an opt-out" \
|
||||
--out "is INVALID" -- \
|
||||
bash -c "cd '$R' && '$GUARD' check-staged"
|
||||
|
||||
# A committed ON silently flipped OFF in the working tree is the same bypass.
|
||||
@@ -580,6 +616,44 @@ expect CONTROL 0 "a NON-empty root commit still pushes" \
|
||||
--out "non-empty root commit" -- \
|
||||
bash -c "cd '$R' && '$GUARD' push --remote origin --branch fresh2"
|
||||
|
||||
# THE MERGE BRANCH HAD NO NEEDLE AT ALL. It was defined, hand-verified, and
|
||||
# shipped -- and deleting the refusal left the suite at 41/41 green. Defining
|
||||
# semantics is not testing them, and an untested branch is indistinguishable
|
||||
# from an absent one to everyone downstream.
|
||||
# EMPTY MERGE: two branches that each change nothing, merged. The merge tree is
|
||||
# then identical to EVERY parent -- it integrates nothing and introduces nothing.
|
||||
R="$(new_repo e9)"
|
||||
git -C "$R" checkout -q -b mx
|
||||
git -C "$R" commit -q --allow-empty -m "x: no tree change"
|
||||
git -C "$R" checkout -q -b my HEAD~1 2>/dev/null || git -C "$R" checkout -q -b my
|
||||
git -C "$R" commit -q --allow-empty -m "y: no tree change"
|
||||
git -C "$R" checkout -q mx
|
||||
git -C "$R" merge -q --no-ff --no-edit my
|
||||
# PROVE THE FIXTURE IS ACTUALLY AN EMPTY MERGE before asserting on it, or the
|
||||
# needle passes for the wrong reason on a repo that never made a merge at all.
|
||||
np="$(git -C "$R" rev-list --parents -n 1 HEAD | wc -w)"
|
||||
if (( np > 2 )) && git -C "$R" diff --quiet HEAD^1 HEAD && git -C "$R" diff --quiet HEAD^2 HEAD; then
|
||||
printf ' ok [%-14s] fixture is a merge (%d fields) with tree identical to both parents\n' "e9-fixture" "$np"; PASS=$(( PASS + 1 ))
|
||||
else
|
||||
printf ' FAIL [%-14s] fixture is not an empty merge -- needle would be vacuous\n' "e9-fixture"; FAIL=$(( FAIL + 1 ))
|
||||
fi
|
||||
expect NEEDLE 5 "an EMPTY MERGE is refused, not exempted" \
|
||||
--out "EMPTY MERGE" -- \
|
||||
bash -c "cd '$R' && '$GUARD' push --remote origin --branch mx"
|
||||
|
||||
# CONTROL: a merge that really integrates must still push. Both sides add a
|
||||
# distinct file, so the merge tree differs from BOTH parents.
|
||||
R="$(new_repo e10)"
|
||||
git -C "$R" checkout -q -b nx
|
||||
printf 'from x\n' > "$R/x.txt"; git -C "$R" add x.txt; git -C "$R" commit -q -m "x adds a file"
|
||||
git -C "$R" checkout -q -b ny HEAD~1
|
||||
printf 'from y\n' > "$R/y.txt"; git -C "$R" add y.txt; git -C "$R" commit -q -m "y adds a file"
|
||||
git -C "$R" checkout -q nx
|
||||
git -C "$R" merge -q --no-ff --no-edit ny
|
||||
expect CONTROL 0 "a NON-empty merge commit still pushes" \
|
||||
--out "non-empty merge commit" -- \
|
||||
bash -c "cd '$R' && '$GUARD' push --remote origin --branch nx"
|
||||
|
||||
echo
|
||||
printf 'push-guard needles: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
(( FAIL == 0 )) || exit 1
|
||||
|
||||
Reference in New Issue
Block a user