ai-eltex runs Claude Code through the Eltex LiteLLM proxy (http://llm.eltex.loc:4000, team GLM subscription). The personal key from admins is checked via /v1/models and stored in ~/.config/ai-setup/eltex_key; the model is taken from the key's allowed list (ELTEX_MODEL / ELTEX_BASE_URL override). The proxy ignores effort/thinking, so no effort forcing. /logout-eltex and /logout in ai-eltex drop the key; setup.sh status shows it. ru-bypass: hosts block now has an end marker and only that block is rewritten, manual entries outside it survive; llm.eltex.loc/llm2 added since the VPN DNS does not resolve it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
382 lines
17 KiB
Bash
Executable File
382 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||
# tests/test_fixes.sh - unit tests for code-review fixes in ai-setup.sh
|
||
# Run: bash tests/test_fixes.sh
|
||
|
||
set -euo pipefail
|
||
|
||
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/ai-setup.sh"
|
||
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
|
||
ADD_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/add-account-hook.sh"
|
||
PASS=0; FAIL=0
|
||
|
||
ok() { echo "[PASS] $1"; PASS=$((PASS+1)); }
|
||
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
|
||
|
||
# Extract sections
|
||
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' "$SCRIPT")
|
||
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' "$SCRIPT")
|
||
GLM_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-glm"/,/^GLMEOF/' "$SCRIPT")
|
||
ELTEX_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex"/,/^ELTEXEOF/' "$SCRIPT")
|
||
LOGOUT_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/logout-account-hook.sh"
|
||
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gemini"/,/^GEMINIEOF/' "$SCRIPT")
|
||
OPENCODE_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-opencode"/,/^OPENCODEEOF/' "$SCRIPT")
|
||
CURSOR_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-cursor"/,/^CURSOREOF/' "$SCRIPT")
|
||
HELPERS_SECTION=$(awk '/^cat > "\$HELPERS_FILE"/,/^HELPEREOF/' "$SCRIPT")
|
||
CODEX_SOUND_HOOKS_SECTION=$(awk '/^_setup_codex_sound_hooks\(\)/,/^}/' "$SCRIPT")
|
||
|
||
# ── ai-gpt: auto-install codex ────────────────────────────────────────────
|
||
test_gpt_autoinstall() {
|
||
if echo "$GPT_SECTION" | grep -q 'curl -fsSL https://chatgpt.com/codex/install.sh'; then
|
||
ok "ai-gpt: auto-installs codex via official install script"
|
||
else
|
||
fail "ai-gpt: missing codex auto-install"
|
||
fi
|
||
}
|
||
|
||
# ── ai-gpt: no proxy logic (simplified launcher) ──────────────────────────
|
||
test_gpt_no_proxy() {
|
||
if echo "$GPT_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||
fail "ai-gpt: still contains proxy logic (ANTHROPIC_BASE_URL)"
|
||
else
|
||
ok "ai-gpt: proxy logic removed (no ANTHROPIC_BASE_URL)"
|
||
fi
|
||
}
|
||
|
||
# ── ai-gpt: informative Codex status line ───────────────────────────────────
|
||
test_gpt_status_line() {
|
||
if echo "$GPT_SECTION" | grep -Fq 'tui.status_line=["model-with-reasoning","git-branch","context-used","five-hour-limit","current-dir"]'; then
|
||
ok "ai-gpt: shows model, branch, context usage, primary limit, and directory"
|
||
else
|
||
fail "ai-gpt: missing configured Codex status line"
|
||
fi
|
||
}
|
||
|
||
# ── ai-kimi: launches claude with Kimi backend ────────────────────────────
|
||
test_kimi_claude_launcher() {
|
||
if echo "$KIMI_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||
ok "ai-kimi: launches claude with ANTHROPIC_BASE_URL set"
|
||
else
|
||
fail "ai-kimi: must launch claude with ANTHROPIC_BASE_URL"
|
||
fi
|
||
}
|
||
|
||
# ── ai-kimi: uses official Kimi API ──────────────────────────────────────
|
||
test_kimi_official_api() {
|
||
if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \
|
||
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7' \
|
||
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6' \
|
||
&& ! echo "$KIMI_SECTION" | grep -q 'artemox'; then
|
||
ok "ai-kimi: uses official Kimi API (K2.7 opus/sonnet, K2.6 haiku)"
|
||
else
|
||
fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) with K2.7 opus/sonnet, K2.6 haiku"
|
||
fi
|
||
}
|
||
|
||
# ── ai-kimi: no artemox or config.toml logic ─────────────────────────────
|
||
test_kimi_no_artemox() {
|
||
if echo "$KIMI_SECTION" | grep -q 'config.toml' || echo "$KIMI_SECTION" | grep -q 'artemox'; then
|
||
fail "ai-kimi: still contains Artemox or config.toml logic"
|
||
else
|
||
ok "ai-kimi: Artemox and config.toml logic removed"
|
||
fi
|
||
}
|
||
|
||
# ── ai-glm: launches claude with GLM backend ──────────────────────────────
|
||
test_glm_claude_launcher() {
|
||
if echo "$GLM_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||
ok "ai-glm: launches claude with ANTHROPIC_BASE_URL set"
|
||
else
|
||
fail "ai-glm: must launch claude with ANTHROPIC_BASE_URL"
|
||
fi
|
||
}
|
||
|
||
# ── ai-glm: uses official Zhipu AI API ────────────────────────────────────
|
||
test_glm_official_api() {
|
||
if echo "$GLM_SECTION" | grep -q 'api.z.ai/api/anthropic' \
|
||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.3' \
|
||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-5.3' \
|
||
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||
&& echo "$GLM_SECTION" | grep -q 'x-api-key'; then
|
||
ok "ai-glm: uses official Zhipu AI API (glm-5.3 everywhere, ctx window 1M via env)"
|
||
else
|
||
fail "ai-glm: must use official Zhipu AI API with glm-5.3, 1M context window and x-api-key"
|
||
fi
|
||
}
|
||
|
||
# ── ai-eltex: Claude Code через корпоративный прокси Eltex ─────────────────
|
||
test_eltex_claude_launcher() {
|
||
if echo "$ELTEX_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'ELTEX_BASE_URL:-http://llm.eltex.loc:4000' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_BASE_URL="$eltex_base_url"' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_key"'; then
|
||
ok "ai-eltex: launches claude via llm.eltex.loc with its own key and config dir"
|
||
else
|
||
fail "ai-eltex: must launch claude via llm.eltex.loc with eltex_key and cfg/eltex"
|
||
fi
|
||
}
|
||
|
||
# ── ai-eltex: модель берётся из /v1/models ключа ──────────────────────────
|
||
test_eltex_model_from_key() {
|
||
if echo "$ELTEX_SECTION" | grep -q '/v1/models' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'model="${ELTEX_MODEL:-}"' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$model"' \
|
||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$model"'; then
|
||
ok "ai-eltex: model resolved from the key's /v1/models (ELTEX_MODEL overrides)"
|
||
else
|
||
fail "ai-eltex: model must come from /v1/models or ELTEX_MODEL"
|
||
fi
|
||
}
|
||
|
||
# ── logout hook: /logout-eltex удаляет eltex_key ──────────────────────────
|
||
test_logout_eltex() {
|
||
local tmp_home rc=0
|
||
tmp_home=$(mktemp -d)
|
||
mkdir -p "$tmp_home/.config/ai-setup"
|
||
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_key"
|
||
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
|
||
ok "logout hook: /logout in ai-eltex removes eltex_key"
|
||
else
|
||
fail "logout hook: /logout in ai-eltex must remove eltex_key (rc=$rc)"
|
||
fi
|
||
rm -rf "$tmp_home"
|
||
}
|
||
|
||
# ── ai-glm: default effort is xhigh (still overridable via AI_EFFORT) ─────
|
||
test_glm_default_effort() {
|
||
if echo "$GLM_SECTION" | grep -q -- '--effort "${AI_EFFORT:-xhigh}"' \
|
||
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1'; then
|
||
ok "ai-glm: starts with --effort xhigh by default, effort sent to Z.ai"
|
||
else
|
||
fail "ai-glm: must start with --effort \"\${AI_EFFORT:-xhigh}\" and CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1"
|
||
fi
|
||
}
|
||
|
||
# ── ai-gemini: native launcher generation ─────────────────────────────────
|
||
test_gemini_native_launcher() {
|
||
if echo "$GEMINI_SECTION" | grep -q 'antigravity CLI (agy)' \
|
||
&& echo "$GEMINI_SECTION" | grep -q 'https://antigravity.google/cli/install.sh'; then
|
||
ok "ai-gemini: native agy launcher is generated"
|
||
else
|
||
fail "ai-gemini: missing native agy launcher generation"
|
||
fi
|
||
}
|
||
|
||
# ── ai-cursor: native launcher generation ──────────────────────────────────
|
||
test_cursor_native_launcher() {
|
||
if echo "$CURSOR_SECTION" | grep -q 'Cursor CLI (agent)' \
|
||
&& echo "$CURSOR_SECTION" | grep -q 'https://cursor.com/install'; then
|
||
ok "ai-cursor: native Cursor CLI (agent) launcher is generated"
|
||
else
|
||
fail "ai-cursor: missing native Cursor CLI launcher generation"
|
||
fi
|
||
}
|
||
|
||
# ── global rules: source and native rule files ───────────────────────
|
||
test_global_rules_source_installed() {
|
||
if [ -s "$GLOBAL_RULES_SOURCE" ] \
|
||
&& grep -q 'GLOBAL_RULES_SOURCE=' "$SCRIPT" \
|
||
&& grep -q 'cp "$GLOBAL_RULES_SOURCE" "$CONFIG_DIR/global_rules.md"' "$SCRIPT"; then
|
||
ok "global rules: non-empty source file is installed as global_rules.md"
|
||
else
|
||
fail "global rules: missing source file or installation wiring"
|
||
fi
|
||
}
|
||
|
||
test_native_rule_files_generated() {
|
||
if grep -q 'cp "$CONFIG_DIR/global_rules.md" "$HOME/.codex/AGENTS.md"' "$SCRIPT" \
|
||
&& grep -q 'cp "$CONFIG_DIR/global_rules.md" "$HOME/.kimi-code/AGENTS.md"' "$SCRIPT" \
|
||
&& grep -q 'cp "$CONFIG_DIR/global_rules.md" "$HOME/.claude/CLAUDE.md"' "$SCRIPT" \
|
||
&& grep -q 'cp "$CONFIG_DIR/global_rules.md" "$HOME/.gemini/GEMINI.md"' "$SCRIPT" \
|
||
&& echo "$HELPERS_SECTION" | grep -q 'echo "$global_rendered" > "$HOME/.codex/AGENTS.md"' \
|
||
&& echo "$HELPERS_SECTION" | grep -q 'echo "$global_rendered" > "$HOME/.kimi-code/AGENTS.md"' \
|
||
&& echo "$HELPERS_SECTION" | grep -q 'echo "$global_rendered" > "$HOME/.claude/CLAUDE.md"' \
|
||
&& echo "$HELPERS_SECTION" | grep -q 'echo "$global_rendered" > "$HOME/.gemini/GEMINI.md"' \
|
||
&& ! echo "$HELPERS_SECTION" | grep -q 'echo "$rendered" >'; then
|
||
ok "global rules: setup and launchers write native rule files from global_rules.md"
|
||
else
|
||
fail "global rules: setup and launchers must write native rule files from global_rules.md"
|
||
fi
|
||
}
|
||
|
||
test_codex_sound_hooks_detach_stdio() {
|
||
local detached_count
|
||
detached_count=$(printf '%s\n' "$CODEX_SOUND_HOOKS_SECTION" | grep -Fc ') </dev/null >/dev/null 2>&1 &' || true)
|
||
if [ "$detached_count" -eq 2 ]; then
|
||
ok "codex hooks: background sound players detach standard streams"
|
||
else
|
||
fail "codex hooks: background sound players can keep hook pipes open"
|
||
fi
|
||
}
|
||
|
||
# ── Fix 7: trap quotes $TMP correctly ────────────────────────────────────────
|
||
test_fix7_trap_tmp() {
|
||
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
|
||
}
|
||
|
||
# ── bash syntax of the whole script ─────────────────────────────────────────
|
||
test_script_syntax() {
|
||
if bash -n "$SCRIPT" 2>&1; then
|
||
ok "syntax: ai-setup.sh passes 'bash -n'"
|
||
else
|
||
fail "syntax: ai-setup.sh has syntax errors"
|
||
fi
|
||
}
|
||
|
||
# ── Android CLI: install block exists ─────────────────────────────────────
|
||
test_android_cli_install_url() {
|
||
if grep -q 'dl.google.com/android/cli/latest' "$SCRIPT"; then
|
||
ok "android-cli: install URL for Android CLI is present"
|
||
else
|
||
fail "android-cli: missing install URL for Android CLI"
|
||
fi
|
||
}
|
||
|
||
test_android_init_called() {
|
||
if grep -q 'android init' "$SCRIPT"; then
|
||
ok "android-cli: android init is invoked after install"
|
||
else
|
||
fail "android-cli: missing android init invocation"
|
||
fi
|
||
}
|
||
|
||
test_android_symlink_to_bin_dir() {
|
||
if grep -q 'ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"' "$SCRIPT"; then
|
||
ok "android-cli: symlink to BIN_DIR is handled"
|
||
else
|
||
fail "android-cli: missing symlink to BIN_DIR"
|
||
fi
|
||
}
|
||
|
||
# ── ai-opencode: launches claude with routatic-proxy backend ──────────────────
|
||
test_opencode_claude_launcher() {
|
||
if echo "$OPENCODE_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||
ok "ai-opencode: launches claude with ANTHROPIC_BASE_URL set"
|
||
else
|
||
fail "ai-opencode: must launch claude with ANTHROPIC_BASE_URL"
|
||
fi
|
||
}
|
||
|
||
# ── ai-opencode: uses routatic-proxy (local proxy) ─────────────────────────────
|
||
test_opencode_routatic_proxy() {
|
||
if echo "$OPENCODE_SECTION" | grep -q 'routatic-proxy' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q '127.0.0.1:3456' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'ROUTATIC_PROXY_API_KEY'; then
|
||
ok "ai-opencode: uses routatic-proxy on 127.0.0.1:3456"
|
||
else
|
||
fail "ai-opencode: must use routatic-proxy with local proxy and API key env"
|
||
fi
|
||
}
|
||
|
||
# ── ai-opencode: manages opencode_key ──────────────────────────────────────────
|
||
test_opencode_key_management() {
|
||
if echo "$OPENCODE_SECTION" | grep -q 'opencode_key' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'AI_RESET_KEY' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'OPENCODE_GO_API_KEY'; then
|
||
ok "ai-opencode: opencode_key with AI_RESET_KEY and OPENCODE_GO_API_KEY env fallback"
|
||
else
|
||
fail "ai-opencode: must manage opencode_key with reset and env fallback"
|
||
fi
|
||
}
|
||
|
||
# ── ai-opencode: correct default models ────────────────────────────────────────
|
||
test_opencode_models() {
|
||
if echo "$OPENCODE_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="deepseek-v4-pro"' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="kimi-k2.6"' \
|
||
&& echo "$OPENCODE_SECTION" | grep -q 'CLAUDE_CODE_DISABLE_1M_CONTEXT=1'; then
|
||
ok "ai-opencode: correct default models (deepseek-v4-pro opus, kimi-k2.6 haiku)"
|
||
else
|
||
fail "ai-opencode: must have deepseek-v4-pro opus and kimi-k2.6 haiku"
|
||
fi
|
||
}
|
||
|
||
# ── add-account: логину нужен tty, фоновый запуск висит вечно ────────────────
|
||
test_add_account_no_headless_login() {
|
||
if grep -q 'claude auth login.*</dev/null' "$ADD_HOOK"; then
|
||
fail "add-account: логин запускается с закрытым stdin — код из браузера вставить некуда"
|
||
else
|
||
ok "add-account: логин не запускается в фоне с закрытым stdin"
|
||
fi
|
||
}
|
||
|
||
test_add_account_single_terminal_flow() {
|
||
if grep -q 'mkfifo' "$ADD_HOOK" \
|
||
&& grep -q 'add-account <код>' "$ADD_HOOK" \
|
||
&& ! grep -q 'gnome-terminal' "$ADD_HOOK"; then
|
||
ok "add-account: двухшаговый флоу в одном терминале, без второго окна"
|
||
else
|
||
fail "add-account: логин должен идти через fifo в текущем терминале, без отдельного окна"
|
||
fi
|
||
}
|
||
|
||
test_add_account_kills_by_pid() {
|
||
if grep -v '^[[:space:]]*#' "$ADD_HOOK" | grep -q 'pkill'; then
|
||
fail "add-account: pkill -f выкосит любой процесс с этой подстрокой в cmdline"
|
||
else
|
||
ok "add-account: процессы гасятся по своим PID, без широкого pkill -f"
|
||
fi
|
||
}
|
||
|
||
test_add_account_checks_login_result() {
|
||
if grep -q 'Login failed' "$ADD_HOOK" \
|
||
&& ! grep -q 'SAVED:' "$ADD_HOOK"; then
|
||
ok "add-account: результат логина проверяется, провал не выдаётся за успех"
|
||
else
|
||
fail "add-account: провал логина не должен маскироваться под успешное сохранение"
|
||
fi
|
||
}
|
||
|
||
test_add_hook_syntax() {
|
||
if bash -n "$ADD_HOOK" 2>&1; then
|
||
ok "syntax: add-account-hook.sh passes 'bash -n'"
|
||
else
|
||
fail "syntax: add-account-hook.sh has syntax errors"
|
||
fi
|
||
}
|
||
|
||
# ── run all tests ─────────────────────────────────────────────────────────────
|
||
test_script_syntax
|
||
test_gpt_autoinstall
|
||
test_gpt_no_proxy
|
||
test_gpt_status_line
|
||
test_kimi_claude_launcher
|
||
test_kimi_official_api
|
||
test_kimi_no_artemox
|
||
test_glm_claude_launcher
|
||
test_glm_official_api
|
||
test_glm_default_effort
|
||
test_eltex_claude_launcher
|
||
test_eltex_model_from_key
|
||
test_logout_eltex
|
||
test_gemini_native_launcher
|
||
test_cursor_native_launcher
|
||
test_global_rules_source_installed
|
||
test_native_rule_files_generated
|
||
test_codex_sound_hooks_detach_stdio
|
||
test_fix7_trap_tmp
|
||
test_android_cli_install_url
|
||
test_android_init_called
|
||
test_android_symlink_to_bin_dir
|
||
test_opencode_claude_launcher
|
||
test_opencode_routatic_proxy
|
||
test_opencode_key_management
|
||
test_opencode_models
|
||
test_add_hook_syntax
|
||
test_add_account_no_headless_login
|
||
test_add_account_single_terminal_flow
|
||
test_add_account_kills_by_pid
|
||
test_add_account_checks_login_result
|
||
|
||
echo ""
|
||
echo "Results: $PASS passed, $FAIL failed"
|
||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|