orchestrator-rate-limits.test.sh
1 #!/bin/sh 2 # Tests for claude-orchestrator.sh should_skip_for_rate_limits() function 3 # 4 # Extracts the function from the orchestrator and tests it in isolation. 5 # Uses a temporary directory for rate-limits.json fixtures. 6 7 PASS=0 8 FAIL=0 9 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 10 PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" 11 ORCHESTRATOR="$PROJECT_ROOT/scripts/claude-orchestrator.sh" 12 13 # Create temp working directory that acts as PROJECT_ROOT for the function 14 TMPDIR=$(mktemp -d) 15 mkdir -p "$TMPDIR/logs" 16 trap 'rm -rf "$TMPDIR"' EXIT 17 18 # ── Helpers ────────────────────────────────────────────────────────────────── 19 20 pass() { 21 PASS=$((PASS + 1)) 22 echo " PASS: $1" 23 } 24 25 fail() { 26 FAIL=$((FAIL + 1)) 27 echo " FAIL: $1" 28 } 29 30 assert_returns() { 31 expected="$1" 32 actual="$2" 33 msg="$3" 34 if [ "$actual" = "$expected" ]; then 35 pass "$msg" 36 else 37 fail "$msg (expected $expected, got $actual)" 38 fi 39 } 40 41 # ── Extract the function ───────────────────────────────────────────────────── 42 # We source just the function (plus a minimal log stub) so we can test in isolation. 43 44 cat > "$TMPDIR/func.sh" << 'FUNCEOF' 45 #!/bin/sh 46 log() { :; } # stub 47 FUNCEOF 48 49 # Extract should_skip_for_rate_limits function from the orchestrator 50 sed -n '/^should_skip_for_rate_limits()/,/^}/p' "$ORCHESTRATOR" >> "$TMPDIR/func.sh" 51 52 # Override PROJECT_ROOT inside the sourced function 53 echo "PROJECT_ROOT=\"$TMPDIR\"" >> "$TMPDIR/func.sh" 54 55 . "$TMPDIR/func.sh" 56 57 # ── Test 1: No rate-limits.json file ──────────────────────────────────────── 58 59 echo "Test 1: No rate-limits.json file → returns 1 (don't skip)" 60 rm -f "$TMPDIR/logs/rate-limits.json" 61 should_skip_for_rate_limits score_semantic; rc=$? 62 assert_returns "1" "$rc" "No file returns 1" 63 64 # ── Test 2: Active rate limit (resetAt in the future) ─────────────────────── 65 66 echo "Test 2: Active rate limit with matching stage → returns 0 (skip)" 67 future_ms=$(node -e "console.log(Date.now() + 3600000)") # 1 hour from now 68 cat > "$TMPDIR/logs/rate-limits.json" << EOF 69 { 70 "openrouter": { 71 "stages": ["scoring", "proposals"], 72 "resetAt": $future_ms, 73 "reason": "rate limit hit" 74 } 75 } 76 EOF 77 should_skip_for_rate_limits score_semantic; rc=$? 78 assert_returns "0" "$rc" "Active rate limit for scoring → skip score_semantic" 79 80 should_skip_for_rate_limits score_sites; rc=$? 81 assert_returns "0" "$rc" "Active rate limit for scoring → skip score_sites" 82 83 # ── Test 3: Expired rate limit (resetAt in the past) ──────────────────────── 84 85 echo "Test 3: Expired rate limit → returns 1 (don't skip)" 86 past_ms=$(node -e "console.log(Date.now() - 3600000)") # 1 hour ago 87 cat > "$TMPDIR/logs/rate-limits.json" << EOF 88 { 89 "openrouter": { 90 "stages": ["scoring"], 91 "resetAt": $past_ms, 92 "reason": "rate limit expired" 93 } 94 } 95 EOF 96 should_skip_for_rate_limits score_semantic; rc=$? 97 assert_returns "1" "$rc" "Expired rate limit returns 1" 98 99 # ── Test 4: Active rate limit but non-matching stage ───────────────────────── 100 101 echo "Test 4: Active rate limit for different stage → returns 1" 102 future_ms=$(node -e "console.log(Date.now() + 3600000)") 103 cat > "$TMPDIR/logs/rate-limits.json" << EOF 104 { 105 "zenrows": { 106 "stages": ["serps"], 107 "resetAt": $future_ms, 108 "reason": "daily quota" 109 } 110 } 111 EOF 112 should_skip_for_rate_limits score_semantic; rc=$? 113 assert_returns "1" "$rc" "Rate limit on serps does not block score_semantic" 114 115 # ── Test 5: Batch type → stage mapping ─────────────────────────────────────── 116 117 echo "Test 5: Batch type → stage mapping correctness" 118 future_ms=$(node -e "console.log(Date.now() + 3600000)") 119 120 # Test each mapping by creating a rate limit for the expected stage 121 for pair in \ 122 "proposals_email:proposals" \ 123 "proposals_sms:proposals" \ 124 "reword_email:reword" \ 125 "reword_sms:reword" \ 126 "reword_form:reword" \ 127 "reword_linkedin:reword" \ 128 "reword_x:reword" \ 129 "reply_responses:outreach" \ 130 "classify_replies:replies" \ 131 "score_semantic:scoring" \ 132 "score_sites:scoring" \ 133 "enrich_sites:enrich"; do 134 135 batch_type=$(echo "$pair" | cut -d: -f1) 136 expected_stage=$(echo "$pair" | cut -d: -f2) 137 138 cat > "$TMPDIR/logs/rate-limits.json" << EOF 139 { 140 "test_api": { 141 "stages": ["$expected_stage"], 142 "resetAt": $future_ms, 143 "reason": "test" 144 } 145 } 146 EOF 147 should_skip_for_rate_limits "$batch_type"; rc=$? 148 assert_returns "0" "$rc" "$batch_type maps to stage '$expected_stage'" 149 done 150 151 # ── Test 6: Unknown batch type → returns 1 ────────────────────────────────── 152 153 echo "Test 6: Unknown batch type → returns 1 (don't skip)" 154 future_ms=$(node -e "console.log(Date.now() + 3600000)") 155 cat > "$TMPDIR/logs/rate-limits.json" << EOF 156 { 157 "test_api": { 158 "stages": ["scoring", "enrich", "proposals", "outreach", "replies"], 159 "resetAt": $future_ms, 160 "reason": "everything blocked" 161 } 162 } 163 EOF 164 should_skip_for_rate_limits "unknown_batch_type"; rc=$? 165 assert_returns "1" "$rc" "Unknown batch type returns 1" 166 167 # ── Summary ────────────────────────────────────────────────────────────────── 168 169 echo "" 170 echo "Results: $PASS passed, $FAIL failed" 171 [ "$FAIL" -eq 0 ] || exit 1