From b7d391f9f41c35ba85c0a592f139b1fcdbf9c89b Mon Sep 17 00:00:00 2001 From: installer-7 Date: Fri, 31 Jul 2026 00:22:45 +0000 Subject: [PATCH] =?UTF-8?q?fix(git):=20test-push-guard.sh=20=E2=80=94=20re?= =?UTF-8?q?quired=20repo=20config=20(#975)=20+=20heredoc-in-substitution?= =?UTF-8?q?=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/tools/git/test-push-guard.sh | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/packages/mosaic/framework/tools/git/test-push-guard.sh b/packages/mosaic/framework/tools/git/test-push-guard.sh index 5a892c79..7212abe9 100644 --- a/packages/mosaic/framework/tools/git/test-push-guard.sh +++ b/packages/mosaic/framework/tools/git/test-push-guard.sh @@ -36,6 +36,14 @@ new_repo() { printf '%s' "$dir/repo" } +# write_config +# 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 [--out ] -- # # --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.