Исправить 7 багов после code review: экспорт ключа, утечка прокси, re-валидация
- Fix 1: export ANTHROPIC_API_KEY при ручном вводе ключа в claude_anthropic ([K]-ветка); без export subprocess claude не видел ключ и падал с ошибкой авторизации - Fix 2: trap RETURN в claude_gpt убивает прокси при любом ранем return 1, устраняя утечку фоновых процессов - Fix 3: sleep 1 заменён на poll-цикл (10 попыток, curl exit 7 = connection refused); connection refused теперь не маскируется под «нет сети» - Fix 4: после codex auth login в claude_gpt добавлена повторная проверка _claude_test_api; прежде claude запускался без подтверждения успешности reauth - Fix 5: аналогичная re-валидация в claude_gemini после браузерной авторизации (ветки 401/403 и 429) - Fix 6: prompt [c/Q] → [C/q] в 429-обработчике claude_anthropic — заглавная буква соответствует умолчанию (стандарт файла: CAPITAL = default) - Fix 7: trap 'rm -rf "$TMP"' EXIT — одинарные кавычки + кавычки вокруг $TMP, предотвращают word-split при путях с пробелами Добавлены тесты: tests/test_fixes.sh (21 тест, все проходят) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
624
claude_setup.sh
Normal file → Executable file
624
claude_setup.sh
Normal file → Executable file
@@ -87,7 +87,7 @@ install_proxy() {
|
||||
[ -z "$LATEST" ] && err "Не удалось получить версию claude-code-proxy с GitHub"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap "rm -rf $TMP" EXIT
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
URL="https://github.com/raine/claude-code-proxy/releases/download/${LATEST}/claude-code-proxy-linux-${ARCH_TAG}.tar.gz"
|
||||
info "Скачиваю $URL"
|
||||
curl -fsSL "$URL" -o "$TMP/proxy.tar.gz" || err "Не удалось скачать claude-code-proxy"
|
||||
@@ -146,12 +146,204 @@ cat >> "$BASHRC" << 'BASHEOF'
|
||||
|
||||
# === CLAUDE LAUNCHER ===
|
||||
|
||||
# ── Shared auth validation helpers ──────────────────────────
|
||||
|
||||
# _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint
|
||||
# Usage: _claude_test_api <url> <auth_header> [model]
|
||||
# auth_header format: "Authorization: Bearer TOKEN" or "x-api-key: KEY"
|
||||
# Side effects: Sets globals _CLAUDE_TEST_CODE, _CLAUDE_TEST_BODY
|
||||
_claude_test_api() {
|
||||
local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}"
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$url" \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \
|
||||
2>/dev/null || echo "000")
|
||||
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
|
||||
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||
}
|
||||
|
||||
# _claude_extract_error: Extract error.message from Anthropic-style error JSON
|
||||
_claude_extract_error() {
|
||||
local body="$1"
|
||||
echo "$body" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
e = d.get('error', {})
|
||||
if isinstance(e, str):
|
||||
print(e)
|
||||
else:
|
||||
msg = e.get('message', '')
|
||||
if msg:
|
||||
print(msg)
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# _claude_offer_reauth: Ask user if they want to re-authenticate
|
||||
# Returns 0 for yes, 1 for no
|
||||
_claude_offer_reauth() {
|
||||
local provider="$1"
|
||||
echo ""
|
||||
echo -n "Хотите выполнить повторную авторизацию $provider? [Y/n] "
|
||||
local answer
|
||||
read -r answer
|
||||
case "${answer:-Y}" in
|
||||
[Yy]|[Yy][Ee][Ss]) return 0 ;;
|
||||
*) echo "Отменено."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── claude_anthropic ──────────────────────────────────────────
|
||||
claude_anthropic() {
|
||||
unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_MODEL
|
||||
unset ANTHROPIC_DEFAULT_OPUS_MODEL ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
unset ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL
|
||||
unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN
|
||||
unset CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
|
||||
|
||||
local _token="" _method=""
|
||||
|
||||
# Извлекаем OAuth-токен из credentials
|
||||
_token=$(python3 -c "
|
||||
import json, os
|
||||
try:
|
||||
with open(os.path.expanduser('~/.claude/.credentials.json')) as f:
|
||||
d = json.load(f)
|
||||
print(d.get('claudeAiOauth', {}).get('accessToken', ''))
|
||||
except: pass
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -n "$_token" ]; then
|
||||
_method="oauth"
|
||||
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
||||
_token="$ANTHROPIC_API_KEY"
|
||||
_method="apikey"
|
||||
fi
|
||||
|
||||
# ── Pre-launch auth validation ──
|
||||
if [ "$_method" = "oauth" ]; then
|
||||
# OAuth-токен предназначен для внутренней авторизации Claude Code, а не для прямых API-вызовов.
|
||||
# Claude Code сам обрабатывает OAuth — прямой тест API с Bearer-токеном некорректен и даёт ложные 429.
|
||||
: # пропускаем проверку, Claude Code обработает авторизацию самостоятельно
|
||||
elif [ -n "$_token" ]; then
|
||||
echo -n "Проверка авторизации Anthropic... "
|
||||
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
|
||||
|
||||
local _emsg
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
;;
|
||||
401)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo -e "\033[0;31mОШИБКА: Недействительная авторизация (HTTP 401)\033[0m"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo ""
|
||||
echo "API-ключ недействителен."
|
||||
echo " export ANTHROPIC_API_KEY=sk-ant-..."
|
||||
echo ""
|
||||
read -r -p "Ввести новый ключ сейчас? [y/N] " _ans
|
||||
case "${_ans:-N}" in
|
||||
[Yy]*)
|
||||
read -r -p "Введите ключ: " _token
|
||||
[ -z "$_token" ] && { echo "Ключ не введён."; return 1; }
|
||||
echo -n "Проверяю новый ключ... "
|
||||
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200) echo -e "\033[0;32mOK\033[0m"; ANTHROPIC_API_KEY="$_token" ;;
|
||||
*) echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"; return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
403)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ПРЕДУПРЕЖДЕНИЕ]\033[0m Доступ запрещён (HTTP 403) — возможно, подписка не позволяет."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo ""
|
||||
echo -n "Продолжить всё равно? (запросы могут не работать) [y/N] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-N}" in [Yy]*) ;; *) return 1 ;; esac
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ЛИМИТ]\033[0m Лимит запросов исчерпан или пустой баланс (HTTP 429)."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo ""
|
||||
echo "Варианты:"
|
||||
echo " [C] Продолжить всё равно (может не работать)"
|
||||
echo " [Q] Выйти"
|
||||
echo -n "Выберите [C/q]: "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-C}" in
|
||||
[Cc]) ;; # продолжаем
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
000)
|
||||
echo ""
|
||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить авторизацию (нет сети?). Продолжаю..."
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo ""
|
||||
echo -n "Продолжить всё равно? [y/N] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-N}" in [Yy]*) ;; *) return 1 ;; esac
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Нет ни OAuth, ни API-ключа — предлагаем выбор
|
||||
echo ""
|
||||
echo -e "\033[1;33mАнтропная авторизация не найдена.\033[0m"
|
||||
echo ""
|
||||
echo "Варианты:"
|
||||
echo " [L] Залогиниться через браузер (OAuth)"
|
||||
echo " [K] Ввести API-ключ вручную"
|
||||
echo " [Q] Выйти"
|
||||
echo -n "Выберите [L/k/q]: "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-L}" in
|
||||
[Ll])
|
||||
echo ""
|
||||
echo "Открываю браузер для входа в Anthropic аккаунт..."
|
||||
claude auth login || {
|
||||
echo ""
|
||||
echo -e "\033[0;31mНе удалось выполнить вход.\033[0m"
|
||||
echo "Попробуйте вручную: claude auth login"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
[Kk])
|
||||
echo ""
|
||||
read -r -p "Введите Anthropic API ключ (sk-ant-...): " _token
|
||||
[ -z "$_token" ] && { echo "Ключ не введён."; return 1; }
|
||||
_method="apikey"
|
||||
echo -n "Проверяю ключ... "
|
||||
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200) echo -e "\033[0;32mOK\033[0m"; export ANTHROPIC_API_KEY="$_token" ;;
|
||||
*) echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"; return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) echo "Отменено."; return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Явно задаём модели Anthropic, чтобы не подхватился deepseek-chat из settings.json
|
||||
ANTHROPIC_MODEL=claude-sonnet-4-6 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-8 \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6 \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5-20251001 \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=claude-haiku-4-5-20251001 \
|
||||
claude "$@"
|
||||
}
|
||||
|
||||
@@ -183,8 +375,72 @@ claude_gpt() {
|
||||
if ! pgrep -f "claude-code-proxy serve" &>/dev/null; then
|
||||
"$proxy_bin" serve &>/tmp/claude-code-proxy.log &
|
||||
proxy_pid=$!
|
||||
sleep 1
|
||||
local _i=0
|
||||
while [ $_i -lt 10 ]; do
|
||||
sleep 1
|
||||
curl -sf --max-time 1 http://localhost:18765/ &>/dev/null; local _ce=$?
|
||||
[ "$_ce" -ne 7 ] && break # exit 7 = connection refused; любой другой = прокси слушает
|
||||
_i=$((_i + 1))
|
||||
done
|
||||
fi
|
||||
# Убиваем прокси при любом выходе из функции (early return или нормальный)
|
||||
trap '[ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null' RETURN
|
||||
|
||||
# ── Pre-launch API validation through proxy ──
|
||||
echo -n "Проверка авторизации ChatGPT... "
|
||||
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
|
||||
|
||||
local _emsg
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
;;
|
||||
401|403)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация ChatGPT недействительна (HTTP $_CLAUDE_TEST_CODE)."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo ""
|
||||
echo "Очищаю недействительную авторизацию..."
|
||||
"$proxy_bin" codex auth logout 2>/dev/null
|
||||
if _claude_offer_reauth "ChatGPT"; then
|
||||
echo "Запускаю повторную авторизацию..."
|
||||
if ! "$proxy_bin" codex auth login 2>&1; then
|
||||
echo ""
|
||||
echo "Если браузер не открылся, попробуйте device flow:"
|
||||
echo " claude-code-proxy codex auth device"
|
||||
return 1
|
||||
fi
|
||||
echo -n "Проверяю авторизацию после входа... "
|
||||
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
|
||||
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
|
||||
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
|
||||
return 1
|
||||
fi
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Лимит ChatGPT исчерпан."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
return 1
|
||||
;;
|
||||
000)
|
||||
echo ""
|
||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ChatGPT прокси (нет сети?). Продолжаю..."
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Прокси вернул HTTP $_CLAUDE_TEST_CODE"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
ANTHROPIC_BASE_URL=http://localhost:18765 \
|
||||
ANTHROPIC_AUTH_TOKEN=dummy \
|
||||
@@ -205,12 +461,66 @@ claude_gpt() {
|
||||
# ── claude_deepseek ───────────────────────────────────────────
|
||||
claude_deepseek() {
|
||||
local key_file="$HOME/.config/claude-launcher/deepseek_key"
|
||||
local api_key=""
|
||||
local api_key="" reauth=0
|
||||
local _emsg
|
||||
|
||||
# Read stored key
|
||||
if [ -f "$key_file" ]; then
|
||||
api_key=$(cat "$key_file")
|
||||
fi
|
||||
|
||||
# Validate stored key if present
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого DeepSeek ключа... "
|
||||
_claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "Authorization: Bearer $api_key" "deepseek-v4-flash"
|
||||
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
;;
|
||||
401|403)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Сохранённый ключ недействителен (HTTP $_CLAUDE_TEST_CODE)."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo "Удаляю недействительный ключ..."
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
reauth=1
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс DeepSeek пуст или превышен лимит."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo " Пополните баланс: https://platform.deepseek.com/top_up"
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-N}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
|
||||
;;
|
||||
000)
|
||||
echo ""
|
||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Re-prompt if key was invalidated
|
||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||
echo ""
|
||||
echo -n "Хотите ввести новый DeepSeek ключ? [Y/n] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-Y}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
|
||||
fi
|
||||
|
||||
# Prompt for key if missing
|
||||
if [ -z "$api_key" ]; then
|
||||
echo ""
|
||||
echo "DeepSeek API ключ не найден."
|
||||
@@ -225,43 +535,42 @@ claude_deepseek() {
|
||||
fi
|
||||
|
||||
echo "Проверяю ключ и баланс..."
|
||||
local http_code http_body err_msg
|
||||
http_body=$(curl -s -w "\n%{http_code}" \
|
||||
https://api.deepseek.com/anthropic/v1/messages \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d '{"model":"deepseek-v4-flash","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' 2>/dev/null || echo "000")
|
||||
http_code=$(echo "$http_body" | tail -1)
|
||||
http_body=$(echo "$http_body" | sed '$d')
|
||||
_claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "Authorization: Bearer $api_key" "deepseek-v4-flash"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo "Ключ действителен, баланс в порядке. Ключ сохранён."
|
||||
elif [ "$http_code" = "000" ]; then
|
||||
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
elif [ "$http_code" = "429" ]; then
|
||||
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
|
||||
echo " Причина: $err_msg"
|
||||
echo " Пополните баланс: https://platform.deepseek.com/top_up"
|
||||
echo " Ключ НЕ сохранён — сначала пополните счёт."
|
||||
return 1
|
||||
elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
|
||||
echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён."
|
||||
return 1
|
||||
else
|
||||
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "$http_body")
|
||||
echo "Ошибка API (HTTP $http_code): $err_msg"
|
||||
echo "Ключ не сохранён."
|
||||
return 1
|
||||
fi
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m"
|
||||
;;
|
||||
000)
|
||||
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
|
||||
[ -n "$_emsg" ] && echo " Причина: $_emsg"
|
||||
echo " Пополните баланс: https://platform.deepseek.com/top_up"
|
||||
echo " Ключ НЕ сохранён — сначала пополните счёт."
|
||||
return 1
|
||||
;;
|
||||
401|403)
|
||||
echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
[ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE"
|
||||
echo "Ошибка API: $_emsg"
|
||||
echo "Ключ не сохранён."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
|
||||
@@ -278,12 +587,66 @@ claude_deepseek() {
|
||||
# ── claude_kimi ─────────────────────────────────────────────
|
||||
claude_kimi() {
|
||||
local key_file="$HOME/.config/claude-launcher/kimi_key"
|
||||
local api_key=""
|
||||
local api_key="" reauth=0
|
||||
local _emsg
|
||||
|
||||
# Read stored key
|
||||
if [ -f "$key_file" ]; then
|
||||
api_key=$(cat "$key_file")
|
||||
fi
|
||||
|
||||
# Validate stored key if present
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого Kimi ключа... "
|
||||
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "Authorization: Bearer $api_key" "kimi-k2.6"
|
||||
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
;;
|
||||
401|403)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Сохранённый ключ недействителен (HTTP $_CLAUDE_TEST_CODE)."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo "Удаляю недействительный ключ..."
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
reauth=1
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс Kimi пуст или превышен лимит."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo " Пополните баланс: https://platform.moonshot.ai/console/billing"
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-N}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
|
||||
;;
|
||||
000)
|
||||
echo ""
|
||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Re-prompt if key was invalidated
|
||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||
echo ""
|
||||
echo -n "Хотите ввести новый Kimi ключ? [Y/n] "
|
||||
local _ans; read -r _ans
|
||||
case "${_ans:-Y}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
|
||||
fi
|
||||
|
||||
# Prompt for key if missing
|
||||
if [ -z "$api_key" ]; then
|
||||
echo ""
|
||||
echo "Kimi (Moonshot AI) API ключ не найден."
|
||||
@@ -298,43 +661,42 @@ claude_kimi() {
|
||||
fi
|
||||
|
||||
echo "Проверяю ключ и баланс..."
|
||||
local http_code http_body err_msg
|
||||
http_body=$(curl -s -w "\n%{http_code}" \
|
||||
https://api.moonshot.ai/anthropic/v1/messages \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d '{"model":"kimi-k2.6","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' 2>/dev/null || echo "000")
|
||||
http_code=$(echo "$http_body" | tail -1)
|
||||
http_body=$(echo "$http_body" | sed '$d')
|
||||
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "Authorization: Bearer $api_key" "kimi-k2.6"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo "Ключ действителен, баланс в порядке. Ключ сохранён."
|
||||
elif [ "$http_code" = "000" ]; then
|
||||
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
elif [ "$http_code" = "429" ]; then
|
||||
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
|
||||
echo " Причина: $err_msg"
|
||||
echo " Пополните баланс: https://platform.moonshot.ai/console/billing"
|
||||
echo " Ключ НЕ сохранён — сначала пополните счёт."
|
||||
return 1
|
||||
elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
|
||||
echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён."
|
||||
return 1
|
||||
else
|
||||
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "$http_body")
|
||||
echo "Ошибка API (HTTP $http_code): $err_msg"
|
||||
echo "Ключ не сохранён."
|
||||
return 1
|
||||
fi
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m"
|
||||
;;
|
||||
000)
|
||||
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
|
||||
[ -n "$_emsg" ] && echo " Причина: $_emsg"
|
||||
echo " Пополните баланс: https://platform.moonshot.ai/console/billing"
|
||||
echo " Ключ НЕ сохранён — сначала пополните счёт."
|
||||
return 1
|
||||
;;
|
||||
401|403)
|
||||
echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
[ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE"
|
||||
echo "Ошибка API: $_emsg"
|
||||
echo "Ключ не сохранён."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic \
|
||||
@@ -373,15 +735,32 @@ claude_gemini() {
|
||||
done
|
||||
fi
|
||||
|
||||
# Проверяем наличие аккаунта
|
||||
local acc_count
|
||||
acc_count=$(curl -sf "http://localhost:8080/v1/models" 2>/dev/null | \
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('data',[],'')))" 2>/dev/null || echo "0")
|
||||
|
||||
local has_auth
|
||||
# ── Проверка аккаунтов ──
|
||||
local has_auth total_count invalid_count
|
||||
has_auth=$(curl -sf "http://localhost:8080/account-limits" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$has_auth" ] || echo "$has_auth" | grep -qE '"accounts":\s*\[\]'; then
|
||||
if [ -n "$has_auth" ]; then
|
||||
total_count=$(echo "$has_auth" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
print(len(d.get('accounts', [])))
|
||||
except:
|
||||
print(0)
|
||||
" 2>/dev/null || echo "0")
|
||||
invalid_count=$(echo "$has_auth" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
accounts = d.get('accounts', [])
|
||||
invalid = [a for a in accounts if a.get('isInvalid')]
|
||||
print(len(invalid))
|
||||
except:
|
||||
print(0)
|
||||
" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
if [ -z "$has_auth" ] || [ "$total_count" = "0" ]; then
|
||||
echo ""
|
||||
echo "Google-аккаунт не найден."
|
||||
echo ""
|
||||
@@ -391,15 +770,86 @@ claude_gemini() {
|
||||
echo "Открываю http://localhost:8080 в браузере..."
|
||||
echo "Перейдите: Accounts → Add Account → войдите через Google."
|
||||
echo ""
|
||||
# Открываем браузер
|
||||
xdg-open "http://localhost:8080" 2>/dev/null || \
|
||||
sensible-browser "http://localhost:8080" 2>/dev/null || \
|
||||
echo "Откройте вручную: http://localhost:8080"
|
||||
|
||||
echo "Нажмите Enter после завершения авторизации в браузере..."
|
||||
read -r
|
||||
elif [ "$invalid_count" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ПРЕДУПРЕЖДЕНИЕ]\033[0m Обнаружены проблемные аккаунты ($invalid_count из $total_count)."
|
||||
echo "Проверьте статус: http://localhost:8080"
|
||||
fi
|
||||
|
||||
# ── Pre-launch API test through proxy ──
|
||||
echo -n "Проверка доступа к Gemini API... "
|
||||
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
|
||||
|
||||
local _emsg
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
200)
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
;;
|
||||
401|403)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Gemini аккаунты не авторизованы (HTTP $_CLAUDE_TEST_CODE)."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo "Попробуйте переавторизоваться через http://localhost:8080"
|
||||
if _claude_offer_reauth "Gemini"; then
|
||||
xdg-open "http://localhost:8080" 2>/dev/null || \
|
||||
sensible-browser "http://localhost:8080" 2>/dev/null || \
|
||||
echo "Откройте http://localhost:8080"
|
||||
echo "Нажмите Enter после авторизации..."
|
||||
read -r
|
||||
echo -n "Проверяю авторизацию Gemini... "
|
||||
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
|
||||
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
|
||||
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
|
||||
return 1
|
||||
fi
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
429)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Все Gemini аккаунты исчерпали лимит."
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
echo "Подождите сброса лимитов или добавьте новый аккаунт."
|
||||
if _claude_offer_reauth "Gemini (добавить аккаунт)"; then
|
||||
xdg-open "http://localhost:8080" 2>/dev/null || \
|
||||
sensible-browser "http://localhost:8080" 2>/dev/null || \
|
||||
echo "Откройте http://localhost:8080"
|
||||
echo "Нажмите Enter после добавления..."
|
||||
read -r
|
||||
echo -n "Проверяю авторизацию Gemini... "
|
||||
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
|
||||
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
|
||||
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
|
||||
return 1
|
||||
fi
|
||||
echo -e "\033[0;32mOK\033[0m"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
000)
|
||||
echo ""
|
||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить Gemini прокси (нет сети?). Продолжаю..."
|
||||
;;
|
||||
*)
|
||||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||||
echo ""
|
||||
echo -e "\033[0;31m[ОШИБКА]\033[0m Прокси вернул HTTP $_CLAUDE_TEST_CODE"
|
||||
[ -n "$_emsg" ] && echo " $_emsg"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Запускаю Claude Code с Gemini..."
|
||||
|
||||
ANTHROPIC_BASE_URL=http://localhost:8080 \
|
||||
|
||||
304
tests/test_fixes.sh
Executable file
304
tests/test_fixes.sh
Executable file
@@ -0,0 +1,304 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
# ── 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_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
|
||||
Reference in New Issue
Block a user