run.sh 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env bash
  2. # Self-test for auto-skill — sandboxed, offline, never touches real state.
  3. #
  4. # evaluate.sh ships real side effects: it reads a per-session tracking file
  5. # (/tmp/claude_autoskill_<8>), classifies the session, then `rm -f`s the tracking
  6. # file and appends to ~/.claude/auto-skill/pending.log. track-tools.sh is the
  7. # PostToolUse writer that populates that tracking file. Testing them must NEVER
  8. # touch the real ~/.claude/auto-skill/ or the repo's skills/ tree.
  9. #
  10. # Isolation strategy:
  11. # - HOME is redirected to a throwaway sandbox for EVERY evaluate.sh call, so
  12. # pending.log + the disable-toggle check land in the sandbox, not real ~.
  13. # - Each scenario gets a UNIQUE 8-char session-id prefix (the first 8 chars of
  14. # the session id select the tracking file). The prefix starts with 'g', which
  15. # never appears in a real (hex-UUID) Claude session id, so our tracking files
  16. # can never collide with a real running session. gen_sid is called WITHOUT
  17. # command substitution so its counter survives (a `$(...)` subshell would
  18. # discard the increment).
  19. # - The trap removes our 'g'-prefixed tracking + suggested files (and only
  20. # those) on exit.
  21. #
  22. # Coverage:
  23. # - track-tools.sh records bare tool names and tags Skill:<name> (sanitised).
  24. # - evaluate.sh classification: fires on qualifying sessions and when only a
  25. # harness skill was loaded; stays silent on too-few-writes / too-few-types /
  26. # non-harness-skill / cooldown.
  27. # - evaluate.sh reset/safety: the reset path deletes ONLY the intended
  28. # tracking file (siblings survive); the happy path cleans the tracking file
  29. # and writes pending.log into the SANDBOX, not real HOME.
  30. #
  31. # Usage: bash tests/run.sh
  32. # Exit: 0 all pass, 1 one or more failures
  33. set -uo pipefail
  34. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  35. SKILL="$(dirname "$HERE")"
  36. EVAL="$SKILL/scripts/evaluate.sh"
  37. TRACK_TOOL="$SKILL/scripts/track-tools.sh"
  38. SB="$(mktemp -d)"; mkdir -p "$SB/home"
  39. cleanup() {
  40. rm -rf "$SB" 2>/dev/null
  41. # 'g' prefix is never a real (hex-UUID) session id → only our files match.
  42. rm -f /tmp/claude_autoskill_g* /tmp/claude_autoskill_suggested_g* 2>/dev/null
  43. }
  44. trap cleanup EXIT
  45. # Unique per-run, per-scenario 8-char session-id prefixes. SID_BASE from the PID
  46. # keeps parallel runs apart; SEQ increments per call (in the parent shell, NOT a
  47. # subshell). The leading 'g' avoids any real Claude session id (hex UUID).
  48. SID_BASE=$(( $$ % 10000 ))
  49. SEQ=0
  50. GEN_SID=""
  51. gen_sid() { SEQ=$((SEQ+1)); GEN_SID=$(printf 'g%04d%03d' "$SID_BASE" "$SEQ"); }
  52. track_file() { printf '/tmp/claude_autoskill_%s' "$1"; }
  53. suggested_file(){ printf '/tmp/claude_autoskill_suggested_%s' "$1"; }
  54. # Write tool names (one per line) into a tracking file.
  55. write_track() { local f="$1"; shift; : > "$f"; for t in "$@"; do printf '%s\n' "$t" >> "$f"; done; }
  56. # Run evaluate.sh with a session id: HOME=sandbox, CWD=sandbox (no project
  57. # disable file). Captures stdout only (evaluate always exits 0, stderr muted).
  58. run_eval() { ( cd "$SB" && printf '{"session_id":"%sxxxxxxxxxxxx"}' "$1" \
  59. | HOME="$SB/home" bash "$EVAL" ); }
  60. # Run track-tools.sh with a tool/session payload, HOME=sandbox.
  61. run_track() { # $1=session_id $2=tool_name $3=skill_name(or '')
  62. local payload
  63. if [[ -n "$3" ]]; then
  64. payload=$(printf '{"tool_name":"%s","session_id":"%sxxxxxxxxxxxx","tool_input":{"skill":"%s"}}' "$2" "$1" "$3")
  65. else
  66. payload=$(printf '{"tool_name":"%s","session_id":"%sxxxxxxxxxxxx"}' "$2" "$1")
  67. fi
  68. ( cd "$SB" && printf '%s' "$payload" | HOME="$SB/home" bash "$TRACK_TOOL" )
  69. }
  70. PASS=0; FAIL=0
  71. ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; }
  72. no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
  73. expect_has() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
  74. echo "=== auto-skill self-test (sandboxed) ==="
  75. # ── syntax ───────────────────────────────────────────────────────────────────
  76. echo "-- syntax --"
  77. bash -n "$EVAL" 2>/dev/null && ok "bash -n evaluate.sh" || no "bash -n evaluate.sh"
  78. bash -n "$TRACK_TOOL" 2>/dev/null && ok "bash -n track-tools.sh" || no "bash -n track-tools.sh"
  79. # ── track-tools.sh: records bare names + tags Skill:<name> ───────────────────
  80. echo "-- track-tools.sh writer --"
  81. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  82. run_track "$SID" "Edit" ""
  83. run_track "$SID" "Bash" ""
  84. run_track "$SID" "Skill" "sync"
  85. run_track "$SID" "Skill" "deep-research"
  86. run_track "$SID" "Skill" "my:weird skill"
  87. contents="$(cat "$TF" 2>/dev/null)"
  88. expect_has "records bare tool name" $'Edit' "$contents"
  89. expect_has "records bare Bash" $'Bash' "$contents"
  90. expect_has "tags harness skill" "Skill:sync" "$contents"
  91. expect_has "tags non-harness skill" "Skill:deep-research" "$contents"
  92. expect_has "sanitises separators" "Skill:my_weird_skill" "$contents"
  93. # written to the per-session file derived from the first 8 chars of session_id
  94. [[ -f "$TF" ]] && ok "track file path derives from session-id prefix" \
  95. || no "track file path derives from session-id prefix"
  96. # ── evaluate.sh classification ───────────────────────────────────────────────
  97. echo "-- evaluate.sh classification (fires) --"
  98. # Qualifying session: 9 mutating ops, 5 distinct types, no skill -> fires.
  99. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  100. write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  101. out="$(run_eval "$SID")"
  102. expect_has "qualify -> systemMessage" "systemMessage" "$out"
  103. expect_has "qualify reports mutating count" "9 mutating ops" "$out"
  104. expect_has "qualify reports type count" "5 tool types" "$out"
  105. expect_has "qualify reports total" "(9 total)" "$out"
  106. # Harness skill loaded (sync) must NOT disqualify -> still fires.
  107. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  108. write_track "$TF" Skill:sync Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  109. out="$(run_eval "$SID")"
  110. expect_has "harness skill does not disqualify" "systemMessage" "$out"
  111. expect_has "harness-fire counts skill in total" "(10 total)" "$out"
  112. echo "-- evaluate.sh classification (silent) --"
  113. # Too few mutating ops (7 < 8).
  114. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  115. write_track "$TF" Edit Edit Edit Write Write Write Bash
  116. out="$(run_eval "$SID")"
  117. [[ -z "$out" ]] && ok "too few writes (7) -> silent" || no "too few writes should be silent: [$out]"
  118. # Enough writes but too few distinct types (3 < 4).
  119. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  120. write_track "$TF" Edit Edit Edit Edit Write Write Bash Bash
  121. out="$(run_eval "$SID")"
  122. [[ -z "$out" ]] && ok "too few distinct types (3) -> silent" || no "too few types should be silent: [$out]"
  123. # Non-harness skill loaded -> Gate 1 disqualifies.
  124. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  125. write_track "$TF" Skill:deep-research Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  126. out="$(run_eval "$SID")"
  127. [[ -z "$out" ]] && ok "non-harness skill -> silent" || no "non-harness skill should be silent: [$out]"
  128. # ── evaluate.sh reset / safety ───────────────────────────────────────────────
  129. echo "-- evaluate.sh reset/safety --"
  130. # Happy path cleans the tracking file and writes pending.log into the SANDBOX.
  131. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  132. write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  133. run_eval "$SID" >/dev/null
  134. [[ ! -f "$TF" ]] && ok "happy path removes tracking file" || no "happy path should remove tracking file"
  135. [[ -f "$SB/home/.claude/auto-skill/pending.log" ]] \
  136. && ok "pending.log lands in sandbox HOME (not real ~)" \
  137. || no "pending.log should land in sandbox HOME"
  138. # Reset path (cooldown) deletes ONLY the intended tracking file: the per-session
  139. # SUGGESTED marker and an unrelated sibling tracking file must survive.
  140. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID"); SG=$(suggested_file "$SID")
  141. gen_sid; OTHER="$GEN_SID"; DEC=$(track_file "$OTHER")
  142. write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  143. printf 'cooldown-marker\n' > "$SG"
  144. printf 'decoy\n' > "$DEC"
  145. out="$(run_eval "$SID")"
  146. [[ -z "$out" ]] && ok "cooldown -> silent" || no "cooldown should be silent"
  147. [[ ! -f "$TF" ]] && ok "cooldown removes tracking file" || no "cooldown should remove tracking file"
  148. [[ -f "$SG" ]] && ok "cooldown keeps SUGGESTED marker" || no "cooldown must not delete SUGGESTED marker"
  149. [[ -f "$DEC" ]] && ok "cooldown keeps sibling file" || no "cooldown must not delete sibling tracking file"
  150. # Reset path (global disable): removes tracking file, no output, no pending.log.
  151. gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
  152. write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
  153. mkdir -p "$SB/home/.claude"; touch "$SB/home/.claude/auto-skill.disable"
  154. LOG_BEFORE="$(wc -l < "$SB/home/.claude/auto-skill/pending.log" 2>/dev/null || echo 0)"
  155. out="$(run_eval "$SID")"
  156. [[ -z "$out" ]] && ok "disabled -> silent" || no "disabled should be silent"
  157. [[ ! -f "$TF" ]] && ok "disabled removes tracking file" || no "disabled should remove tracking file"
  158. LOG_AFTER="$(wc -l < "$SB/home/.claude/auto-skill/pending.log" 2>/dev/null || echo 0)"
  159. [[ "$LOG_AFTER" == "$LOG_BEFORE" ]] && ok "disabled writes no pending.log line" \
  160. || no "disabled must not append pending.log"
  161. rm -f "$SB/home/.claude/auto-skill.disable"
  162. echo ""
  163. echo "=== $PASS passed, $FAIL failed ==="
  164. [[ "$FAIL" -eq 0 ]] || exit 1
  165. exit 0