test: add profile-logic and installer test suite

bash tests/test_profiles.sh  (or: make test)

Covers the bug classes that surfaced during recent work, without any
external dependencies or network:

Profile logic (stub `claude` captures exported ANTHROPIC_* env):
- pro profile clears stale ANTHROPIC env and launches claude
- glm/free profiles export BASE_URL/AUTH_TOKEN/MODEL correctly
- error paths: missing OPENROUTER_API_KEY, unknown profile, bad mode
  each exit 1

Installer symlink logic (isolated temp HOME, throwaway clone):
- S1 broken/dangling symlink -> repointed via ln -sfn (was fatal before)
- S2 real directory at target -> warning, preserved (was silent)
- S3 free path -> new symlink created
- S4 existing symlink -> repointed

All tests use temp dirs for HOME and a throwaway repo clone, so the real
working tree is never touched.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-28 04:03:53 +02:00
commit 44a9a02a94
2 changed files with 225 additions and 0 deletions

3
Makefile Normal file
View file

@ -0,0 +1,3 @@
.PHONY: test
test:
@bash tests/test_profiles.sh

222
tests/test_profiles.sh Executable file
View file

@ -0,0 +1,222 @@
#!/usr/bin/env bash
#
# Tests for claude-launcher-profiles (profile logic + installer).
#
# Design:
# - A stub `claude` captures the ANTHROPIC_* env vars the launcher exports,
# so we can assert on them without a real Claude Code or network.
# - launch_profile calls `exec claude`, so every invocation runs inside a
# subshell — the exec then only ends the subshell, not the test runner.
# - Installer tests run with HOME pointed at a temp dir and a throwaway repo
# clone, so they never touch the real repo working tree.
#
# Run: bash tests/test_profiles.sh
# or: make test
set -u
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TESTS_RUN=0
TESTS_FAIL=0
FAILED_CASES=()
# ---- assert helpers --------------------------------------------------------
assert_eq() {
# assert_eq <label> <expected> <actual>
local label="$1" expected="$2" actual="$3"
TESTS_RUN=$((TESTS_RUN + 1))
if [[ "$expected" == "$actual" ]]; then
printf ' ok %s\n' "$label"
else
printf ' FAIL %s\n expected: <%s>\n actual: <%s>\n' \
"$label" "$expected" "$actual" >&2
TESTS_FAIL=$((TESTS_FAIL + 1))
FAILED_CASES+=("$label")
fi
}
assert_true() {
# assert_true <label> <condition-as-string>; runs the condition in a subshell
local label="$1"; shift
TESTS_RUN=$((TESTS_RUN + 1))
if ( "$@" ); then
printf ' ok %s\n' "$label"
else
printf ' FAIL %s (condition false)\n' "$label" >&2
TESTS_FAIL=$((TESTS_FAIL + 1))
FAILED_CASES+=("$label")
fi
}
assert_exit() {
# assert_exit <label> <expected_code> -- <cmd...>
local label="$1" expected="$2"; shift 2
[[ "$1" == "--" ]] && shift
local actual
"$@" >/dev/null 2>&1
actual=$?
TESTS_RUN=$((TESTS_RUN + 1))
if [[ "$expected" == "$actual" ]]; then
printf ' ok %s (exit %s)\n' "$label" "$actual"
else
printf ' FAIL %s: expected exit %s, got %s\n' "$label" "$expected" "$actual" >&2
TESTS_FAIL=$((TESTS_FAIL + 1))
FAILED_CASES+=("$label (exit $expected != $actual)")
fi
}
# ---- stub claude on PATH ----------------------------------------------------
STUB_DIR="$(mktemp -d)"
trap 'rm -rf "$STUB_DIR" "$REAL_CLONE" "$CAPTURE" "$H"' EXIT
export PATH="$STUB_DIR:$PATH"
cat > "$STUB_DIR/claude" <<'STUB'
#!/usr/bin/env bash
# If CLAUDE_ENV_CAPTURE is set, snapshot the exported ANTHROP_* vars into it.
if [[ -n "${CLAUDE_ENV_CAPTURE:-}" ]]; then
{
printf 'BASE_URL=%s\n' "${ANTHROPIC_BASE_URL:-}"
printf 'AUTH_TOKEN=%s\n' "${ANTHROPIC_AUTH_TOKEN:-}"
printf 'API_KEY=%s\n' "${ANTHROPIC_API_KEY:-}"
printf 'MODEL=%s\n' "${ANTHROPIC_MODEL:-}"
} > "$CLAUDE_ENV_CAPTURE"
fi
echo "stub-claude-invoked"
STUB
chmod +x "$STUB_DIR/claude"
# ---- throwaway repo clone with a test config -------------------------------
REAL_CLONE="$(mktemp -d)"
cp -r "$REPO_DIR"/. "$REAL_CLONE"/
chmod +x "$REAL_CLONE/bin/"* "$REAL_CLONE/lib/profiles.sh" 2>/dev/null || true
cat > "$REAL_CLONE/config/profiles.yml" <<'YML'
defaults:
claude_cmd: "claude"
openrouter_base_url: "https://openrouter.ai/api"
profiles:
pro:
mode: "pro"
glm:
mode: "openrouter"
model: "z-ai/glm-5.2"
free:
mode: "openrouter"
model: "openrouter/free"
badmode:
mode: "bogus"
YML
CAPTURE="$(mktemp)"
# Run a profile in a subshell (launch_profile execs claude -> ends subshell
# only). Env capture written into $CAPTURE.
run_profile() {
CLAUDE_ENV_CAPTURE="$CAPTURE" bash -c '
set -euo pipefail
source "$0/lib/profiles.sh"
launch_profile "$1"
' "$REAL_CLONE" "$1" >/dev/null 2>&1
}
field() { sed -n "s/^$1=//p" "$CAPTURE" 2>/dev/null; }
# ===========================================================================
echo "## Profile logic"
# ---------------------------------------------------------------------------
echo "-- pro profile clears ANTHROPIC env and launches claude"
export ANTHROPIC_BASE_URL="stale" ANTHROPIC_AUTH_TOKEN="stale" ANTHROPIC_MODEL="stale"
run_profile pro
assert_eq "pro: BASE_URL cleared" "" "$(field BASE_URL)"
assert_eq "pro: AUTH_TOKEN cleared" "" "$(field AUTH_TOKEN)"
assert_eq "pro: MODEL cleared" "" "$(field MODEL)"
echo "-- glm profile exports OpenRouter env"
export OPENROUTER_API_KEY="sk-or-v1-TEST123"
run_profile glm
assert_eq "glm: BASE_URL" "https://openrouter.ai/api" "$(field BASE_URL)"
assert_eq "glm: AUTH_TOKEN" "sk-or-v1-TEST123" "$(field AUTH_TOKEN)"
assert_eq "glm: API_KEY empty" "" "$(field API_KEY)"
assert_eq "glm: MODEL" "z-ai/glm-5.2" "$(field MODEL)"
echo "-- free profile exports configured model slug"
run_profile free
assert_eq "free: MODEL" "openrouter/free" "$(field MODEL)"
echo "-- error: missing OPENROUTER_API_KEY"
assert_exit "missing key exits 1" 1 -- env -u OPENROUTER_API_KEY bash -c '
set -euo pipefail
source "$0/lib/profiles.sh"
launch_profile glm
' "$REAL_CLONE"
echo "-- error: unknown profile"
assert_exit "unknown profile exits 1" 1 -- bash -c '
set -euo pipefail
source "$0/lib/profiles.sh"
launch_profile does-not-exist
' "$REAL_CLONE"
echo "-- error: unsupported mode"
assert_exit "bad mode exits 1" 1 -- bash -c '
set -euo pipefail
source "$0/lib/profiles.sh"
launch_profile badmode
' "$REAL_CLONE"
# ===========================================================================
echo "## Installer symlink logic"
# ---------------------------------------------------------------------------
# Run install-claude-profiles with HOME pointed at a temp dir, so the real
# repo working tree is never touched.
target_points_to_clone() {
local t="$1/src/claude-launcher-profiles"
[[ -L "$t" ]] && [[ "$(readlink "$t")" == "$REAL_CLONE" ]]
}
echo "-- S1: broken (dangling) symlink is repointed, not fatal"
H="$(mktemp -d)"; mkdir -p "$H/src"
ln -sfn /does/not/exist "$H/src/claude-launcher-profiles"
HOME="$H" bash "$REAL_CLONE/bin/install-claude-profiles" >/dev/null 2>&1
assert_true "S1 broken link repointed to clone" target_points_to_clone "$H"
rm -rf "$H"
echo "-- S2: real directory at target -> warning, left in place"
H="$(mktemp -d)"; mkdir -p "$H/src/claude-launcher-profiles"
OUT="$(HOME="$H" bash "$REAL_CLONE/bin/install-claude-profiles" 2>&1 >/dev/null)"
if [[ "$OUT" == *"exists and is not a symlink"* ]] && [[ -d "$H/src/claude-launcher-profiles" ]] && [[ ! -L "$H/src/claude-launcher-profiles" ]]; then
assert_eq "S2 real dir warned and preserved" "ok" "ok"
else
assert_eq "S2 real dir warned and preserved" "ok" "FAIL ($OUT)"
fi
rm -rf "$H"
echo "-- S3: free path -> new symlink created"
H="$(mktemp -d)"; mkdir -p "$H/src"
HOME="$H" bash "$REAL_CLONE/bin/install-claude-profiles" >/dev/null 2>&1
assert_true "S3 free path got a new symlink" target_points_to_clone "$H"
rm -rf "$H"
echo "-- S4: existing valid symlink -> repointed"
H="$(mktemp -d)"; mkdir -p "$H/src"
ln -sfn /some/other/old "$H/src/claude-launcher-profiles"
HOME="$H" bash "$REAL_CLONE/bin/install-claude-profiles" >/dev/null 2>&1
assert_true "S4 existing symlink repointed" target_points_to_clone "$H"
rm -rf "$H"
# ===========================================================================
echo
echo "------------------------------------------------------------"
printf 'tests: %d run, %d failed\n' "$TESTS_RUN" "$TESTS_FAIL"
if (( TESTS_FAIL > 0 )); then
printf 'FAILED cases:\n'
printf ' - %s\n' "${FAILED_CASES[@]}"
exit 1
fi
echo "all tests passed."