#!/usr/bin/env bash # tests/test_fixes.sh — unit tests for code-review fixes in claude_setup.sh # Run: bash tests/test_fixes.sh # Requires: bash 4+, curl (can be mocked via PATH) set -euo pipefail SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/claude_setup.sh" PASS=0; FAIL=0 ok() { echo "[PASS] $1"; PASS=$((PASS+1)); } fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); } # ── helpers ────────────────────────────────────────────────────────────────── # Source only the heredoc functions, not the setup-script body. # The heredoc begins after "cat >> \"$BASHRC\" << 'BASHEOF'" and contains # all the launcher functions; we extract and source that block directly. _source_functions() { local tmp tmp=$(mktemp) awk '/^# === CLAUDE LAUNCHER ===/,/^# === END CLAUDE LAUNCHER ===/' "$SCRIPT" > "$tmp" # shellcheck disable=SC1090 source "$tmp" rm -f "$tmp" } # ── Fix 1: ANTHROPIC_API_KEY exported in manual-key path ──────────────────── test_fix1_export_api_key() { # Extract the [Kk] branch from the script and confirm `export` keyword exists local kk_block kk_block=$(awk '/\[Kk\]/,/\[Ll\]/' "$SCRIPT" | grep 'ANTHROPIC_API_KEY') if echo "$kk_block" | grep -q 'export ANTHROPIC_API_KEY'; then ok "Fix1: [K] branch uses 'export ANTHROPIC_API_KEY'" else fail "Fix1: [K] branch missing 'export' for ANTHROPIC_API_KEY" fi } # ── Fix 2: trap RETURN kills proxy on early exit ───────────────────────────── test_fix2_trap_return() { if grep -q "trap '.*kill.*proxy_pid.*' RETURN" "$SCRIPT"; then ok "Fix2: trap RETURN for proxy cleanup present in claude_gpt" else fail "Fix2: trap RETURN for proxy cleanup missing in claude_gpt" fi } # ── Fix 3: readiness loop replaces bare sleep 1 ────────────────────────────── test_fix3_readiness_loop() { # The old code had just "sleep 1" after starting proxy; now there's a while loop local gpt_section gpt_section=$(awk '/^claude_gpt\(\)/,/^}/' "$SCRIPT") if echo "$gpt_section" | grep -q 'while \[ \$_i -lt'; then ok "Fix3: readiness poll loop present in claude_gpt proxy start" else fail "Fix3: readiness poll loop missing in claude_gpt" fi # Confirm bare "sleep 1" is gone from the proxy-start section (the loop contains sleep 1 but in context) # The old pattern was: proxy_pid=$!\n sleep 1\n fi if echo "$gpt_section" | grep -qP 'proxy_pid=\$!\n\s+sleep 1\n\s+fi'; then fail "Fix3: bare 'sleep 1' still present right after proxy_pid=\$!" else ok "Fix3: bare 'sleep 1; fi' pattern removed" fi } # ── Fix 3b: curl exit-7 logic correct ──────────────────────────────────────── test_fix3b_exit7_logic() { # Verify the comment and condition are as expected if grep -q 'exit 7 = connection refused' "$SCRIPT"; then ok "Fix3b: exit-7 comment present (connection refused check documented)" else fail "Fix3b: exit-7 comment missing" fi if grep -q '_ce.*-ne 7' "$SCRIPT"; then ok "Fix3b: [ \$_ce -ne 7 ] break condition present" else fail "Fix3b: exit-7 break condition missing" fi } # ── Fix 4: re-validate after claude_gpt reauth ─────────────────────────────── test_fix4_gpt_revalidate() { local gpt_section gpt_section=$(awk '/^claude_gpt\(\)/,/^}/' "$SCRIPT") if echo "$gpt_section" | grep -q 'Проверяю авторизацию после входа'; then ok "Fix4: re-validate after codex auth login present in claude_gpt" else fail "Fix4: re-validate after codex auth login missing in claude_gpt" fi } # ── Fix 5: re-validate after claude_gemini reauth (both 401 and 429) ───────── test_fix5_gemini_revalidate() { local gemini_section gemini_section=$(awk '/^claude_gemini\(\)/,/^}/' "$SCRIPT") local count count=$(echo "$gemini_section" | grep -c 'Проверяю авторизацию Gemini' || true) if [ "$count" -ge 2 ]; then ok "Fix5: re-validate after gemini reauth present in both 401/403 and 429 branches ($count occurrences)" else fail "Fix5: re-validate after gemini reauth missing or only in one branch (found $count)" fi } # ── Fix 6: prompt [C/q] matches default C in 429 handler ───────────────────── test_fix6_prompt_default() { # The prompt should now show [C/q] (capital C = default) matching case "${_ans:-C}" if grep -q '\[C/q\]' "$SCRIPT"; then ok "Fix6: prompt shows [C/q] — capital C signals default=continue" else fail "Fix6: prompt still shows [c/Q] (misleading) or was changed incorrectly" fi # Confirm the default in case is still C local anthropic_section anthropic_section=$(awk '/^claude_anthropic\(\)/,/^}/' "$SCRIPT") if echo "$anthropic_section" | grep -q '${_ans:-C}'; then ok "Fix6: default in case is still C (continue on Enter)" else fail "Fix6: default in case changed unexpectedly" fi } # ── Fix 7: trap quotes $TMP correctly ──────────────────────────────────────── test_fix7_trap_tmp() { # Should be single-quoted trap so $TMP expands at execution, not definition if grep -q "trap 'rm -rf \"\$TMP\"' EXIT" "$SCRIPT"; then ok "Fix7: trap uses single quotes with quoted \"\$TMP\"" else fail "Fix7: trap still uses double quotes or $TMP still unquoted at execution" fi # Old bad form should be gone if grep -q 'trap "rm -rf \$TMP" EXIT' "$SCRIPT"; then fail "Fix7: old unquoted trap form still present" else ok "Fix7: old unquoted trap form removed" fi } # ── _claude_test_api function isolation ────────────────────────────────────── test_globals_set_by_test_api() { _source_functions # Mock curl to return a fake 200 response curl() { echo '{"id":"msg_test"}' echo "200" } export -f curl _CLAUDE_TEST_CODE="" _CLAUDE_TEST_BODY="" _claude_test_api "http://fake.api/v1/messages" "x-api-key: testkey" "test-model" if [ "$_CLAUDE_TEST_CODE" = "200" ]; then ok "test_api: _CLAUDE_TEST_CODE set to 200" else fail "test_api: _CLAUDE_TEST_CODE='$_CLAUDE_TEST_CODE' expected '200'" fi if echo "$_CLAUDE_TEST_BODY" | grep -q '"id"'; then ok "test_api: _CLAUDE_TEST_BODY contains response body" else fail "test_api: _CLAUDE_TEST_BODY='$_CLAUDE_TEST_BODY' missing body" fi unset -f curl } test_globals_set_on_curl_fail() { _source_functions # curl fails → fallback "000" curl() { return 1; } export -f curl _CLAUDE_TEST_CODE="" _claude_test_api "http://unreachable/" "x-api-key: k" "m" if [ "$_CLAUDE_TEST_CODE" = "000" ]; then ok "test_api: _CLAUDE_TEST_CODE=000 on curl failure" else fail "test_api: expected 000 on curl failure, got '$_CLAUDE_TEST_CODE'" fi unset -f curl } # ── _claude_extract_error ──────────────────────────────────────────────────── test_extract_error_message() { _source_functions local body='{"type":"error","error":{"type":"authentication_error","message":"Invalid API key"}}' local result result=$(_claude_extract_error "$body") if [ "$result" = "Invalid API key" ]; then ok "extract_error: extracts error.message from JSON" else fail "extract_error: expected 'Invalid API key', got '$result'" fi } test_extract_error_empty_body() { _source_functions local result result=$(_claude_extract_error "not json at all") if [ -z "$result" ]; then ok "extract_error: returns empty string on non-JSON input" else fail "extract_error: unexpected output '$result' on bad input" fi } # ── _claude_offer_reauth ───────────────────────────────────────────────────── test_offer_reauth_yes() { _source_functions # Simulate user typing "Y" local result result=$(echo "Y" | ( _claude_offer_reauth "TestProvider" echo "retcode:$?" )) if echo "$result" | grep -q "retcode:0"; then ok "offer_reauth: returns 0 on 'Y'" else fail "offer_reauth: expected retcode 0 on Y, got: $result" fi } test_offer_reauth_no() { _source_functions local result result=$(echo "n" | ( _claude_offer_reauth "TestProvider" echo "retcode:$?" )) if echo "$result" | grep -q "retcode:1"; then ok "offer_reauth: returns 1 on 'n'" else fail "offer_reauth: expected retcode 1 on n, got: $result" fi } test_offer_reauth_enter_defaults_yes() { _source_functions local result result=$(echo "" | ( _claude_offer_reauth "TestProvider" echo "retcode:$?" )) if echo "$result" | grep -q "retcode:0"; then ok "offer_reauth: Enter (empty) defaults to Yes (retcode 0)" else fail "offer_reauth: Enter should default to Yes, got: $result" fi } # ── Fix8: no double login — [L] branch calls claude directly, not auth login ─ test_fix8_no_double_login() { # [L] branch must NOT call "claude auth login"; must call claude directly and return. # Find the line number of [Ll]) local ll_line ll_line=$(grep -n '^\s*\[Ll\])' "$SCRIPT" | head -1 | cut -d: -f1) # Extract the next 20 lines starting at [Ll]) — enough to cover the whole arm local ll_branch ll_branch=$(awk "NR>=$ll_line && NR<=$((ll_line+20))" "$SCRIPT") # Check for actual invocation (not just a comment mentioning the command) if echo "$ll_branch" | grep -v '^\s*#' | grep -q 'claude auth login'; then fail "Fix8: [L] branch still calls 'claude auth login' — double login present" else ok "Fix8: [L] branch does NOT call 'claude auth login' (only mentions it in a comment)" fi if echo "$ll_branch" | grep -qF 'return "$?"'; then ok "Fix8: [L] branch returns after launching claude (no fallthrough to outer call)" else fail "Fix8: [L] branch missing 'return \"\$?\"' — outer claude call still reached" fi if echo "$ll_branch" | grep -q 'ANTHROPIC_MODEL='; then ok "Fix8: [L] branch sets model env vars before launching claude" else fail "Fix8: [L] branch missing model env vars" fi } # ── bash syntax of the whole script ───────────────────────────────────────── test_script_syntax() { if bash -n "$SCRIPT" 2>&1; then ok "syntax: claude_setup.sh passes 'bash -n'" else fail "syntax: claude_setup.sh has syntax errors" fi } # ── run all tests ───────────────────────────────────────────────────────────── test_script_syntax test_fix8_no_double_login test_fix1_export_api_key test_fix2_trap_return test_fix3_readiness_loop test_fix3b_exit7_logic test_fix4_gpt_revalidate test_fix5_gemini_revalidate test_fix6_prompt_default test_fix7_trap_tmp test_globals_set_by_test_api test_globals_set_on_curl_fail test_extract_error_message test_extract_error_empty_body test_offer_reauth_yes test_offer_reauth_no test_offer_reauth_enter_defaults_yes echo "" echo "Results: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ] && exit 0 || exit 1