#!/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") ELTEX_QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex-qwen"/,/^ELTEXQWENEOF/' "$SCRIPT") MIMO_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-mimo"/,/^MIMOEOF/' "$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 '\*" anth-glm-5.3 "\*) model="anth-glm-5.3"' \ && ! echo "$ELTEX_SECTION" | grep -q 'ids\[0\]' \ && echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$model"' \ && echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$model"'; then ok "ai-eltex: uses anth-glm-5.3, never the first /v1/models entry (ELTEX_MODEL overrides)" else fail "ai-eltex: must use anth-glm-5.3 (not the first /v1/models entry), ELTEX_MODEL overrides" fi } # ── ai-eltex: без ENABLE_TOOL_SEARCH (на anthropic-пути GLM теряет Read/Bash) ── test_eltex_no_tool_search() { if echo "$ELTEX_SECTION" | grep -q '^ENABLE_TOOL_SEARCH='; then fail "ai-eltex: must not force ENABLE_TOOL_SEARCH (GLM loses core tools on anthropic path)" else ok "ai-eltex: ENABLE_TOOL_SEARCH not forced" fi } # ── ai-eltex: окно 1M, как у ai-glm (иначе ctx: завышен в ~5 раз) ───────── test_eltex_context_window() { if echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \ && ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \ && ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_AUTO_COMPACT_WINDOW='; then ok "ai-eltex: 1M context window, no 200K compact window / DISABLE_1M" else fail "ai-eltex: must set CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 without DISABLE_1M / AUTO_COMPACT_WINDOW" fi } # ── ai-eltex-qwen: свой ключ и конфиг, модели Qwen, окно 200K ───────────── test_eltex_qwen_launcher() { if echo "$ELTEX_QWEN_SECTION" | grep -q 'claude --dangerously-skip-permissions' \ && echo "$ELTEX_QWEN_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_qwen_key"' \ && echo "$ELTEX_QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"' \ && echo "$ELTEX_QWEN_SECTION" | grep -q 'model="${ELTEX_QWEN_MODEL:-}"' \ && echo "$ELTEX_QWEN_SECTION" | grep -q '("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next")' \ && echo "$ELTEX_QWEN_SECTION" | grep -q 'api_key="${AI_ELTEX_LLM_API_KEY:-}"' \ && echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=1'; then ok "ai-eltex-qwen: own key/config, Coder-Senior then Flash-Next, 200K window" else fail "ai-eltex-qwen: must use eltex_qwen_key, cfg/eltex-qwen, Qwen model preference and DISABLE_1M" fi } test_logout_eltex_qwen() { 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_qwen_key" echo "sk-glm" > "$tmp_home/.config/ai-setup/eltex_key" echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex-qwen bash "$LOGOUT_HOOK" 2>/dev/null || rc=$? if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_qwen_key" ] && [ -f "$tmp_home/.config/ai-setup/eltex_key" ]; then ok "logout hook: /logout in ai-eltex-qwen removes only eltex_qwen_key" else fail "logout hook: /logout in ai-eltex-qwen must remove eltex_qwen_key and keep eltex_key (rc=$rc)" fi rm -rf "$tmp_home" } # ── ai-eltex / ai-eltex-qwen: effort max по умолчанию через CLI, вывод 64000 ──── test_eltex_effort() { if echo "$ELTEX_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \ && echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1' \ && echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \ && ! echo "$ELTEX_SECTION" | grep -q '^_apply_effort'; then ok "ai-eltex: starts with --effort max by default, AI_EFFORT via CLI, 64000 output tokens" else fail "ai-eltex: must start with --effort \"\${AI_EFFORT:-max}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1, MAX_OUTPUT_TOKENS=64000, no _apply_effort" fi } test_eltex_qwen_effort() { if echo "$ELTEX_QWEN_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \ && echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \ && ! echo "$ELTEX_QWEN_SECTION" | grep -q '^_apply_effort'; then ok "ai-eltex-qwen: starts with --effort max by default, 64000 output tokens" else fail "ai-eltex-qwen: must start with --effort \"\${AI_EFFORT:-max}\" and MAX_OUTPUT_TOKENS=64000, no _apply_effort" fi } # ── ai-eltex: команда /logout-eltex зарегистрирована (иначе Unknown command, хук не зовётся) ── test_eltex_logout_command() { if echo "$ELTEX_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"'; then ok "ai-eltex: registers /logout-eltex command so the logout hook receives it" else fail "ai-eltex: must create \$CLAUDE_CONFIG_DIR/commands/logout-eltex.md" 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-mimo: Claude Code через Xiaomi MiMo V2.6 ─────────────────────────── test_mimo_launcher() { if echo "$MIMO_SECTION" | grep -q 'claude --dangerously-skip-permissions' \ && echo "$MIMO_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/mimo_key"' \ && echo "$MIMO_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"' \ && echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$mimo_base_url"' \ && echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro' \ && echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro' \ && echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro' \ && echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro' \ && echo "$MIMO_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus ' \ && echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000' \ && echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \ && ! echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \ && echo "$MIMO_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md"'; then ok "ai-mimo: own key/config, Pro in every slot and subagents, --model opus, 128000 output, 1M window, /logout-mimo" else fail "ai-mimo: must use mimo_key, cfg/mimo, mimo-v2.6-pro everywhere, --model opus, 128000 output, 1M window and logout-mimo.md" fi } # ── ai-mimo: эндпоинт региона Token Plan находится перебором и запоминается ── test_mimo_region_probe() { local tmp ok_run=1 tmp=$(mktemp -d) mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home" echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash echo "$MIMO_SECTION" | BIN_DIR="$tmp/bin" bash # Ключ подходит только к региону ams, sgp недоступен по сети (как curl: -w печатает # 000 и exit 28), остальные эндпоинты отвечают 401. MOCK_DOWN=1 - сети нет совсем. cat > "$tmp/mock/curl" <> "$tmp/curl.log" ;; esac case "\$a" in https://*) [ -n "\${MOCK_DOWN:-}" ] && { printf '\n000'; exit 28; } ;; esac case "\$a" in https://api.xiaomimimo.com/*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"Insufficient account balance"}}\n402'; exit 0; } ;; https://token-plan-*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"invalid api key"}}\n401'; exit 0; } ;; esac case "\$a" in https://token-plan-sgp.xiaomimimo.com/*) printf '\n000'; exit 28 ;; https://token-plan-ams.xiaomimimo.com/*) printf '{"content":[]}\n200'; exit 0 ;; https://*) printf '{"error":{"message":"invalid api key"}}\n401'; exit 0 ;; esac done CURLEOF printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL" > "%s/claude.url"\n' "$tmp" > "$tmp/mock/claude" chmod +x "$tmp/mock/curl" "$tmp/mock/claude" _run_mimo() { (cd "$tmp" && printf 'tp-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \ MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" >/dev/null 2>&1) } local ams=https://token-plan-ams.xiaomimimo.com/anthropic _run_mimo || ok_run=0 [ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0 [ "$(cat "$tmp/home/.config/ai-setup/mimo_base_url" 2>/dev/null)" = "$ams" ] || ok_run=0 [ "$(cat "$tmp/home/.config/ai-setup/mimo_key" 2>/dev/null)" = "tp-test" ] || ok_run=0 # Повторный запуск: сохранённый эндпоинт, без перебора регионов. rm -f "$tmp/curl.log" "$tmp/claude.url" _run_mimo || ok_run=0 [ "$(cat "$tmp/curl.log" 2>/dev/null)" = "$ams/v1/messages" ] || ok_run=0 [ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0 # Без сети: ключ не сохраняется и Claude Code не запускается с пустым эндпоинтом. rm -rf "$tmp/home" "$tmp/claude.url" MOCK_DOWN=1 _run_mimo && ok_run=0 [ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0 # Ключ pay-as-you-go без баланса: Token Plan отвечает 401, api.xiaomimimo.com - 402, # нужна подсказка про ключ tp-... rm -rf "$tmp/home" local out out=$(cd "$tmp" && printf 'sk-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" MOCK_SK=1 \ MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" 2>&1) && ok_run=0 echo "$out" | grep -q 'Dedicated API Key' || ok_run=0 [ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0 if [ "$ok_run" -eq 1 ]; then ok "ai-mimo: finds the Token Plan region by probing (skips unreachable), saves and reuses it" else fail "ai-mimo: region probe must skip unreachable regions, pick the one that accepts the key and reuse it" fi rm -rf "$tmp" } # ── ai-mimo: effort max по умолчанию через CLI (MiMo effort игнорирует) ──── test_mimo_effort() { if echo "$MIMO_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \ && ! echo "$MIMO_SECTION" | grep -q '^_apply_effort'; then ok "ai-mimo: starts with --effort max by default, AI_EFFORT via CLI" else fail "ai-mimo: must start with --effort \"\${AI_EFFORT:-max}\", no _apply_effort" fi } test_logout_mimo() { local tmp_home rc=0 tmp_home=$(mktemp -d) mkdir -p "$tmp_home/.config/ai-setup" echo "tp-test" > "$tmp_home/.config/ai-setup/mimo_key" echo "https://token-plan-ams.xiaomimimo.com/anthropic" > "$tmp_home/.config/ai-setup/mimo_base_url" echo '{"prompt":"/logout-mimo"}' | HOME="$tmp_home" AI_LAUNCHER=claude bash "$LOGOUT_HOOK" 2>/dev/null || rc=$? if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_key" ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_base_url" ]; then ok "logout hook: /logout-mimo removes mimo_key and mimo_base_url" else fail "logout hook: /logout-mimo must remove mimo_key and mimo_base_url (rc=$rc)" fi rm -rf "$tmp_home" } # ── ai-glm: default effort is max (still overridable via AI_EFFORT) ─────── test_glm_default_effort() { if echo "$GLM_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \ && echo "$GLM_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \ && echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1'; then ok "ai-glm: starts with --effort max by default, effort sent to Z.ai, 64000 output tokens" else fail "ai-glm: must start with --effort \"\${AI_EFFORT:-max}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 and MAX_OUTPUT_TOKENS=64000" 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_eltex_no_tool_search test_eltex_context_window test_eltex_logout_command test_eltex_effort test_eltex_qwen_effort test_logout_eltex test_eltex_qwen_launcher test_logout_eltex_qwen test_mimo_launcher test_mimo_region_probe test_mimo_effort test_logout_mimo 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