run.sh 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. #!/usr/bin/env bash
  2. # net-ops :: tests/run.sh
  3. # Lightweight self-tests. Run from the repo root:
  4. # bash skills/net-ops/tests/run.sh
  5. #
  6. # These verify structural and output invariants of the probe scripts WITHOUT
  7. # trying to simulate broken network state. They catch regressions in:
  8. # - bash syntax / unbound vars / set -u trips
  9. # - section labels and ordering
  10. # - --redact actually masking private addrs / tailnet names
  11. # - --json producing parseable NDJSON
  12. # - summary block format
  13. # - dispatcher routing to the right per-OS script
  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. # Locate skill root regardless of invocation dir
  32. here="$(cd "$(dirname "$0")" && pwd)"
  33. root="$(cd "$here/.." && pwd)"
  34. echo "=== net-ops self-tests ==="
  35. echo "Root: $root"
  36. # ---------------------------------------------------------------------------
  37. echo
  38. echo "--- terminal design system (term.sh panel adoption) ---"
  39. # ---------------------------------------------------------------------------
  40. # OS-independent: drives _lib/output.sh directly (no live probe), so it runs on
  41. # every platform including Windows where the probe ladder is skipped below.
  42. OUTLIB="$root/scripts/_lib/output.sh"
  43. TERMLIB="$root/../_lib/term.sh"
  44. assert "output.sh sources shared term.sh" contains "$(cat "$OUTLIB")" '_lib/term.sh'
  45. assert "probe scripts set a PANEL_TITLE" \
  46. bash -c 'grep -q "PANEL_TITLE=" "$1"' _ "$root/scripts/linux/probe.sh"
  47. # Exercise the public output API in one of the three modes.
  48. _drive() {
  49. bash -c '
  50. OUT="$1"; shift
  51. . "$OUT"
  52. PANEL_TITLE="linux probe"
  53. parse_output_flags "$@"
  54. section "1. LINK LAYER"; pass "iface up" "eth0"; fail "carrier" "no link"
  55. emit_summary
  56. ' _ "$OUTLIB" "$@"
  57. }
  58. # Panel path (FORCE_COLOR forces the render): the enclosing frame appears and is
  59. # pure ASCII under TERM_ASCII=1.
  60. panel_ascii="$(TERM_ASCII=1 FORCE_COLOR=1 _drive 2>/dev/null)"
  61. assert "panel renders the enclosing frame" contains "$panel_ascii" "+-- "
  62. assert "panel footer carries a health indicator" contains "$panel_ascii" "fail"
  63. assert "panel is pure ASCII under TERM_ASCII=1" \
  64. bash -c '! printf "%s" "$1" | LC_ALL=C grep -q "[^[:print:][:cntrl:]]"' _ "$panel_ascii"
  65. # Legacy text path (piped / non-TTY): the greppable [PASS]/[FAIL]/SUMMARY contract
  66. # is byte-stable, so humans, LLMs, tests, and the --watch dispatcher keep working.
  67. legacy="$(_drive 2>/dev/null)"
  68. assert "piped text keeps [PASS] anchor" contains "$legacy" "[PASS]"
  69. assert "piped text keeps [FAIL] anchor" contains "$legacy" "[FAIL]"
  70. assert "piped text keeps SUMMARY block" contains "$legacy" "=== SUMMARY ==="
  71. assert "piped text carries no ANSI" not_contains "$legacy" $'\033'
  72. # JSON unaffected by the panel.
  73. js="$(_drive --json 2>/dev/null)"
  74. assert "json mode still emits a summary record" contains "$js" '"type":"summary"'
  75. assert "json mode carries no panel chrome" not_contains "$js" "+-- "
  76. # term.sh primitives are pure ASCII under TERM_ASCII=1.
  77. if [[ -f "$TERMLIB" ]]; then
  78. prim="$(TERM_ASCII=1 LT="$TERMLIB" bash -c '. "$LT"; term_init; printf "%s%s%s%s" \
  79. "$(term_mark ok)" "$(term_status_row ok a b)" "$(term_panel_open net-ops x)" "$TERM_DOT"')"
  80. assert "term.sh primitives pure ASCII under TERM_ASCII=1" \
  81. bash -c '! printf "%s" "$1" | LC_ALL=C grep -q "[^[:print:][:cntrl:]]"' _ "$prim"
  82. fi
  83. # Determine the local OS probe for testing
  84. case "$(uname -s)" in
  85. Darwin) probe="$root/scripts/macos/probe.sh"; audit="$root/scripts/macos/dns-audit.sh" ;;
  86. Linux) probe="$root/scripts/linux/probe.sh"; audit="$root/scripts/linux/dns-audit.sh" ;;
  87. *) echo "Skipping: unsupported OS for local probe tests." ; exit 0 ;;
  88. esac
  89. # ---------------------------------------------------------------------------
  90. echo
  91. echo "--- Probe structural tests ---"
  92. # ---------------------------------------------------------------------------
  93. out=$(bash "$probe" 2>&1)
  94. assert "probe runs without bash error" \
  95. not_contains "$out" "syntax error"
  96. assert "probe runs without unbound variable error" \
  97. not_contains "$out" "unbound variable"
  98. assert "probe emits summary block" \
  99. contains "$out" "=== SUMMARY ==="
  100. assert "probe emits PASS/FAIL counts" \
  101. contains "$out" "PASS:"
  102. check_all_sections() {
  103. local out="$1"
  104. for s in "1. LINK LAYER" "2. IP / ICMP" "3. TCP/UDP SOCKET" "4. DNS INFRASTRUCTURE" "6. APPLICATION" "7. KNOWN VPN"; do
  105. contains "$out" "=== $s" || return 1
  106. done
  107. # Section 5 has OS-specific naming; match on the common anchor.
  108. contains "$out" "(the hook layer)" || return 1
  109. return 0
  110. }
  111. assert "probe contains all 7 sections" check_all_sections "$out"
  112. # ---------------------------------------------------------------------------
  113. echo
  114. echo "--- --redact tests ---"
  115. # ---------------------------------------------------------------------------
  116. redacted=$(bash "$probe" --redact 2>&1)
  117. # Common private patterns that should NEVER appear in redacted output.
  118. # (We use specific octets that are unlikely to appear in unrelated contexts.)
  119. assert "--redact masks 192.168.x.x" \
  120. bash -c '! grep -E "\b192\.168\.[0-9]+\.[0-9]+\b" <<< "$0" | grep -v "192.168.X.X" >/dev/null' "$redacted"
  121. assert "--redact masks .ts.net tailnet names" \
  122. bash -c '! grep -E "\b[a-z0-9-]+\.ts\.net\b" <<< "$0" | grep -v "REDACTED.ts.net" >/dev/null' "$redacted"
  123. assert "--redact preserves 100.100.100.100 anchor" \
  124. bash -c '[[ "$0" != *"100.X.X.X"* ]] || grep -q "100.100.100.100" <<< "$0"' "$redacted"
  125. assert "--redact preserves 1.1.1.1 public anchor" \
  126. contains "$redacted" "1.1.1.1"
  127. # ---------------------------------------------------------------------------
  128. echo
  129. echo "--- --json tests ---"
  130. # ---------------------------------------------------------------------------
  131. json_out=$(bash "$probe" --json 2>&1)
  132. assert "--json emits at least one section record" \
  133. contains "$json_out" '"type":"section"'
  134. assert "--json emits at least one check record" \
  135. contains "$json_out" '"type":"check"'
  136. assert "--json emits a summary record" \
  137. contains "$json_out" '"type":"summary"'
  138. assert "--json summary contains pass count" \
  139. bash -c 'grep -q "\"type\":\"summary\".*\"pass\":[0-9]" <<< "$0"' "$json_out"
  140. # ---------------------------------------------------------------------------
  141. echo
  142. echo "--- Dispatcher test ---"
  143. # ---------------------------------------------------------------------------
  144. disp_out=$("$root/scripts/probe" 2>&1 | tail -5)
  145. assert "dispatcher routes to per-OS probe (summary present)" \
  146. contains "$disp_out" "PASS:"
  147. # ---------------------------------------------------------------------------
  148. echo
  149. echo "--- dns-audit smoke test ---"
  150. # ---------------------------------------------------------------------------
  151. audit_out=$(bash "$audit" 2>&1)
  152. assert "dns-audit runs without error" \
  153. not_contains "$audit_out" "syntax error"
  154. assert "dns-audit emits attribution hints section" \
  155. contains "$audit_out" "ATTRIBUTION HINTS"
  156. # ---------------------------------------------------------------------------
  157. echo
  158. echo "--- Edge cases ---"
  159. # ---------------------------------------------------------------------------
  160. # --json should emit ONLY JSON (no chatter leaking through)
  161. json_pure=$(bash "$probe" --json 2>&1)
  162. non_json=$(echo "$json_pure" | grep -vc '^{')
  163. assert "--json produces pure NDJSON (no non-JSON chatter)" \
  164. bash -c '[[ "$0" -eq 0 ]]' "$non_json"
  165. # --json + --redact: redacted private addrs AND only JSON
  166. combo=$(bash "$probe" --json --redact 2>&1)
  167. combo_non_json=$(echo "$combo" | grep -vc '^{')
  168. combo_leaks=$(echo "$combo" | grep -E "\b192\.168\.[0-9]+\.[0-9]+\b" | grep -v "192.168.X.X")
  169. assert "--json + --redact produces pure NDJSON" \
  170. bash -c '[[ "$0" -eq 0 ]]' "$combo_non_json"
  171. assert "--json + --redact has no private-IP leaks" \
  172. bash -c '[[ -z "$0" ]]' "$combo_leaks"
  173. # Unknown flag should not crash
  174. assert "unknown --frobnicate flag does not crash" \
  175. bash -c 'bash "$0" --frobnicate 2>&1 | grep -q "PASS\\|FAIL"' "$probe"
  176. # Help flag prints usage and exits cleanly
  177. help_out=$(bash "$probe" --help 2>&1)
  178. assert "--help mentions --redact" \
  179. contains "$help_out" "--redact"
  180. assert "--help mentions --json" \
  181. contains "$help_out" "--json"
  182. assert "--help mentions --quick" \
  183. contains "$help_out" "--quick"
  184. # Dispatcher works from a different cwd
  185. disp_remote=$(cd /tmp && "$root/scripts/probe" 2>&1 | tail -5)
  186. assert "dispatcher works from /tmp (cwd-independent)" \
  187. contains "$disp_remote" "PASS:"
  188. # ---------------------------------------------------------------------------
  189. echo
  190. echo "=== TOTAL: $PASS pass, $FAIL fail ==="
  191. if [[ "$FAIL" -gt 0 ]]; then
  192. echo "Failed tests:"
  193. for t in "${FAILED_TESTS[@]}"; do echo " - $t"; done
  194. exit 1
  195. fi
  196. exit 0