PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 10s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 12s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Failing after 6s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Skipped
Bounded correction round on PR #134 after a PASS-WITH-FINDINGS review whose
Finding 4 is High. The guard's purpose and its fail-closed fix stand; the
problem was that with 'enforce' as the default it gates EVERY scheduled
contract, and three legitimate states produce a refusal - a clone legitimately
ahead of origin/master mid-review, a detached HEAD, and an offline or failed
fetch - so any of them would turn the fleet's monitoring into withheld
verdicts. That risk outweighs the staleness the guard catches.
1. DEFAULT IS NOW 'warn'. 'enforce' remains available and documented. The
criteria for flipping the default later are written into the doc as a
decision with evidence - a sustained window (30 days / 200+ runs) with zero
mismatch:* and zero cannot-verify:* refusals, no fetch blips, and a pinned
clone demonstrably kept current - explicitly as its own change, not a silent
flip.
2. 'COULD NOT CHECK' IS NOW DISTINGUISHABLE FROM 'THIS COPY IS WRONG'. Every
non-zero exit prints a machine-readable REASON=<class> line:
cannot-verify:fetch-failed | cannot-verify:ref-unresolvable (exit 2)
mismatch:path-absent | mismatch:content
mismatch:detached-head | mismatch:clone-ahead (exit 1)
detached-head and clone-ahead are named separately because they are
legitimate states, far less alarming than a hand-edited file. clone-ahead
requires HEAD to be STRICTLY ahead; an uncommitted edit on a commit that IS
the ref is a plain content mismatch (my own first cut got this wrong and the
new test 7d caught it).
3. THE DEFAULT FETCH IS BOUNDED: --fetch-timeout, default 20s, 0 = unbounded,
and a missing 'timeout' binary is itself a cannot-verify rather than an
unbounded fetch inside a scheduled contract.
4. TEST COVERAGE ADDED for every new class: fetch failure, fetch timeout
(asserted to return promptly under a 1s bound), unresolvable ref, detached
HEAD, clone-ahead, genuine content mismatch, and the contract-run.sh default.
The pre-fix draft fixture comparisons are kept: 31 passed, 0 failed.
5. MERGE-TIME SEQUENCE documented: fast-forward /opt/contract-runner, confirm
clean, prove a contract runs and reports. Baseline recorded as of today -
firstmate has already fast-forwarded it to 9faffe4 - with the note that an
untracked file blocks a fast-forward even when byte-identical.
Live behaviour re-verified on the real runner path:
default: REASON=mismatch:clone-ahead -> 'continuing because ...=warn' -> VERDICT: PASS, exit 0
enforce: REASON=mismatch:clone-ahead -> 'VERDICT WITHHELD: mismatch:clone-ahead', exit 2
MANDATORY CHECKS (master went red once from a credential-SHAPED string, so
these are now run on every shippable branch):
bash scripts/prose-lint.sh -> LINT PASSED (18 warning(s))
secret scan -> secret scan clean (tree; 34 allowlisted,
24 inert value(s) ignored); No committed credentials
shellcheck revision-preflight.sh -> clean
shellcheck test_revision_preflight.sh -> clean
shellcheck contract-run.sh -> SC2034 x1, SC2086 x2 - byte-identical on
master, i.e. pre-existing, none introduced
tests/test_probe_drift.py::test_prose_lint_accepts_report_format_with_provenance
fails both before and after this branch (it runs prose-lint from a temp CWD and
cannot find its sibling secret-scan.sh). Pre-existing, unrelated, not fixed here.
283 lines
12 KiB
Bash
Executable File
283 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Behavioural tests for scripts/revision-preflight.sh
|
|
#
|
|
# Every case builds a throwaway clone with a real bare remote, so origin/master
|
|
# is genuine and the guard's fetch path is exercised. Nothing outside mktemp is
|
|
# touched.
|
|
#
|
|
# The pre-fix draft is kept at tests/fixtures/revision-preflight.prefix.sh and
|
|
# is run against the SAME cases, to prove these tests bite: the pre-fix guard
|
|
# exits 0 where the fixed guard exits 1.
|
|
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO="$(cd "$HERE/.." && pwd)"
|
|
GUARD="$REPO/scripts/revision-preflight.sh"
|
|
PREFIX_GUARD="$HERE/fixtures/revision-preflight.prefix.sh"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
FAILED_CASES=()
|
|
|
|
pass() { printf ' ✓ %s\n' "$1"; PASS=$((PASS + 1)); }
|
|
fail() { printf ' ✗ %s\n' "$1"; FAIL=$((FAIL + 1)); FAILED_CASES+=("$1"); }
|
|
|
|
# Build a clone with a real remote; echo the clone path.
|
|
make_clone() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
git init --bare -q "$tmp/remote.git"
|
|
git init -q "$tmp/clone"
|
|
(
|
|
cd "$tmp/clone" || exit 1
|
|
git config user.email test@example.invalid
|
|
git config user.name test
|
|
mkdir -p scripts
|
|
printf '#!/bin/bash\necho hello\n' > scripts/demo.sh
|
|
chmod +x scripts/demo.sh
|
|
git add -A
|
|
git commit -qm init
|
|
git branch -M master
|
|
git remote add origin "$tmp/remote.git"
|
|
git push -q origin master
|
|
git fetch -q origin
|
|
)
|
|
echo "$tmp/clone"
|
|
}
|
|
|
|
echo "== revision-preflight behavioural tests =="
|
|
|
|
# ── 1. match → exit 0 ────────────────────────────────────────────────────────
|
|
echo "1. matching copy"
|
|
C=$(make_clone)
|
|
if out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); then
|
|
pass "matching copy exits 0"
|
|
else
|
|
fail "matching copy should exit 0 (got $?, output: $out)"
|
|
fi
|
|
if [[ -z "$( "$GUARD" --quiet "$C/scripts/demo.sh" "$C" 2>&1 )" ]]; then
|
|
pass "--quiet prints nothing on a match"
|
|
else
|
|
fail "--quiet should print nothing on a match"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 2. mismatch → exit 1 and names both hashes ───────────────────────────────
|
|
echo "2. mismatched copy"
|
|
C=$(make_clone)
|
|
printf '#!/bin/bash\necho TAMPERED\n' > "$C/scripts/demo.sh"
|
|
out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "mismatch exits 1"; else fail "mismatch should exit 1 (got $rc)"; fi
|
|
if [[ "$out" == *"MISMATCH"* ]]; then pass "mismatch says MISMATCH"; else fail "mismatch should say MISMATCH"; fi
|
|
if [[ "$out" == *"executed:"* && "$out" == *"merged:"* ]]; then
|
|
pass "mismatch prints both revisions"
|
|
else
|
|
fail "mismatch should print both revisions"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 3. paths under scripts/ resolve (the original basename defect) ───────────
|
|
echo "3. repo-relative path resolution"
|
|
C=$(make_clone)
|
|
if "$GUARD" --quiet "$C/scripts/demo.sh" "$C" >/dev/null 2>&1; then
|
|
pass "script under scripts/ resolves against origin/master"
|
|
else
|
|
fail "script under scripts/ must resolve (basename defect)"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 4. script that exists in NO revision → must fail ─────────────────────────
|
|
echo "4. untracked script present in no revision"
|
|
C=$(make_clone)
|
|
printf '#!/bin/bash\necho never committed\n' > "$C/scripts/ghost.sh"
|
|
out=$("$GUARD" "$C/scripts/ghost.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "ghost script exits 1"; else fail "ghost script must exit 1 (got $rc)"; fi
|
|
if [[ "$out" == *"does not exist in"* ]]; then
|
|
pass "ghost script says it is absent from the ref"
|
|
else
|
|
fail "ghost script should say it is absent from the ref"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
reason_of() { grep -m1 '^REASON=' <<<"$1" | cut -d= -f2-; }
|
|
|
|
# ── 5. unresolvable ref → CANNOT VERIFY (exit 2) ─────────────────────────────
|
|
echo "5. unresolvable ref"
|
|
C=$(make_clone)
|
|
out=$("$GUARD" --no-fetch --ref origin/nope "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 2 ]]; then pass "unresolvable ref exits 2 (cannot verify)"; else fail "unresolvable ref must exit 2 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "cannot-verify:ref-unresolvable" ]]; then
|
|
pass "unresolvable ref names cannot-verify:ref-unresolvable"
|
|
else
|
|
fail "unresolvable ref should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 5b. fetch failure → CANNOT VERIFY, and it names that ─────────────────────
|
|
echo "5b. fetch failed"
|
|
C=$(make_clone)
|
|
( cd "$C" && git remote set-url origin /nonexistent/definitely-not-a-repo )
|
|
out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 2 ]]; then pass "fetch failure exits 2 (cannot verify)"; else fail "fetch failure must exit 2 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "cannot-verify:fetch-failed" ]]; then
|
|
pass "fetch failure names cannot-verify:fetch-failed"
|
|
else
|
|
fail "fetch failure should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
if [[ "$out" == *"bound:"* ]]; then pass "fetch failure reports the bound"; else fail "fetch failure should report the bound"; fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 5c. fetch timeout → CANNOT VERIFY, bounded (never hangs) ─────────────────
|
|
echo "5c. fetch timeout is bounded"
|
|
C=$(make_clone)
|
|
# a remote that will never answer: a fifo-backed git daemon is overkill, so use
|
|
# a black-hole address with a 1s bound and assert we return promptly.
|
|
( cd "$C" && git remote set-url origin http://10.255.255.1:9/never.git )
|
|
start=$(date +%s)
|
|
out=$("$GUARD" --fetch-timeout 1 "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
elapsed=$(( $(date +%s) - start ))
|
|
if [[ $rc -eq 2 ]]; then pass "timeout exits 2 (cannot verify)"; else fail "timeout must exit 2 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "cannot-verify:fetch-failed" ]]; then
|
|
pass "timeout names cannot-verify:fetch-failed"
|
|
else
|
|
fail "timeout should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
if [[ $elapsed -le 10 ]]; then pass "timeout returned promptly (${elapsed}s, bound 1s)"; else fail "timeout did not bound the fetch (${elapsed}s)"; fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 6. missing script → MISMATCH:path-absent (exit 1) ────────────────────────
|
|
echo "6. missing script"
|
|
C=$(make_clone)
|
|
out=$("$GUARD" "$C/scripts/nope.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "missing script exits 1"; else fail "missing script must exit 1 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "mismatch:path-absent" ]]; then
|
|
pass "missing script names mismatch:path-absent"
|
|
else
|
|
fail "missing script should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 7. script outside the clone → MISMATCH (exit 1) ──────────────────────────
|
|
echo "7. script outside the clone"
|
|
C=$(make_clone)
|
|
OUTSIDE=$(mktemp)
|
|
printf '#!/bin/bash\necho outside\n' > "$OUTSIDE"
|
|
out=$("$GUARD" "$OUTSIDE" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "outside script exits 1"; else fail "outside script must exit 1 (got $rc)"; fi
|
|
rm -f "$OUTSIDE"; rm -rf "$(dirname "$C")"
|
|
|
|
# ── 7b. detached HEAD is named, not reported as a raw content mismatch ───────
|
|
echo "7b. detached HEAD"
|
|
C=$(make_clone)
|
|
( cd "$C" && printf '#!/bin/bash\necho TAMPERED\n' > scripts/demo.sh \
|
|
&& git add scripts/demo.sh && git commit -qm tamper && git checkout -q --detach HEAD )
|
|
out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "detached HEAD exits 1"; else fail "detached HEAD must exit 1 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "mismatch:detached-head" ]]; then
|
|
pass "detached HEAD names mismatch:detached-head"
|
|
else
|
|
fail "detached HEAD should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 7c. a branch ahead of the ref is named as such, not as a raw mismatch ────
|
|
echo "7c. clone ahead of the ref"
|
|
C=$(make_clone)
|
|
( cd "$C" && git checkout -q -b feature \
|
|
&& printf '#!/bin/bash\necho FEATURE\n' > scripts/demo.sh \
|
|
&& git add scripts/demo.sh && git commit -qm feature )
|
|
out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 1 ]]; then pass "clone ahead exits 1"; else fail "clone ahead must exit 1 (got $rc)"; fi
|
|
if [[ "$(reason_of "$out")" == "mismatch:clone-ahead" ]]; then
|
|
pass "clone ahead names mismatch:clone-ahead"
|
|
else
|
|
fail "clone ahead should name its class (got: $(reason_of "$out"))"
|
|
fi
|
|
if [[ "$out" == *"mid-review"* ]]; then pass "clone ahead explains it is a legitimate state"; else fail "clone ahead should explain the state"; fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 7d. a genuine content mismatch is named as content ──────────────────────
|
|
echo "7d. genuine content mismatch"
|
|
C=$(make_clone)
|
|
printf '#!/bin/bash\necho TAMPERED\n' > "$C/scripts/demo.sh"
|
|
out=$("$GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ "$(reason_of "$out")" == "mismatch:content" ]]; then
|
|
pass "hand-edit names mismatch:content"
|
|
else
|
|
fail "hand-edit should name mismatch:content (got: $(reason_of "$out"))"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 8. --no-fetch states the freshness assumption ────────────────────────────
|
|
echo "8. --no-fetch states its assumption"
|
|
C=$(make_clone)
|
|
out=$("$GUARD" --no-fetch "$C/scripts/demo.sh" "$C" 2>&1)
|
|
if [[ "$out" == *"freshness is assumed"* ]]; then
|
|
pass "--no-fetch states the freshness assumption"
|
|
else
|
|
fail "--no-fetch should state the freshness assumption"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# ── 8b. contract-run.sh defaults to warn, not enforce ───────────────────────
|
|
echo "8b. contract-run.sh default mode"
|
|
DEFAULT=$(grep -m1 'CONTRACT_REVISION_PREFLIGHT:-' "$REPO/scripts/contract-run.sh" | sed 's/.*:-//; s/}.*//')
|
|
if [[ "$DEFAULT" == "warn" ]]; then
|
|
pass "contract-run.sh defaults to warn"
|
|
else
|
|
fail "contract-run.sh default must be warn (found: '$DEFAULT')"
|
|
fi
|
|
if grep -q 'CONTRACT_REVISION_PREFLIGHT=enforce' "$REPO/scripts/contract-run.sh"; then
|
|
pass "enforce remains available and documented"
|
|
else
|
|
fail "enforce must remain documented"
|
|
fi
|
|
|
|
# ── 9. the pre-fix guard must FAIL these same cases (proves the tests bite) ──
|
|
echo "9. pre-fix draft fails the same cases (bite proof)"
|
|
if [[ ! -f "$PREFIX_GUARD" ]]; then
|
|
fail "pre-fix fixture missing: $PREFIX_GUARD"
|
|
else
|
|
# 9a. repo-relative path: pre-fix drops scripts/ and cannot resolve
|
|
C=$(make_clone)
|
|
out=$("$PREFIX_GUARD" "$C/scripts/demo.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 0 && "$out" == *"could not resolve"* ]]; then
|
|
pass "pre-fix: exits 0 and cannot resolve scripts/demo.sh (defect confirmed)"
|
|
else
|
|
fail "pre-fix should exit 0 with 'could not resolve' (got rc=$rc)"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# 9b. ghost script: pre-fix passes a script that exists in no revision
|
|
C=$(make_clone)
|
|
printf '#!/bin/bash\necho never committed\n' > "$C/scripts/ghost.sh"
|
|
out=$("$PREFIX_GUARD" "$C/scripts/ghost.sh" "$C" 2>&1); rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
pass "pre-fix: PASSES a ghost script that exists in no revision (defect confirmed)"
|
|
else
|
|
fail "pre-fix was expected to wrongly pass the ghost script (got rc=$rc)"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
|
|
# 9c. a file that DOES exist at the repo root still works pre-fix, showing
|
|
# the defect is specific to nested paths
|
|
C=$(make_clone)
|
|
printf '#!/bin/bash\necho root\n' > "$C/rootlevel.sh"
|
|
( cd "$C" && git add rootlevel.sh && git commit -qm root && git push -q origin master && git fetch -q origin )
|
|
if "$PREFIX_GUARD" "$C/rootlevel.sh" "$C" >/dev/null 2>&1; then
|
|
pass "pre-fix: root-level path resolves (so the defect is the basename, not git)"
|
|
else
|
|
fail "pre-fix should resolve a root-level tracked file"
|
|
fi
|
|
rm -rf "$(dirname "$C")"
|
|
fi
|
|
|
|
echo
|
|
echo " passed: $PASS failed: $FAIL"
|
|
if [[ $FAIL -gt 0 ]]; then
|
|
printf ' FAILED: %s\n' "${FAILED_CASES[@]}"
|
|
exit 1
|
|
fi
|
|
echo "All revision-preflight tests passed."
|