run.sh 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. #!/usr/bin/env bash
  2. # Self-test for claude-code-ops — fully offline: no network.
  3. #
  4. # Wraps the skill's offline hooks-contract verifier (scripts/validate-hooks-json.py),
  5. # which lints a hooks.json / settings.json hooks block against the 30-event Claude
  6. # Code hook catalog. Contract (py_compile + --help), offline happy path against
  7. # the shipped skill (assets/hooks.json.template, stripped of its JSONC comments
  8. # exactly as the template's own header instructs — "STRIP EVERY COMMENT"), the
  9. # --json §7 envelope, and a NEGATIVE proving the verifier rejects an unknown
  10. # event (exit 10, names the event). The verifier is structural-only by design;
  11. # there is no --live mode to avoid.
  12. #
  13. # Usage: bash tests/run.sh
  14. # Exit: 0 all pass, 1 one or more failures
  15. set -uo pipefail
  16. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  17. SKILL="$(dirname "$HERE")"
  18. V="$SKILL/scripts/validate-hooks-json.py"
  19. TPL="$SKILL/assets/hooks.json.template"
  20. # Pick a python that actually executes (the Windows Store python3 stub exists on
  21. # PATH but exits non-zero; probe by running it).
  22. PYTHON=""
  23. for c in python python3 py; do
  24. if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PYTHON="$c"; break; fi
  25. done
  26. SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
  27. PASS=0; FAIL=0
  28. ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; }
  29. no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
  30. expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
  31. expect_has() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
  32. echo "=== claude-code-ops self-test ==="
  33. if [[ -z "$PYTHON" ]]; then
  34. echo " SKIP no working python (verifier is python) — cannot test"
  35. [[ "$FAIL" -eq 0 ]] || exit 1
  36. exit 0
  37. fi
  38. # ── contract ──────────────────────────────────────────────────────────────────
  39. echo "-- contract --"
  40. "$PYTHON" -m py_compile "$V" 2>/dev/null && ok "py_compile validate-hooks-json.py" || no "py_compile validate-hooks-json.py"
  41. "$PYTHON" "$V" --help >/dev/null 2>&1; expect_exit "--help exits 0" 0 $?
  42. out="$("$PYTHON" "$V" --help 2>&1)"
  43. expect_has "--help has EXAMPLES" "EXAMPLES" "$out"
  44. "$PYTHON" "$V" --bogus >/dev/null 2>&1; expect_exit "unknown flag -> 2" 2 $?
  45. # ── offline happy path: shipped template, comments stripped (its own rule) ─────
  46. echo "-- offline structural --"
  47. # The shipped assets/hooks.json.template is intentionally JSONC. Stripping // lines
  48. # yields strict JSON that must lint clean against the catalog. The path is passed
  49. # as an argv (MSYS translates it for Windows Python); never embed it in -c.
  50. grep -vE '^[[:space:]]*//' "$TPL" > "$SB/hooks.json"
  51. "$PYTHON" -c "import json,sys; json.load(open(sys.argv[1],encoding='utf-8'))" "$SB/hooks.json" \
  52. >/dev/null 2>&1 && ok "stripped template is strict JSON" || no "stripped template is strict JSON"
  53. "$PYTHON" "$V" "$SB/hooks.json" >/dev/null 2>&1; expect_exit "lint clean on shipped skill" 0 $?
  54. out="$("$PYTHON" "$V" --json "$SB/hooks.json" 2>/dev/null)"
  55. expect_has "--json envelope schema" '"schema": "claude-mods.claude-code-ops.hooks-lint/v1"' "$out"
  56. expect_has "--json zero errors" '"errors": 0' "$out"
  57. # ── negative: an unknown event must be flagged (exit 10, names the event) ─────
  58. echo "-- negative --"
  59. cat > "$SB/bad.json" <<'EOF'
  60. {
  61. "hooks": {
  62. "PreToolUs": [
  63. { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo hi" } ] }
  64. ]
  65. }
  66. }
  67. EOF
  68. "$PYTHON" "$V" "$SB/bad.json" >"$SB/neg.out" 2>&1
  69. expect_exit "unknown event -> 10" 10 $?
  70. expect_has "finding names the bad event" "PreToolUs" "$(cat "$SB/neg.out")"
  71. # ── SKILL.md sanity ───────────────────────────────────────────────────────────
  72. echo "-- SKILL.md --"
  73. grep -q '^name: claude-code-ops$' "$SKILL/SKILL.md" && ok "frontmatter name" || no "frontmatter name"
  74. grep -q 'validate-hooks-json.py' "$SKILL/SKILL.md" && ok "verifier cited from SKILL.md" || no "verifier cited from SKILL.md"
  75. echo ""
  76. echo "=== $PASS passed, $FAIL failed ==="
  77. [[ "$FAIL" -eq 0 ]] || exit 1
  78. exit 0