#!/usr/bin/env bash
# tests/repo-lint/run-tests.sh -- offline harness for repo_lint.py (DOCFIX-074).
# Builds throwaway fixture repos with planted violations; asserts each lint class
# fires, and that a clean fixture passes. Mutates nothing outside $TMP.
# Exit: 0 all pass | 1 any case failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
mkrepo() { # mkrepo <name> -> minimal clean fixture repo
local d="$TMP/$1"; mkdir -p "$d/runbooks" "$d/scripts" "$d/docs"
printf '# readme\n' > "$d/README.md"
printf 'applications: {}\n' > "$d/bundle.yaml"
printf '## D-001\ntext\n' > "$d/docs/design-decisions.md"
printf '#!/usr/bin/env bash\necho ok\n' > "$d/scripts/real.sh"
printf '# phase\n```bash\nbash scripts/real.sh\n```\n' > "$d/runbooks/phase-x.md"
echo "$d"
}
run() { # run <want_rc> <regex> <label> <dir>
local out rc
out="$(python3 "$REPO/scripts/repo_lint.py" "$4" 2>&1)"; rc=$?
if [[ "$rc" == "$1" ]] && grep -qE "$2" <<<"$out"; then
echo " PASS $3"; PASS=$((PASS+1))
else
echo " FAIL $3 (rc=$rc want=$1)"; echo "$out" | sed 's/^/ /' | head -5; FAIL=$((FAIL+1))
fi
}
D=$(mkrepo clean); run 0 'PASS: repo lint' "T1 clean fixture passes" "$D"
D=$(mkrepo l1); printf 'line with \xe2\x80\x94 dash\n' >> "$D/runbooks/phase-x.md"
run 1 'L1 .*non-ASCII' "T2 L1 non-ASCII FAILS" "$D"
D=$(mkrepo l1b); printf 'crlf line\r\n' >> "$D/scripts/real.sh"
run 1 'L1 .*CR byte' "T3 L1 CRLF FAILS" "$D"
D=$(mkrepo l2); printf 'bind to provider-vip here\n' >> "$D/runbooks/phase-x.md"
run 1 'L2 .*retired space' "T4 L2 stale space FAILS" "$D"
D=$(mkrepo l2b); printf 'the provider-vip plane is retired (D-060)\n' >> "$D/runbooks/phase-x.md"
run 0 'PASS' "T5 L2 historical context EXEMPT" "$D"
D=$(mkrepo l3); printf 'run scripts/ghost.sh now\n' >> "$D/runbooks/phase-x.md"
run 1 'L3 .*missing scripts/ghost.sh' "T6 L3 ghost ref FAILS" "$D"
D=$(mkrepo l4); printf 'use phase-00-teardown.sh --apply\n' >> "$D/runbooks/phase-x.md"
run 1 'L4 .*deprecated' "T7 L4 deprecated ref FAILS" "$D"
D=$(mkrepo l5); printf '## D-001\ndupe\n' >> "$D/docs/design-decisions.md"
run 1 'L5 D-001 defined 2' "T8 L5 numbering collision FAILS" "$D"
D=$(mkrepo l6); printf '```bash\nscripts/real.sh --apply\n```\n' >> "$D/runbooks/phase-x.md"
run 1 'L6 .*bare script invocation' "T9 L6 bare invoke FAILS" "$D"
# ---- L7 clientdocs sweep-on-change receipt (DOCFIX-104) ----
mkcd() { # mkcd <name> -> clean fixture WITH clientdocs + tenant-facing files
local d; d=$(mkrepo "$1")
mkdir -p "$d/clientdocs"
printf '# welcome\n' > "$d/clientdocs/welcome.md"
printf '#!/usr/bin/env bash\necho onboard\n' > "$d/scripts/tenant-onboard.sh"
printf '# contract\n' > "$d/docs/tenant-onboarding-contract.md"
echo "$d"
}
rec() { python3 "$REPO/scripts/repo_lint.py" --record-clientdocs-sweep "$1" >/dev/null 2>&1; }
mkguide() { # mkcd + a Jenkins guide + skill refs (for the L8 guide<->skill coupling tests)
local d; d=$(mkcd "$1")
printf '# guide v1\n' > "$d/clientdocs/jenkins-kubernetes-guide.md"
mkdir -p "$d/clientdocs/tenant-skill/references"
printf '# k8s\n' > "$d/clientdocs/tenant-skill/references/kubernetes.md"
printf '# trbl\n' > "$d/clientdocs/tenant-skill/references/troubleshooting.md"
echo "$d"
}
reccpl() { python3 "$REPO/scripts/repo_lint.py" --record-guide-skill-coupling "$1" >/dev/null 2>&1; }
D=$(mkcd l7a); run 1 'L7 clientdocs/ exists without sweep-receipt' \
"T10 L7 missing receipt FAILS" "$D"
D=$(mkcd l7b); rec "$D"; run 0 'PASS: repo lint' "T11 L7 recorded receipt PASSES" "$D"
D=$(mkcd l7c); rec "$D"; printf 'echo changed\n' >> "$D/scripts/tenant-onboard.sh"
run 1 'L7 scripts/tenant-onboard.sh changed since the recorded clientdocs sweep' \
"T12 L7 stale marker FAILS" "$D"
D=$(mkcd l7d); rec "$D"; printf 'echo changed\n' >> "$D/scripts/tenant-onboard.sh"; rec "$D"
run 0 'PASS: repo lint' "T13 L7 marker bump PASSES again" "$D"
D=$(mkcd l7e); rec "$D"; printf 'unrelated\n' >> "$D/README.md"
run 0 'PASS: repo lint' "T14 L7 unrelated change does NOT fire" "$D"
D=$(mkcd l7f); rec "$D"; printf '# pricing\n' > "$D/clientdocs/pricing.md"
run 1 'L7 tenant-facing clientdocs/pricing.md is not in the sweep receipt' \
"T15 L7 uncovered new tenant-facing file FAILS" "$D"
D=$(mkcd l7g); rec "$D"; rm "$D/scripts/tenant-onboard.sh"
run 1 'L7 sweep receipt pins missing file scripts/tenant-onboard.sh' \
"T16 L7 pinned file removed FAILS" "$D"
D=$(mkcd l7h); rec "$D"; printf 'not-a-hash-line\n' >> "$D/clientdocs/sweep-receipt.txt"
run 1 'L7 .*malformed line' "T17 L7 malformed receipt line FAILS" "$D"
# clientdocs/scripts/* is auto-covered like clientdocs/*.md (starter-kit files)
D=$(mkcd l7i); rec "$D"; mkdir -p "$D/clientdocs/scripts"
printf '#!/usr/bin/env bash\necho smoke\n' > "$D/clientdocs/scripts/smoke.sh"
run 1 'L7 tenant-facing clientdocs/scripts/smoke.sh is not in the sweep receipt' \
"T18 L7 uncovered clientdocs/scripts file FAILS" "$D"
D=$(mkcd l7j); mkdir -p "$D/clientdocs/scripts"
printf '#!/usr/bin/env bash\necho smoke\n' > "$D/clientdocs/scripts/smoke.sh"; rec "$D"
run 0 'PASS: repo lint' "T19 L7 recorded clientdocs/scripts file PASSES" "$D"
D=$(mkcd l7k); mkdir -p "$D/clientdocs/scripts"
printf '#!/usr/bin/env bash\necho smoke\n' > "$D/clientdocs/scripts/smoke.sh"; rec "$D"
printf 'echo changed\n' >> "$D/clientdocs/scripts/smoke.sh"
run 1 'L7 clientdocs/scripts/smoke.sh changed since the recorded clientdocs sweep' \
"T20 L7 clientdocs/scripts drift FAILS" "$D"
# *.md in ANY clientdocs subdir is auto-covered (tenant-skill/ package)
D=$(mkcd l7l); rec "$D"; mkdir -p "$D/clientdocs/tenant-skill/references"
printf '# skill ref\n' > "$D/clientdocs/tenant-skill/references/ops.md"
run 1 'L7 tenant-facing clientdocs/tenant-skill/references/ops.md is not in the sweep receipt' \
"T21 L7 uncovered clientdocs subdir .md FAILS" "$D"
D=$(mkcd l7m); mkdir -p "$D/clientdocs/tenant-skill/references"
printf '# skill ref\n' > "$D/clientdocs/tenant-skill/references/ops.md"; rec "$D"
run 0 'PASS: repo lint' "T22 L7 recorded subdir .md PASSES" "$D"
# .claude/ (agent worktrees, hooks) is session-local, never linted
D=$(mkcd l1c); rec "$D"; mkdir -p "$D/.claude/worktrees/agent-x/docs"
printf '# em\xe2\x80\x94dash noise\n' > "$D/.claude/worktrees/agent-x/docs/design-decisions.md"
run 0 'PASS: repo lint' "T23 .claude/ contents excluded from lint" "$D"
# ---- L8 guide<->skill coupling (skill-lag guard) ----
# guide present + L7 recorded, but no coupling pin -> L8 FAILS
D=$(mkguide l8a); rec "$D"
run 1 'L8 clientdocs/guide-skill-coupling.txt missing' \
"T24 L8 missing coupling pin FAILS" "$D"
# pin recorded -> PASSES
D=$(mkguide l8b); rec "$D"; reccpl "$D"
run 0 'PASS: repo lint' "T25 L8 recorded coupling PASSES" "$D"
# guide edited, L7 re-recorded (so L7 is clean) but skill NOT re-reviewed -> L8 FAILS alone
D=$(mkguide l8c); rec "$D"; reccpl "$D"
printf '# guide v2 (new topic)\n' >> "$D/clientdocs/jenkins-kubernetes-guide.md"; rec "$D"
run 1 'L8 jenkins-kubernetes-guide.md changed since the last' \
"T26 L8 guide drift FAILS when skill not re-reviewed" "$D"
# after re-recording the coupling (the review ack) -> PASSES again
D=$(mkguide l8d); rec "$D"; reccpl "$D"
printf '# guide v2\n' >> "$D/clientdocs/jenkins-kubernetes-guide.md"; rec "$D"; reccpl "$D"
run 0 'PASS: repo lint' "T27 L8 re-record after review PASSES" "$D"
# no guide in the fixture -> L8 is skipped entirely (clean repo still passes)
D=$(mkcd l8e); rec "$D"; run 0 'PASS: repo lint' "T28 L8 skipped when no guide present" "$D"
echo; echo "RESULT: PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1