run.sh 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env bash
  2. # mac-ops :: tests/run.sh
  3. # Lightweight self-tests. Run from repo root:
  4. # bash skills/mac-ops/tests/run.sh
  5. #
  6. # Validates structural and output invariants WITHOUT trying to simulate
  7. # broken macOS state. Catches regressions in:
  8. # - bash syntax / unbound vars / set -u trips
  9. # - section headers + ordering
  10. # - --json producing parseable NDJSON
  11. # - --redact masking private addrs / tailnet names
  12. # - --help working for every script
  13. # - summary block format
  14. set -u
  15. PASS=0
  16. FAIL=0
  17. FAILED_TESTS=()
  18. assert() {
  19. local name="$1"; shift
  20. if "$@"; then
  21. PASS=$((PASS+1))
  22. printf " [PASS] %s\n" "$name"
  23. else
  24. FAIL=$((FAIL+1))
  25. FAILED_TESTS+=("$name")
  26. printf " [FAIL] %s\n" "$name"
  27. fi
  28. }
  29. contains() { local hay="$1" needle="$2"; [[ "$hay" == *"$needle"* ]]; }
  30. not_contains() { local hay="$1" needle="$2"; [[ "$hay" != *"$needle"* ]]; }
  31. # Skip on non-macOS
  32. if [[ "$(uname -s)" != "Darwin" ]]; then
  33. echo "Skipping: mac-ops tests only run on macOS (this is $(uname -s))"
  34. exit 0
  35. fi
  36. here="$(cd "$(dirname "$0")" && pwd)"
  37. root="$(cd "$here/.." && pwd)"
  38. echo "=== mac-ops self-tests ==="
  39. echo "Root: $root"
  40. # ----------------------------------------------------------------------------
  41. echo
  42. echo "--- Script parse + permissions ---"
  43. # ----------------------------------------------------------------------------
  44. for f in "$root"/scripts/*.sh; do
  45. name=$(basename "$f")
  46. # bash -n parses without executing
  47. if bash -n "$f" 2>/dev/null; then
  48. assert "parse: $name" true
  49. else
  50. assert "parse: $name" false
  51. fi
  52. # executable bit set
  53. if [[ -x "$f" ]]; then
  54. assert "executable: $name" true
  55. else
  56. assert "executable: $name" false
  57. fi
  58. done
  59. # ----------------------------------------------------------------------------
  60. echo
  61. echo "--- --help works for every script ---"
  62. # ----------------------------------------------------------------------------
  63. for f in "$root"/scripts/*.sh; do
  64. name=$(basename "$f")
  65. out=$(bash "$f" --help 2>&1)
  66. assert "--help: $name returns usage" contains "$out" "Usage:"
  67. done
  68. # ----------------------------------------------------------------------------
  69. echo
  70. echo "--- health-audit structural ---"
  71. # ----------------------------------------------------------------------------
  72. audit_out=$(bash "$root/scripts/health-audit.sh" --days 1 --quiet 2>&1)
  73. assert "health-audit emits SUMMARY block" contains "$audit_out" "=== SUMMARY ==="
  74. assert "health-audit shows PASS counts" contains "$audit_out" "PASS:"
  75. assert "health-audit runs without unbound vars" not_contains "$audit_out" "unbound variable"
  76. # ----------------------------------------------------------------------------
  77. echo
  78. echo "--- --json produces pure NDJSON ---"
  79. # ----------------------------------------------------------------------------
  80. # Capture stdout only — JSON contract is "stdout = NDJSON, stderr may have noise"
  81. json_out=$(bash "$root/scripts/health-audit.sh" --days 1 --json 2>/dev/null)
  82. json_lines=$(echo "$json_out" | grep -c '^{' | tr -d '\n ')
  83. non_json=$(echo "$json_out" | grep -v '^{' | grep -c . | tr -d '\n ')
  84. assert "--json: at least one JSON record" bash -c "[[ \"$json_lines\" -ge 1 ]]"
  85. assert "--json: stdout is pure NDJSON (no non-JSON)" bash -c "[[ \"$non_json\" -eq 0 ]]"
  86. assert "--json: includes summary record" contains "$json_out" '"type":"summary"'
  87. # ----------------------------------------------------------------------------
  88. echo
  89. echo "--- --redact masks private addrs ---"
  90. # ----------------------------------------------------------------------------
  91. # Use startup-audit since it lists Adobe-style paths under /Users/...
  92. redact_out=$(bash "$root/scripts/startup-audit.sh" --redact --quiet 2>&1)
  93. # Should NOT contain raw 192.168.x.x or .ts.net hostnames
  94. leaks=$(echo "$redact_out" | grep -E '\b192\.168\.[0-9]+\.[0-9]+\b' | grep -v '192.168.X.X')
  95. assert "--redact: no 192.168.* leak in startup-audit" bash -c "[[ -z \"$leaks\" ]]"
  96. # ----------------------------------------------------------------------------
  97. echo
  98. echo "--- startup-audit produces clean output ---"
  99. # ----------------------------------------------------------------------------
  100. startup_out=$(bash "$root/scripts/startup-audit.sh" --quiet 2>&1)
  101. # Plutil errors should be filtered (we use || val="" pattern)
  102. assert "startup-audit: no plutil 'Could not extract'" not_contains "$startup_out" "Could not extract value"
  103. # ----------------------------------------------------------------------------
  104. echo
  105. echo "--- safe-disable-startup --list works ---"
  106. # ----------------------------------------------------------------------------
  107. list_out=$(bash "$root/scripts/safe-disable-startup.sh" --list 2>&1)
  108. assert "--list returns SUMMARY" contains "$list_out" "SUMMARY"
  109. # ----------------------------------------------------------------------------
  110. echo
  111. echo "--- panic-triage handles 'no panics' gracefully ---"
  112. # ----------------------------------------------------------------------------
  113. # Most dev Macs have no recent panics. Verify the script doesn't error.
  114. panic_out=$(bash "$root/scripts/panic-triage.sh" --quiet 2>&1 || true)
  115. assert "panic-triage runs without crashing" contains "$panic_out" "PANIC REPORT"
  116. # ----------------------------------------------------------------------------
  117. echo
  118. echo "--- tcc-audit gracefully handles permission denial ---"
  119. # ----------------------------------------------------------------------------
  120. tcc_out=$(bash "$root/scripts/tcc-audit.sh" --quiet 2>&1)
  121. # Should exit cleanly even without TCC.db read access
  122. assert "tcc-audit reaches SUMMARY (or handles no-access path)" bash -c "echo '$tcc_out' | grep -qE 'SUMMARY|TCC.db readable'"
  123. # ----------------------------------------------------------------------------
  124. echo
  125. echo "--- wake-reasons parses real pmset log ---"
  126. # ----------------------------------------------------------------------------
  127. wake_out=$(bash "$root/scripts/wake-reasons.sh" --since 1d --quiet 2>&1)
  128. assert "wake-reasons reaches SUMMARY" contains "$wake_out" "SUMMARY"
  129. # ----------------------------------------------------------------------------
  130. echo
  131. echo "--- spotlight-status filters system volumes ---"
  132. # ----------------------------------------------------------------------------
  133. spot_out=$(bash "$root/scripts/spotlight-status.sh" --quiet 2>/dev/null)
  134. # Should NOT have "Error: unknown indexing state" leak from system vols
  135. err_leaks=$(echo "$spot_out" | grep -c "unknown indexing state" | tr -d '\n ')
  136. assert "spotlight-status: system-vol error filtering" bash -c "[[ \"${err_leaks:-0}\" -le 0 ]]"
  137. # ----------------------------------------------------------------------------
  138. echo
  139. echo "--- All 12 scripts present ---"
  140. # ----------------------------------------------------------------------------
  141. expected_scripts=(
  142. health-audit.sh panic-triage.sh startup-audit.sh safe-disable-startup.sh
  143. disk-health.sh drive-dependencies.sh boot-perf.sh recover-clone.sh
  144. tcc-audit.sh wake-reasons.sh spotlight-status.sh storage-pressure.sh
  145. kext-audit.sh firewall-audit.sh network-locations.sh
  146. sysdiagnose-helper.sh brew-health.sh update-state.sh media-libraries.sh
  147. keychain-audit.sh bluetooth-audit.sh font-audit.sh
  148. )
  149. for s in "${expected_scripts[@]}"; do
  150. assert "script exists: $s" test -f "$root/scripts/$s"
  151. done
  152. # ----------------------------------------------------------------------------
  153. echo
  154. echo "--- All 7 reference docs present ---"
  155. # ----------------------------------------------------------------------------
  156. expected_refs=(
  157. storage-events.md recovery-patterns.md tcc-mechanics.md
  158. launchd-deep-dive.md panic-codes.md startup-mechanisms.md
  159. remote-diagnostics.md apple-silicon-specifics.md
  160. mac-vs-windows-ops.md worked-examples.md
  161. )
  162. for r in "${expected_refs[@]}"; do
  163. assert "reference exists: $r" test -f "$root/references/$r"
  164. done
  165. # ----------------------------------------------------------------------------
  166. echo
  167. echo "=== TOTAL: $PASS pass, $FAIL fail ==="
  168. if [[ "$FAIL" -gt 0 ]]; then
  169. echo "Failed tests:"
  170. for t in "${FAILED_TESTS[@]}"; do echo " - $t"; done
  171. exit 1
  172. fi
  173. exit 0