/ tests / scripts / orchestrator-rate-limits.test.sh
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