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:
parent
55d9e52b43
commit
44a9a02a94
2 changed files with 225 additions and 0 deletions
3
Makefile
Normal file
3
Makefile
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@bash tests/test_profiles.sh
|
||||||
222
tests/test_profiles.sh
Executable file
222
tests/test_profiles.sh
Executable 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."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue