Files
ai-setup/tests/test_fixes.sh
Виталий Никитенко c74a435d89 feat(android): install Android CLI only on demand (setup.sh item 8)
ai-setup.sh no longer installs Google Android CLI or runs android init,
so menu items 1 and 7 (setup / update) leave it alone. The install
block moved verbatim into scripts/android-cli-setup.sh, which menu item
8 now runs instead of the whole ai-setup.sh.

The final summary of ai-setup.sh points to item 8, README describes the
opt-in install, tests check the new script and that ai-setup.sh does
not install Android CLI any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:49:53 +07:00

750 lines
36 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
ANDROID_SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/android-cli-setup.sh"
SETUP_MENU="$(cd "$(dirname "$0")/.." && pwd)/setup.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")
QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-qwen"/,/^QWENEOF/' "$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/anthropic"' \
&& 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 -Fq 'model="${ELTEX_MODEL:-${model:-glm-5.3}}"' \
&& echo "$ELTEX_SECTION" | grep -Fq 'pick("glm-5.3", "glm-5.3-", "flash")' \
&& ! echo "$ELTEX_SECTION" | grep -q 'ids\[0\]' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model"' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model"' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model"'; then
ok "ai-eltex: glm-5.3 by default, never the first /v1/models entry (ELTEX_MODEL overrides)"
else
fail "ai-eltex: must default to glm-5.3 (not the first /v1/models entry), ELTEX_MODEL overrides"
fi
}
# ── ai-eltex: glm-5.3 во всех слотах пикера (имена с суффиксом тоже) ───────
test_eltex_three_slots() {
local tmp ok_run=1 out
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
cat > "$tmp/mock/curl" <<'CURLEOF'
#!/usr/bin/env bash
printf '%s\n%s' "$MODELS_BODY" "200"
CURLEOF
cat > "$tmp/mock/claude" <<'CLAUDEEOF'
#!/usr/bin/env bash
echo "SLOTS opus=$ANTHROPIC_DEFAULT_OPUS_MODEL sonnet=$ANTHROPIC_DEFAULT_SONNET_MODEL haiku=$ANTHROPIC_DEFAULT_HAIKU_MODEL sub=$CLAUDE_CODE_SUBAGENT_MODEL"
CLAUDEEOF
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
echo sk-test > "$tmp/home/.config/ai-setup/eltex_key"
# Как на прокси: flash идёт первой, плюс имя группы доступа - во всех слотах glm-5.3
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"glm-5.3-flash"},{"id":"glm-5.3"},{"id":"ZAI-Test-Group"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1)
echo "$out" | grep -Fq 'SLOTS opus=glm-5.3[1m] sonnet=glm-5.3[1m] haiku=glm-5.3[1m] sub=glm-5.3[1m]' || ok_run=0
# Имена с суффиксом: модель не путается с flash
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"},{"id":"glm-5.3-2"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1)
echo "$out" | grep -Fq 'SLOTS opus=glm-5.3-2[1m] sonnet=glm-5.3-2[1m] haiku=glm-5.3-2[1m]' || ok_run=0
if [ "$ok_run" -eq 1 ]; then
ok "ai-eltex: glm-5.3[1m] in every picker slot, flash never picked"
else
fail "ai-eltex: every picker slot must be glm-5.3[1m], flash must never be picked"
fi
rm -rf "$tmp"
}
# ── ai-eltex: нет glm-5.3 у ключа -> авто-логаут, свежий ключ не сохраняется ──
test_eltex_autologout_on_missing_model() {
local tmp ok_run=1 rc=0 out
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
# Прокси отвечает 200, но glm-5.3 в списке моделей ключа нет
cat > "$tmp/mock/curl" <<'CURLEOF'
#!/usr/bin/env bash
if [[ "$*" == *"/v1/models"* ]]; then
printf '%s\n%s' "$MODELS_BODY" "200"
else
printf '%s\n%s' '{"ok":true}' "200"
fi
CURLEOF
printf '#!/usr/bin/env bash\nexit 0\n' > "$tmp/mock/claude"
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
# Сохранённый ключ: авто-логаут (файл удалён), новый ключ не вводим -> выход 1
echo sk-old > "$tmp/home/.config/ai-setup/eltex_key"
out=$(printf 'n\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"},{"id":"ZAI-Test-Group"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 1 ] || ok_run=0
echo "$out" | grep -q 'Ключу недоступна модель glm-5.3' || ok_run=0
echo "$out" | grep -q 'авто-логаут' || ok_run=0
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
# Свежий ключ без glm-5.3 не сохраняется вовсе
rc=0
out=$(printf 'sk-new\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 1 ] || ok_run=0
echo "$out" | grep -q 'Ключ НЕ сохранён' || ok_run=0
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
# ELTEX_MODEL перебивает проверку: ключ живёт, запуск идёт
echo sk-keep > "$tmp/home/.config/ai-setup/eltex_key"
rc=0
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"}]}' \
ELTEX_MODEL=glm-5.3-2 bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 0 ] || ok_run=0
echo "$out" | grep -q 'Модель: glm-5.3-2' || ok_run=0
[ "$(cat "$tmp/home/.config/ai-setup/eltex_key")" = "sk-keep" ] || ok_run=0
if [ "$ok_run" -eq 1 ]; then
ok "ai-eltex: missing glm-5.3 auto-logouts saved key, fresh key not saved, ELTEX_MODEL bypasses"
else
fail "ai-eltex: missing glm-5.3 must auto-logout (saved key removed, fresh key not saved, ELTEX_MODEL bypass)"
fi
rm -rf "$tmp"
}
# ── 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 -Fq 'glm-5.3*) opus_model="${model}[1m]"' \
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=' \
&& ! 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 window for glm-5.3 via [1m], no global MAX_CONTEXT / DISABLE_1M"
else
fail "ai-eltex: 1M for glm-5.3 via [1m], no global CLAUDE_CODE_MAX_CONTEXT_TOKENS / 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" <<CURLEOF
#!/usr/bin/env bash
for a in "\$@"; do
case "\$a" in
https://*) echo "\$a" >> "$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-qwen: Claude Code через Alibaba Token Plan, ключ вшит ───────────────
test_qwen_launcher() {
if echo "$QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$qwen_base_url"' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash' \
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
&& echo "$QWEN_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}"'; then
ok "ai-qwen: own config, qwen3.8-max in opus/sonnet/subagents, flash in haiku, 1M window, effort max"
else
fail "ai-qwen: must use cfg/qwen, qwen3.8-max/flash slots, 1M window, --model opus and --effort max"
fi
}
# ── ai-qwen: стартует сразу со вшитым ключом, ничего не спрашивая ─────────
test_qwen_starts_without_prompt() {
local tmp url token 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 "$QWEN_SECTION" | BIN_DIR="$tmp/bin" bash
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL $ANTHROPIC_AUTH_TOKEN" > "%s/claude.env"\n' "$tmp" > "$tmp/mock/claude"
chmod +x "$tmp/mock/claude"
(cd "$tmp" && HOME="$tmp/home" PATH="$tmp/mock:$PATH" AI_EFFORT= bash "$tmp/bin/ai-qwen" </dev/null >/dev/null 2>&1) || ok_run=0
read -r url token < "$tmp/claude.env" 2>/dev/null || ok_run=0
[ "${url:-}" = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic" ] || ok_run=0
case "${token:-}" in sk-sp-?*) ;; *) ok_run=0 ;; esac
if [ "$ok_run" -eq 1 ]; then
ok "ai-qwen: starts Claude Code with the built-in endpoint and key, no prompt"
else
fail "ai-qwen: must start Claude Code right away with the built-in Token Plan endpoint and sk-sp- key"
fi
rm -rf "$tmp"
}
# ── 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_MAX_OUTPUT_TOKENS=64000' \
&& 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, 64000 output tokens"
else
fail "ai-glm: must start with --effort \"\${AI_EFFORT:-xhigh}\", 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 >/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: opt-in install (setup.sh item 8), not part of ai-setup.sh ─
test_android_script_syntax() {
if bash -n "$ANDROID_SCRIPT" 2>&1; then
ok "android-cli: android-cli-setup.sh passes 'bash -n'"
else
fail "android-cli: android-cli-setup.sh has syntax errors"
fi
}
test_android_not_installed_by_default() {
if ! grep -q 'dl.google.com/android/cli' "$SCRIPT" && ! grep -q 'android init' "$SCRIPT"; then
ok "android-cli: ai-setup.sh does not install Android CLI by default"
else
fail "android-cli: ai-setup.sh still installs Android CLI"
fi
}
test_android_menu_item_runs_script() {
if awk '/^ 8\)/,/;;/' "$SETUP_MENU" | grep -q 'bash scripts/android-cli-setup.sh'; then
ok "android-cli: setup.sh item 8 runs android-cli-setup.sh"
else
fail "android-cli: setup.sh item 8 does not run android-cli-setup.sh"
fi
}
test_android_cli_install_url() {
if grep -q 'dl.google.com/android/cli/latest' "$ANDROID_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' "$ANDROID_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"' "$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_eltex_three_slots
test_eltex_autologout_on_missing_model
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_qwen_launcher
test_qwen_starts_without_prompt
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_script_syntax
test_android_not_installed_by_default
test_android_menu_item_runs_script
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