#!/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 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.*' "$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