Claude Code now talks to the raw /anthropic path of the Eltex proxy, which forwards requests byte-for-byte to the Z.ai Anthropic endpoint, the same one ai-glm uses directly: - images work through Z.ai's server-side analyze_image (Read png -> server_tool_use analyze_image -> correct answer, same as ai-glm); the regular /v1/messages route converts to the OpenAI endpoint, where Z.ai rejects images with 400; - effort reaches Z.ai: low has no thinking, then medium < high < max; - prompt cache: ~99% of input is a cache read from the second turn, same as ai-glm. The key check and model pick still use the proxy root /v1/models. The model is glm-5.3 in every picker slot with [1m] for the 1M window; the anth-glm-5.3 / zai-glm-5.3 / flash names are gone. Effort stays max by default via CLI --effort. Checked on 20.10 on llm-t2.eltex.loc:4000; admins are moving that config to llm.eltex.loc:4000, which stays the default URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
721 lines
35 KiB
Bash
Executable File
721 lines
35 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")
|
||
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: 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_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_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
|