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
660 lines
29 KiB
Bash
Executable File
660 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-push-guard.sh - Needle harness for push-guard.sh.
|
|
#
|
|
# Every check gets BOTH polarities:
|
|
# NEEDLE a deliberately-broken fixture that MUST trip the guard.
|
|
# CONTROL a clean fixture that MUST pass.
|
|
#
|
|
# The controls are not decoration. A guard that failed unconditionally would
|
|
# satisfy every needle and look fully covered — which is the same class of defect
|
|
# (an assertion satisfied by the null case) that push-guard.sh exists to prevent.
|
|
# A needle set without controls cannot tell "working guard" from "broken guard".
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
GUARD="$SCRIPT_DIR/push-guard.sh"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$SCRIPT_DIR/.work}"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
# Fresh repo + bare "remote" for each case, so no case can inherit another's state.
|
|
new_repo() {
|
|
local name="$1"
|
|
local dir="$WORK_DIR/$name"
|
|
rm -rf "$dir"
|
|
mkdir -p "$dir"
|
|
git init -q -b main "$dir/repo"
|
|
git init -q --bare "$dir/remote.git"
|
|
git -C "$dir/repo" remote add origin "$dir/remote.git"
|
|
git -C "$dir/repo" config user.name "Needle Harness"
|
|
git -C "$dir/repo" config user.email "[email protected]"
|
|
printf 'seed\n' > "$dir/repo/seed.txt"
|
|
git -C "$dir/repo" add seed.txt
|
|
git -C "$dir/repo" commit -q -m "seed"
|
|
printf '%s' "$dir/repo"
|
|
}
|
|
|
|
# write_config <repo> <json-text>
|
|
#
|
|
# THIS HELPER USED TO WRITE THE CONFIG UNTRACKED, and the comment that lived here
|
|
# justified it ("does not need to be staged"). That made every opt-out control in
|
|
# this file assert the FORBIDDEN provenance and pass — a control that does not
|
|
# merely test nothing, but tests the OPPOSITE of the requirement and goes green.
|
|
# The whole design is: ON from anywhere, OFF only from a committed reviewable
|
|
# artifact. A harness that opts out from an untracked file was proving the bypass
|
|
# worked. It now COMMITS, so the opt-out controls exercise the supported path.
|
|
write_config() {
|
|
printf '%s\n' "$2" > "$1/.push-guard.json"
|
|
git -C "$1" add .push-guard.json
|
|
git -C "$1" commit -q -m "push-guard config"
|
|
}
|
|
|
|
# write_config_untracked <repo> <json-text>
|
|
# Deliberately NOT committed — used only where the untracked config is the thing
|
|
# under test and the expected outcome is a REFUSAL.
|
|
write_config_untracked() {
|
|
printf '%s\n' "$2" > "$1/.push-guard.json"
|
|
}
|
|
|
|
# expect <kind> <expected_exit> <description> [--out <substring>] -- <command...>
|
|
#
|
|
# --out asserts a substring of the guard's own output. It exists because exit 0
|
|
# alone cannot distinguish "checked the files and they were fine" from "matched
|
|
# no files and had nothing to check". Two controls in an earlier revision of this
|
|
# harness were passing vacuously for exactly that reason — the pathspec silently
|
|
# matched nothing. A control that cannot fail is not a control.
|
|
expect() {
|
|
local kind="$1" want="$2" desc="$3"; shift 3
|
|
local need_out=""
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--out) need_out="$2"; shift 2 ;;
|
|
--) shift; break ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
local got=0 out
|
|
out="$("$@" 2>&1)" || got=$?
|
|
|
|
local why=""
|
|
[[ "$got" == "$want" ]] || why="wanted exit $want, got $got"
|
|
if [[ -z "$why" && -n "$need_out" && "$out" != *"$need_out"* ]]; then
|
|
why="exit $got as expected, but output never said: $need_out"
|
|
fi
|
|
|
|
if [[ -z "$why" ]]; then
|
|
printf ' PASS [%-7s] %s (exit %s)\n' "$kind" "$desc" "$got"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
printf ' FAIL [%-7s] %s — %s\n' "$kind" "$desc" "$why"
|
|
printf '%s\n' "$out" | sed 's/^/ | /'
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$WORK_DIR"
|
|
|
|
echo "=== (a) conflict markers / unmerged index ==="
|
|
|
|
# NEEDLE: staged content carrying real conflict markers.
|
|
R="$(new_repo a1)"
|
|
cat > "$R/conflicted.txt" <<'EOF'
|
|
intro line
|
|
<<<<<<< HEAD
|
|
ours
|
|
=======
|
|
theirs
|
|
>>>>>>> feature/x
|
|
tail line
|
|
EOF
|
|
git -C "$R" add conflicted.txt
|
|
expect NEEDLE 2 "staged conflict markers are refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: a genuine unmerged index (merge in progress).
|
|
R="$(new_repo a2)"
|
|
git -C "$R" checkout -q -b other
|
|
printf 'from-other\n' > "$R/clash.txt"
|
|
git -C "$R" add clash.txt && git -C "$R" commit -q -m other
|
|
git -C "$R" checkout -q main
|
|
printf 'from-main\n' > "$R/clash.txt"
|
|
git -C "$R" add clash.txt && git -C "$R" commit -q -m main
|
|
git -C "$R" merge other -q >/dev/null 2>&1 || true
|
|
expect NEEDLE 2 "unmerged index paths are refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: ordinary staged content passes.
|
|
# The config is REQUIRED as of the decision gate: this fixture has no generated
|
|
# JSON, so it records that fact with a reason. This is the migration cost of the
|
|
# hard cutover, paid here first — these two controls were the only pre-existing
|
|
# cases that reached the JSON stage without nominating a path, and they are
|
|
# exactly the "repo that never wired it up" the gate now refuses.
|
|
R="$(new_repo a3)"
|
|
write_config "$R" '{"json_check": "none", "reason": "fixture has no generated JSON"}'
|
|
printf 'just some code\n' > "$R/clean.txt"
|
|
git -C "$R" add clean.txt
|
|
expect CONTROL 0 "clean staged content passes (1 file actually scanned)" \
|
|
--out "no conflict markers in 1 staged file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL (false-positive guard): prose using ======= as an underline must NOT trip.
|
|
# This is why the marker pattern deliberately ignores a bare '======='.
|
|
R="$(new_repo a4)"
|
|
write_config "$R" '{"json_check": "none", "reason": "fixture has no generated JSON"}'
|
|
cat > "$R/README.rst" <<'EOF'
|
|
Section Title
|
|
=============
|
|
|
|
Body text.
|
|
|
|
Another Heading
|
|
=======
|
|
EOF
|
|
git -C "$R" add README.rst
|
|
expect CONTROL 0 "prose using ======= underlines does not trip the guard" \
|
|
--out "no conflict markers in 1 staged file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE (fault injection): if the scan itself ERRORS, the guard must refuse
|
|
# rather than report a clean result. git grep exits 1 for "no match" but >=2 for
|
|
# a real failure; a blanket '|| true' would collapse the two. We inject the
|
|
# failure with a PATH shim rather than trying to corrupt an index.
|
|
R="$(new_repo a5)"
|
|
SHIM="$WORK_DIR/a5/bin"
|
|
mkdir -p "$SHIM"
|
|
REAL_GIT="$(command -v git)"
|
|
cat > "$SHIM/git" <<SH
|
|
#!/usr/bin/env bash
|
|
if [[ "\$1" == "grep" ]]; then exit 2; fi
|
|
exec "$REAL_GIT" "\$@"
|
|
SH
|
|
chmod +x "$SHIM/git"
|
|
printf 'ordinary content\n' > "$R/thing.txt"
|
|
git -C "$R" add thing.txt
|
|
expect NEEDLE 2 "a conflict scan that ERRORS is refused, not reported clean" \
|
|
--out "did not run" -- \
|
|
bash -c "cd '$R' && export PATH=\"$SHIM:\$PATH\" && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: RENAME + MODIFY. Git reports a `git mv` plus a small edit as a single
|
|
# 'R' entry, which --diff-filter=ACM does not match, making the file invisible to
|
|
# every content check. A clean file is co-staged deliberately: without it the
|
|
# file list is empty and the nothing-staged guard fires by ACCIDENT, which would
|
|
# make this needle pass for the wrong reason and hide the real defect.
|
|
R="$(new_repo a6)"
|
|
mkdir -p "$R/data"
|
|
seq 1 200 | sed 's/^/line /' > "$R/data/canon.txt"
|
|
printf 'original\n' > "$R/other.txt"
|
|
git -C "$R" add -A && git -C "$R" commit -q -m seed
|
|
git -C "$R" mv data/canon.txt data/canon2.txt
|
|
printf '<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\n' >> "$R/data/canon2.txt"
|
|
printf 'an ordinary innocent change\n' > "$R/other.txt"
|
|
git -C "$R" add -A
|
|
expect NEEDLE 2 "conflict markers in a RENAMED file are refused (rename+modify)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: a path marked binary in .gitattributes. `git grep -I` honours that
|
|
# classification and skips the blob entirely while the summary still counts the
|
|
# file as scanned — a clean pass AND a false count. Hence -a, not -I.
|
|
R="$(new_repo a7)"
|
|
printf 'notes.txt binary\n' > "$R/.gitattributes"
|
|
printf 'x\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\ny\n' > "$R/notes.txt"
|
|
git -C "$R" add -A
|
|
expect NEEDLE 2 "conflict markers in a .gitattributes-binary file are refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
echo "=== (c) staged JSON parses ==="
|
|
|
|
# NEEDLE: malformed JSON, the shape a broken generator emits.
|
|
R="$(new_repo c1)"
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1,\n' > "$R/data/broken.json"
|
|
git -C "$R" add data/broken.json
|
|
expect NEEDLE 3 "staged unparseable JSON under --json-path is refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/**/*.json'"
|
|
|
|
# NEEDLE: conflict markers inside a generated JSON file — the 70-file incident.
|
|
R="$(new_repo c2)"
|
|
mkdir -p "$R/data"
|
|
cat > "$R/data/canon.json" <<'EOF'
|
|
{
|
|
<<<<<<< HEAD
|
|
"v": 1
|
|
=======
|
|
"v": 2
|
|
>>>>>>> main
|
|
}
|
|
EOF
|
|
git -C "$R" add data/canon.json
|
|
expect NEEDLE 2 "conflict markers in generated JSON are refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: valid JSON passes.
|
|
R="$(new_repo c3)"
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1, "b": [2, 3]}\n' > "$R/data/good.json"
|
|
git -C "$R" add data/good.json
|
|
expect CONTROL 0 "valid staged JSON passes (and was actually parsed)" \
|
|
--out "1 staged .json file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/**/*.json'"
|
|
|
|
# CONTROL: an explicit exemption works (and is reported, never silent).
|
|
R="$(new_repo c4)"
|
|
mkdir -p "$R/fixtures"
|
|
printf 'not json at all' > "$R/fixtures/malformed.json"
|
|
git -C "$R" add fixtures/malformed.json
|
|
expect CONTROL 0 "deliberate malformed fixture can be exempted (exemption announced)" \
|
|
--out "EXEMPTED by --allow-invalid-json" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'fixtures/*' --allow-invalid-json 'fixtures/*'"
|
|
|
|
# CONTROL (false-positive regression): JSONC is the common real-world case.
|
|
# tsconfig.json legally carries comments and does NOT parse strictly. Measured on
|
|
# a real repo: 17 of 119 tracked .json files are like this. An on-by-default
|
|
# check would fire on all of them; scoping by --json-path is what prevents it.
|
|
R="$(new_repo c5)"
|
|
cat > "$R/tsconfig.json" <<'EOF'
|
|
{
|
|
// JSONC: comments are legal here and strict json.load() rejects them.
|
|
"compilerOptions": { "strict": true }
|
|
}
|
|
EOF
|
|
mkdir -p "$R/data"
|
|
printf '{"generated": true}\n' > "$R/data/view.json"
|
|
git -C "$R" add tsconfig.json data/view.json
|
|
expect CONTROL 0 "JSONC outside --json-path does not trip the JSON check" \
|
|
--out "1 staged .json file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/**/*.json'"
|
|
|
|
# NEEDLE: the same tsconfig DOES fail if someone wrongly nominates it, proving
|
|
# the check is genuinely running and the control above is not a vacuous pass.
|
|
expect NEEDLE 3 "the same JSONC file fails when explicitly nominated" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'tsconfig.json'"
|
|
|
|
echo "=== (d) the JSON decision is MANDATORY (.push-guard.json) ==="
|
|
|
|
# NEEDLE: the fail-open this gate closes. No --json-path, no config: the previous
|
|
# release printed "JSON check NOT REQUESTED" and exited 0, leaving the repo
|
|
# unprotected forever with nothing ever failing.
|
|
R="$(new_repo d1)"
|
|
printf '{"a": 1}\n' > "$R/thing.json"
|
|
git -C "$R" add thing.json
|
|
expect NEEDLE 6 "no --json-path and no config is REFUSED (the closed fail-open)" \
|
|
--out "NO JSON DECISION" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: config nominates paths, and the check genuinely runs off it.
|
|
R="$(new_repo d2)"
|
|
write_config "$R" '{"json_paths": ["data/**/*.json"]}'
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1,\n' > "$R/data/broken.json"
|
|
git -C "$R" add data/broken.json
|
|
expect NEEDLE 3 "broken JSON is caught via config-supplied json_paths" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: opt-out WITHOUT a reason. An unexplained opt-out is the absence again,
|
|
# merely relocated into a file.
|
|
R="$(new_repo d3)"
|
|
write_config "$R" '{"json_check": "none"}'
|
|
printf 'code\n' > "$R/x.txt"
|
|
git -C "$R" add x.txt
|
|
expect NEEDLE 6 "json_check:none without a reason is refused" \
|
|
--out "REQUIRES a non-empty" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: malformed config must REFUSE, never fall back to "treat as absent".
|
|
# That fallback would mean a typo silently disables the check the file enables.
|
|
R="$(new_repo d4)"
|
|
write_config "$R" '{"json_paths": ["data/**/*.json",'
|
|
printf 'code\n' > "$R/x.txt"
|
|
git -C "$R" add x.txt
|
|
expect NEEDLE 6 "an unparseable config is refused, not treated as absent" \
|
|
--out "INVALID" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: a config that parses but states NO decision. Distinct from malformed —
|
|
# this one is valid JSON and still says nothing, which is the original defect
|
|
# wearing a config file as a disguise.
|
|
R="$(new_repo d5)"
|
|
write_config "$R" '{}'
|
|
printf 'code\n' > "$R/x.txt"
|
|
git -C "$R" add x.txt
|
|
expect NEEDLE 6 "a valid config that states no decision is refused" \
|
|
--out "states no decision" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# NEEDLE: a bogus json_check value must not be read as an opt-out.
|
|
R="$(new_repo d6)"
|
|
write_config "$R" '{"json_check": "off", "reason": "typo for none"}'
|
|
printf 'code\n' > "$R/x.txt"
|
|
git -C "$R" add x.txt
|
|
expect NEEDLE 6 'json_check with an unrecognised value is refused' \
|
|
--out 'must be "none"' -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: config nominates paths, JSON is clean.
|
|
# --out is REQUIRED. Exit 0 alone cannot distinguish "read the config, resolved
|
|
# the paths, parsed the files" from "matched nothing and had nothing to do" —
|
|
# the vacuous-control trap that already caught this harness once.
|
|
R="$(new_repo d7)"
|
|
write_config "$R" '{"json_paths": ["data/**/*.json"]}'
|
|
mkdir -p "$R/data"
|
|
printf '{"generated": true}\n' > "$R/data/view.json"
|
|
git -C "$R" add data/view.json
|
|
expect CONTROL 0 "config-supplied json_paths pass and are reported as parsed" \
|
|
--out "1 staged .json file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: a recorded opt-out passes AND the reason is echoed. Asserting on the
|
|
# reason is the whole point — an opt-out nobody can see is what we just removed.
|
|
R="$(new_repo d8)"
|
|
write_config "$R" '{"json_check": "none", "reason": "no generated JSON in this repo"}'
|
|
printf 'code\n' > "$R/x.txt"
|
|
git -C "$R" add x.txt
|
|
expect CONTROL 0 "a recorded opt-out passes and PRINTS its reason" \
|
|
--out "recorded reason: no generated JSON in this repo" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: config-supplied exemptions still work through the config path.
|
|
R="$(new_repo d9)"
|
|
write_config "$R" '{"json_paths": ["fixtures/*"], "allow_invalid_json": ["fixtures/*"]}'
|
|
mkdir -p "$R/fixtures"
|
|
printf 'not json at all' > "$R/fixtures/malformed.json"
|
|
git -C "$R" add fixtures/malformed.json
|
|
expect CONTROL 0 "config-supplied allow_invalid_json exempts (and announces it)" \
|
|
--out "EXEMPTED by --allow-invalid-json" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: --json-path alone still satisfies the decision, with no config at all.
|
|
# This is what keeps every pre-existing caller working: nominating a path IS an
|
|
# explicit decision. Only saying nothing is refused.
|
|
R="$(new_repo d10)"
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1}\n' > "$R/data/good.json"
|
|
git -C "$R" add data/good.json
|
|
expect CONTROL 0 "--json-path alone satisfies the decision with no config present" \
|
|
--out "1 staged .json file(s)" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/**/*.json'"
|
|
|
|
# NEEDLE: a CLI nomination must WIN over a config opt-out, loudly. Resolving a
|
|
# contradiction toward more checking is the only safe direction, but doing it
|
|
# silently would hide a genuine disagreement between caller and repo.
|
|
R="$(new_repo d11)"
|
|
write_config "$R" '{"json_check": "none", "reason": "claims no generated JSON"}'
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1,\n' > "$R/data/broken.json"
|
|
git -C "$R" add data/broken.json
|
|
expect NEEDLE 3 "--json-path overrides a config opt-out and still catches bad JSON" \
|
|
--out "honouring the nomination" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/**/*.json'"
|
|
|
|
echo "=== (z) the guard itself emits no stray output ==="
|
|
|
|
# CONTROL: the guard must not print interpreter warnings.
|
|
#
|
|
# This case exists because a real one shipped: the config parser was an inline
|
|
# `<<'PY'` heredoc inside a $( ) command substitution, so bash printed
|
|
# warning: command substitution: 1 unterminated here-document
|
|
# on EVERY run. Thirty needles and a clean shellcheck all missed it — the
|
|
# warning went to stderr, and no assertion in this harness looked at output it
|
|
# had not already been told to expect. It was found only by running the guard
|
|
# against a real repository.
|
|
#
|
|
# The lesson is narrow and worth encoding: asserting on what you EXPECT to see
|
|
# cannot detect what you never thought to look for. This asserts on the absence
|
|
# of a whole output class instead.
|
|
R="$(new_repo z1)"
|
|
write_config "$R" '{"json_paths": ["data/**/*.json"]}'
|
|
mkdir -p "$R/data"
|
|
printf '{"a": 1}\n' > "$R/data/view.json"
|
|
git -C "$R" add data/view.json
|
|
z_out="$(cd "$R" && "$GUARD" check-staged 2>&1)"
|
|
if grep -qiE 'warning:|unterminated|command substitution' <<<"$z_out"; then
|
|
printf ' FAIL [%-7s] %s\n' "CONTROL" "guard emits interpreter warnings"
|
|
printf '%s\n' "$z_out" | sed 's/^/ | /'
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
printf ' PASS [%-7s] %s\n' "CONTROL" "guard runs without emitting any interpreter warning"
|
|
PASS=$((PASS + 1))
|
|
fi
|
|
|
|
# NEEDLE for the case above: prove the detector can actually fire, otherwise it
|
|
# is one more assertion that passes because it looked at nothing.
|
|
if grep -qiE 'warning:|unterminated|command substitution' \
|
|
<<<"bash: warning: command substitution: 1 unterminated here-document"; then
|
|
printf ' PASS [%-7s] %s\n' "NEEDLE" "the warning detector fires on a known warning string"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
printf ' FAIL [%-7s] %s\n' "NEEDLE" "the warning detector is dead — it cannot fire"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
echo "=== null case: nothing staged ==="
|
|
|
|
# NEEDLE: the index equals HEAD. Committing here produces the empty commit.
|
|
R="$(new_repo n1)"
|
|
expect NEEDLE 5 "empty index is refused before a commit happens" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
echo "=== (b) push actually happened ==="
|
|
|
|
# CONTROL: a real push that moves the remote.
|
|
R="$(new_repo b1)"
|
|
printf 'work\n' > "$R/work.txt"
|
|
git -C "$R" add work.txt && git -C "$R" commit -q -m "real work"
|
|
expect CONTROL 0 "a push that moves the remote is confirmed" \
|
|
--out "PUSH CONFIRMED" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch main"
|
|
|
|
# NEEDLE: THE ORIGINAL FALSE POSITIVE. Remote already equals local HEAD, so the
|
|
# naive 'remote == local' assertion succeeds while nothing is transferred.
|
|
R="$(new_repo b2)"
|
|
printf 'work\n' > "$R/work.txt"
|
|
git -C "$R" add work.txt && git -C "$R" commit -q -m "real work"
|
|
git -C "$R" push -q origin HEAD:refs/heads/main
|
|
expect NEEDLE 4 "second push transferring nothing is refused (remote did not move)" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch main"
|
|
|
|
# NEEDLE: the commit step aborted, so HEAD never advanced.
|
|
R="$(new_repo b3)"
|
|
HEAD_BEFORE="$(git -C "$R" rev-parse HEAD)"
|
|
expect NEEDLE 5 "push after an aborted commit is refused (HEAD did not advance)" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch main --since-head '$HEAD_BEFORE'"
|
|
|
|
# NEEDLE: an empty commit carrying a real message.
|
|
R="$(new_repo b4)"
|
|
git -C "$R" commit -q --allow-empty -m "feat: important-sounding message"
|
|
expect NEEDLE 5 "pushing an empty commit is refused" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch main"
|
|
|
|
# CONTROL: --since-head is satisfied when a real commit was made.
|
|
R="$(new_repo b5)"
|
|
HEAD_BEFORE="$(git -C "$R" rev-parse HEAD)"
|
|
printf 'work\n' > "$R/work.txt"
|
|
git -C "$R" add work.txt && git -C "$R" commit -q -m "real work"
|
|
# --out is required here. Without it this control is VACUOUS: deleting the whole
|
|
# --since-head validation block would still yield exit 0, because the empty-commit
|
|
# and remote-moved checks independently succeed. It would prove nothing about the
|
|
# code path it names.
|
|
expect CONTROL 0 "--since-head passes when a real commit was created" \
|
|
--out "HEAD advanced" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch main --since-head '$HEAD_BEFORE'"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Needles for the five blockers rev-974 found from a clean checkout. Every one
|
|
# of these was reproduced before it was fixed; none is a hypothesis.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
echo
|
|
echo "-- blocker 2: the enumerating producer's exit status --"
|
|
# FAULT INJECTION. mapfile < <(git diff) reported MAPFILE's status, so a git diff
|
|
# that died returned zero files and the guard published that silence as clean.
|
|
R="$(new_repo e1)"
|
|
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
|
git -C "$R" add -f data/bad.json
|
|
expect NEEDLE 6 "a failed file enumeration REFUSES instead of reporting clean" \
|
|
--out "CANNOT ENUMERATE STAGED FILES" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path ':(this-magic-does-not-exist)data/*.json'"
|
|
# CONTROL: the same malformed file with a WORKING pathspec must still be caught,
|
|
# so the needle above is not passing merely because everything now refuses.
|
|
expect CONTROL 3 "a working pathspec still catches the malformed JSON" \
|
|
--out "data/bad.json" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged --json-path 'data/*.json'"
|
|
|
|
echo
|
|
echo "-- blocker 3: OFF must come from a committed, reviewable object --"
|
|
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 "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.
|
|
R="$(new_repo e3)"
|
|
write_config "$R" '{"json_paths": ["data/**/*.json"]}'
|
|
printf '%s\n' '{"json_check": "none", "reason": "local convenience"}' > "$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 "a committed ON flipped OFF in the working tree is refused" \
|
|
--out "LOCAL-ONLY OPT-OUT" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# A committed SYMLINK is a mutable target: the reviewed blob is a path, and what
|
|
# it points at can change with no diff at all.
|
|
R="$(new_repo e4)"
|
|
printf '%s\n' '{"json_check": "none", "reason": "via symlink"}' > "$R/real-config.json"
|
|
ln -s real-config.json "$R/.push-guard.json"
|
|
git -C "$R" add real-config.json .push-guard.json
|
|
git -C "$R" commit -q -m "symlinked config"
|
|
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
|
git -C "$R" add -f data/bad.json
|
|
expect NEEDLE 6 "a committed SYMLINK config is refused as a mutable target" \
|
|
--out "committed SYMLINK" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
# CONTROL: the supported path still works. Without this the whole opt-out feature
|
|
# could be dead and every needle above would still pass — a gate that can never
|
|
# say yes is not a gate.
|
|
R="$(new_repo e5)"
|
|
write_config "$R" '{"json_check": "none", "reason": "committed and reviewable"}'
|
|
mkdir -p "$R/data"; printf '{ not json' > "$R/data/bad.json"
|
|
git -C "$R" add -f data/bad.json
|
|
expect CONTROL 0 "a COMMITTED opt-out is honoured and prints its committed reason" \
|
|
--out "recorded reason: committed and reviewable" -- \
|
|
bash -c "cd '$R' && '$GUARD' check-staged"
|
|
|
|
echo
|
|
echo "-- blocker 4: advancement is ancestry, not inequality --"
|
|
R="$(new_repo e6)"
|
|
git -C "$R" checkout -qb other
|
|
printf 'o\n' > "$R/o.txt"; git -C "$R" add o.txt; git -C "$R" commit -q -m o
|
|
OTHER="$(git -C "$R" rev-parse HEAD)"
|
|
git -C "$R" checkout -q main 2>/dev/null || git -C "$R" checkout -q master
|
|
printf 'm\n' > "$R/m.txt"; git -C "$R" add m.txt; git -C "$R" commit -q -m m
|
|
MAINH="$(git -C "$R" rev-parse HEAD)"
|
|
git -C "$R" checkout -q "$OTHER"
|
|
expect NEEDLE 5 "a checkout of pre-existing diverged history is not 'advancement'" \
|
|
--out "NOT A DESCENDANT" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch other --since-head '$MAINH'"
|
|
|
|
echo
|
|
echo "-- blocker 5: root and merge commits have defined emptiness --"
|
|
R="$(new_repo e7)"
|
|
git -C "$R" checkout -q --orphan fresh
|
|
git -C "$R" rm -q -rf . >/dev/null 2>&1 || true
|
|
git -C "$R" commit -q --allow-empty -m "empty root"
|
|
expect NEEDLE 5 "an EMPTY ROOT commit is refused, not exempted" \
|
|
--out "EMPTY ROOT COMMIT" -- \
|
|
bash -c "cd '$R' && '$GUARD' push --remote origin --branch fresh"
|
|
|
|
# CONTROL: a real root commit must still push, or the fix is just a new wall.
|
|
R="$(new_repo e8)"
|
|
git -C "$R" checkout -q --orphan fresh2
|
|
git -C "$R" rm -q -rf . >/dev/null 2>&1 || true
|
|
printf 'real\n' > "$R/real.txt"; git -C "$R" add real.txt
|
|
git -C "$R" commit -q -m "real root"
|
|
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
|