В Claude Code v2.x команда "claude auth login" запускает полный интерактивный TUI (включая "Welcome to Claude Code" и "Select login method"), а затем функция дополнительно вызывала claude "$@" — вторая сессия. Пользователь проходил выбор типа аккаунта дважды. Исправление: ветка [L] теперь запускает claude "$@" напрямую с нужными моделями и сразу делает return "$?", не допуская повторного вызова claude снизу функции. Claude сам обрабатывает весь auth flow (браузер → OAuth → выбор аккаунта) за одно взаимодействие. Добавлен тест test_fix8_no_double_login (3 проверки), итого 24/24. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
338 lines
11 KiB
Bash
Executable File
338 lines
11 KiB
Bash
Executable File
#!/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
|