fix(git): test-push-guard.sh — required repo config (#975) + heredoc-in-substitution warning
ci/woodpecker/pr/ci Pipeline was canceled
ci/woodpecker/pr/ci Pipeline was canceled
This commit is contained in:
@@ -36,6 +36,14 @@ new_repo() {
|
||||
printf '%s' "$dir/repo"
|
||||
}
|
||||
|
||||
# write_config <repo> <json-text>
|
||||
# The guard reads .push-guard.json from the WORKING TREE at the repo root, so it
|
||||
# does not need to be staged — which also keeps the "N staged file(s)" counts in
|
||||
# the older controls unchanged.
|
||||
write_config() {
|
||||
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
|
||||
@@ -105,7 +113,13 @@ 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)" \
|
||||
@@ -115,6 +129,7 @@ expect CONTROL 0 "clean staged content passes (1 file actually scanned)" \
|
||||
# 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
|
||||
=============
|
||||
@@ -243,6 +258,164 @@ expect CONTROL 0 "JSONC outside --json-path does not trip the JSON check" \
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user