Claude Code не знает моделей glm-* и для glm-5.3 без суффикса считал окно = 200K: счётчик ctx: врал в ~5 раз, автокомпакт жал сессию в 200K. Окно теперь задаётся CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 (env применяется к не-claude моделям независимо от суффикса), поэтому [1m] у Haiku-слота стал дублирующим и убран — все три слота пикера теперь glm-5.3. Co-Authored-By: Claude <noreply@anthropic.com>
326 lines
14 KiB
Bash
Executable File
326 lines
14 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")
|
||
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-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_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
|