test-requesting-code-review.sh 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #!/usr/bin/env bash
  2. # Integration Test: requesting-code-review skill
  3. # Verifies the code reviewer dispatched via the skill catches a planted bug
  4. set -euo pipefail
  5. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  6. PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
  7. source "$SCRIPT_DIR/test-helpers.sh"
  8. echo "========================================"
  9. echo " Integration Test: requesting-code-review"
  10. echo "========================================"
  11. echo ""
  12. echo "This test verifies the code reviewer subagent by:"
  13. echo " 1. Setting up a tiny project with a baseline commit"
  14. echo " 2. Adding a second commit that plants an obvious bug"
  15. echo " 3. Dispatching the code reviewer via the requesting-code-review skill"
  16. echo " 4. Verifying the reviewer flags the planted bug as Critical/Important"
  17. echo ""
  18. TEST_PROJECT=$(create_test_project)
  19. echo "Test project: $TEST_PROJECT"
  20. trap "cleanup_test_project $TEST_PROJECT" EXIT
  21. cd "$TEST_PROJECT"
  22. # Baseline: a small "safe" implementation
  23. mkdir -p src
  24. cat > src/db.js <<'EOF'
  25. import { Database } from "./database-driver.js";
  26. const db = new Database();
  27. export async function findUserByEmail(email) {
  28. if (typeof email !== "string" || !email) {
  29. throw new Error("email required");
  30. }
  31. return db.query(
  32. "SELECT id, email, created_at FROM users WHERE email = ?",
  33. [email],
  34. );
  35. }
  36. EOF
  37. cat > package.json <<'EOF'
  38. { "name": "test-codereview", "version": "1.0.0", "type": "module" }
  39. EOF
  40. git init --quiet
  41. git config user.email "test@test.com"
  42. git config user.name "Test User"
  43. git add .
  44. git commit -m "Initial: parameterized findUserByEmail" --quiet
  45. BASE_SHA=$(git rev-parse HEAD)
  46. # Second commit: plant two real bugs
  47. # 1. SQL injection — switch from parameterized to string concatenation
  48. # 2. Logs the user's password hash on every successful login
  49. cat > src/db.js <<'EOF'
  50. import { Database } from "./database-driver.js";
  51. const db = new Database();
  52. export async function findUserByEmail(email) {
  53. return db.query(
  54. "SELECT id, email, password_hash, created_at FROM users WHERE email = '" + email + "'",
  55. );
  56. }
  57. export async function login(email, password) {
  58. const user = await findUserByEmail(email);
  59. if (user && user.password_hash === hash(password)) {
  60. console.log("login success", { email, password_hash: user.password_hash });
  61. return user;
  62. }
  63. return null;
  64. }
  65. function hash(s) { return s; }
  66. EOF
  67. git add .
  68. git commit -m "Refactor user lookup, add login" --quiet
  69. HEAD_SHA=$(git rev-parse HEAD)
  70. echo ""
  71. echo "Planted bugs in $BASE_SHA..$HEAD_SHA:"
  72. echo " - SQL injection (string concat instead of parameterized query)"
  73. echo " - Password hash logged in plaintext on every successful login"
  74. echo " - hash() is the identity function (passwords stored & compared in plaintext)"
  75. echo ""
  76. OUTPUT_FILE="$TEST_PROJECT/claude-output.txt"
  77. PROMPT="I just finished a refactor. The change is between commits $BASE_SHA and $HEAD_SHA on the current branch.
  78. Use the superpowers:requesting-code-review skill to review these changes before I merge. Follow the skill exactly: dispatch the code reviewer subagent with the template, give the subagent the SHA range, and report back what it found.
  79. Print the reviewer's full output."
  80. # Run claude from inside the test project so its session JSONL lands in a
  81. # project-specific directory under ~/.claude/projects/, isolated from any
  82. # other concurrent claude sessions.
  83. echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..."
  84. echo "================================================================================"
  85. cd "$TEST_PROJECT" && timeout 600 claude -p "$PROMPT" \
  86. --plugin-dir "$PLUGIN_DIR" \
  87. --permission-mode bypassPermissions 2>&1 | tee "$OUTPUT_FILE" || {
  88. echo ""
  89. echo "================================================================================"
  90. echo "EXECUTION FAILED (exit code: $?)"
  91. exit 1
  92. }
  93. echo "================================================================================"
  94. echo ""
  95. echo "Analyzing reviewer output..."
  96. echo ""
  97. # Find the session transcript. Because we ran claude from $TEST_PROJECT (a
  98. # unique tmp dir), its sessions live in their own ~/.claude/projects/ folder.
  99. # Resolve the real path (macOS mktemp returns /var/... but claude normalizes
  100. # it to /private/var/...) and replicate claude's normalization (every
  101. # non-alphanumeric char becomes `-`).
  102. TEST_PROJECT_REAL=$(cd "$TEST_PROJECT" && pwd -P)
  103. SESSION_DIR="$HOME/.claude/projects/$(echo "$TEST_PROJECT_REAL" | sed 's|[^a-zA-Z0-9]|-|g')"
  104. # `|| true` prevents pipefail killing the script if ls gets SIGPIPE'd by head.
  105. SESSION_FILE=$(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | head -1 || true)
  106. FAILED=0
  107. echo "=== Verification Tests ==="
  108. echo ""
  109. # Test 1: Skill was actually invoked, and a subagent was actually dispatched
  110. echo "Test 1: requesting-code-review skill invoked + reviewer subagent dispatched..."
  111. if [ -z "$SESSION_FILE" ] || [ ! -f "$SESSION_FILE" ]; then
  112. echo " [FAIL] Could not locate session transcript in $SESSION_DIR"
  113. FAILED=$((FAILED + 1))
  114. elif ! grep -q '"skill":"superpowers:requesting-code-review"' "$SESSION_FILE"; then
  115. echo " [FAIL] requesting-code-review skill was not invoked"
  116. echo " Session: $SESSION_FILE"
  117. FAILED=$((FAILED + 1))
  118. elif ! grep -q '"name":"Agent"' "$SESSION_FILE"; then
  119. echo " [FAIL] Skill ran but no subagent was dispatched"
  120. FAILED=$((FAILED + 1))
  121. else
  122. echo " [PASS] Skill invoked and subagent dispatched"
  123. fi
  124. echo ""
  125. # Test 2: Reviewer caught the SQL injection
  126. echo "Test 2: SQL injection flagged..."
  127. if grep -qiE "sql injection|injection|string concat|parameterize|prepared statement|sanitiz" "$OUTPUT_FILE"; then
  128. echo " [PASS] Reviewer flagged the SQL injection vector"
  129. else
  130. echo " [FAIL] Reviewer missed the SQL injection — most obvious planted bug"
  131. FAILED=$((FAILED + 1))
  132. fi
  133. echo ""
  134. # Test 3: Reviewer caught the credential / password issue (either logging or no real hashing)
  135. echo "Test 3: Credential handling issue flagged..."
  136. if grep -qiE "password|credential|secret|plaintext|log.*hash|hash.*log|sensitive" "$OUTPUT_FILE"; then
  137. echo " [PASS] Reviewer flagged a credential / password handling issue"
  138. else
  139. echo " [FAIL] Reviewer missed the password/credential issues"
  140. FAILED=$((FAILED + 1))
  141. fi
  142. echo ""
  143. # Test 4: Reviewer marked at least one issue as Critical or Important (not just Minor)
  144. echo "Test 4: Severity classification..."
  145. if grep -qiE "critical|important|severe|high.*risk|security" "$OUTPUT_FILE"; then
  146. echo " [PASS] Reviewer classified findings at Critical/Important severity"
  147. else
  148. echo " [FAIL] Reviewer did not classify findings as Critical or Important"
  149. FAILED=$((FAILED + 1))
  150. fi
  151. echo ""
  152. # Test 5: Reviewer did NOT approve the diff for merge
  153. echo "Test 5: Reviewer verdict..."
  154. # A correct reviewer says No or "With fixes". A broken/sycophantic reviewer says Yes/Ready.
  155. if grep -qiE "ready to merge.*yes|approved.*for merge|^\s*yes\s*$|safe to merge" "$OUTPUT_FILE" \
  156. && ! grep -qiE "ready to merge.*no|with fixes|do not merge|not ready|block.*merge" "$OUTPUT_FILE"; then
  157. echo " [FAIL] Reviewer approved a diff with planted Critical bugs"
  158. FAILED=$((FAILED + 1))
  159. else
  160. echo " [PASS] Reviewer did not approve the diff"
  161. fi
  162. echo ""
  163. echo "========================================"
  164. echo " Test Summary"
  165. echo "========================================"
  166. echo ""
  167. if [ $FAILED -eq 0 ]; then
  168. echo "STATUS: PASSED"
  169. echo "The code reviewer correctly:"
  170. echo " ✓ Was dispatched via the requesting-code-review skill"
  171. echo " ✓ Flagged the SQL injection"
  172. echo " ✓ Flagged the credential handling issues"
  173. echo " ✓ Classified findings at Critical/Important severity"
  174. echo " ✓ Did not approve the diff for merge"
  175. exit 0
  176. else
  177. echo "STATUS: FAILED"
  178. echo "Failed $FAILED verification tests"
  179. echo ""
  180. echo "Output saved to: $OUTPUT_FILE"
  181. exit 1
  182. fi