Tests, licence, and the tooling to keep the repo consistent
A test suite with no dependencies beyond bash and the Python standard library, run by one command: scripts/test.sh (or: make test) scripts/test.sh --windows [host] adds the PowerShell suite over SSH - tests/static.sh: script syntax, JSON validity, VERSION against the widget manifest and CHANGELOG, providers.json against the kinds, probes and checks the code implements (and ids that would shadow a command), qmllint and shellcheck when installed. - tests/python: unit tests for cm-json.py - providers and the column contract bash depends on, the original three scaffolds byte for byte, every catalogue parser and the cache, session scan/dismiss/repair, rename, defaults, health, the cost guard. - tests/cli: the CLI in a sandbox with its own HOME, CM_ROOT, CLAUDE_CONFIG_DIR and a file-only vault, against fake Ollama, LM Studio, keyed-gateway and proxy servers - preflight, presets, models, doctor, a real switch, sessions, and the key helper under a rename race. - tests/windows: the same idea on a Windows host, with USERPROFILE pointed at a temp folder so the real install is never touched. Also: MIT licence, CHANGELOG.md reconstructed from history, .editorconfig and .gitattributes pinning LF everywhere (what the tree already is), and scripts/bump-version.sh, since the version lives in two files and drifted once before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# doctor on a server provider: model ids against the catalogue, and Ollama's
|
||||
# server-side context window.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
start_fake
|
||||
|
||||
ready_preset ollama "$OLLAMA_URL"
|
||||
set_state ollama ollama
|
||||
run_cm doctor
|
||||
expect_out "Ollama reachable at $OLLAMA_URL" 'the server is found'
|
||||
expect_out 'opus qwen3-coder [30.5B Q4_K_M]' 'a bare name matches its :latest tag'
|
||||
expect_no_out 'NOT available' 'no model is reported missing'
|
||||
expect_out 'loaded with a 4096-token context, below the declared 65536' 'the short server context is caught'
|
||||
expect_out 'OLLAMA_CONTEXT_LENGTH=65536' 'and the fix is named'
|
||||
|
||||
ready_preset lmstudio "$LMSTUDIO_URL"
|
||||
python3 "$J" set-all "$P/lmstudio.json" gemma-small >/dev/null
|
||||
set_state lmstudio lmstudio
|
||||
run_cm doctor
|
||||
expect_out 'gemma-small [not-loaded, ctx 8192]' 'lm studio models show their state'
|
||||
expect_out 'below the declared 262144 - this tier can overflow' 'a small model window is warned about'
|
||||
|
||||
python3 "$J" set-url "$P/lmstudio.json" http://127.0.0.1:1 >/dev/null
|
||||
run_cm doctor
|
||||
expect_out 'LM Studio not reachable' 'a dead server is a failure'
|
||||
|
||||
finish
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# The key helper Claude Code runs on a timer in every live session.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
H="$CM_ROOT/bin/claude-key-helper.sh"
|
||||
|
||||
helper() { OUT="$("$H" 2>&1)"; RC=$?; }
|
||||
|
||||
set_state anthropic
|
||||
helper
|
||||
expect_eq "$RC:$OUT" '0:' 'silent on anthropic'
|
||||
set_state lmstudio lmstudio
|
||||
helper
|
||||
expect_eq "$RC:$OUT" '0:' 'silent for an inline token'
|
||||
set_state lmstudio nosuch
|
||||
helper
|
||||
expect_rc 1 'a missing preset fails'
|
||||
expect_out 'does not exist' 'and says which'
|
||||
|
||||
"$CM" set-key zai sk-test-0123456789abcdef >/dev/null 2>&1
|
||||
set_state zai zai
|
||||
helper
|
||||
expect_eq "$OUT" sk-test-0123456789abcdef 'emits the vault key, and nothing else'
|
||||
|
||||
# Renaming the active preset while the helper loops: no fetch may see a gap.
|
||||
cp "$P/lmstudio.json" "$P/racer.json"
|
||||
set_state lmstudio racer
|
||||
(
|
||||
misses=0
|
||||
end=$((SECONDS + 4))
|
||||
while [ "$SECONDS" -lt "$end" ]; do
|
||||
"$H" >/dev/null 2>"$SB/helper.err" || { grep -q 'does not exist' "$SB/helper.err" && misses=$((misses + 1)); }
|
||||
done
|
||||
echo "$misses" > "$SB/misses"
|
||||
) &
|
||||
loop=$!
|
||||
cur=racer
|
||||
for i in $(seq 1 30); do
|
||||
"$CM" preset rename "$cur" "racer$i" >/dev/null 2>&1
|
||||
cur="racer$i"
|
||||
done
|
||||
wait "$loop"
|
||||
expect_eq "$(cat "$SB/misses")" 0 'no key fetch is lost to a rename'
|
||||
expect_eq "$(jget "$CM_ROOT/state.json" preset)" racer30 'state ends on the last name'
|
||||
|
||||
finish
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Model catalogues: fetched by each provider's kind, cached per server.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
start_fake
|
||||
C="$CM_ROOT/models-cache.json"
|
||||
|
||||
ready_preset ollama "$OLLAMA_URL"
|
||||
run_cm models --preset ollama
|
||||
expect_out 'qwen3-coder:latest' 'ollama models are listed'
|
||||
run_cm models --preset ollama --refresh
|
||||
expect_out 'cached 2 ollama model(s)' 'and cached'
|
||||
expect_eq "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["providers"]["ollama"]["baseUrl"])' "$C")" \
|
||||
"$OLLAMA_URL" 'against the server they came from'
|
||||
|
||||
ready_preset lmstudio "$LMSTUDIO_URL"
|
||||
run_cm models --preset lmstudio
|
||||
expect_out 'qwen3-coder-30b' 'lm studio models are listed'
|
||||
|
||||
python3 - "$P/custom.json" "$KEYED_URL" <<'PY'
|
||||
import json, sys
|
||||
p = json.load(open(sys.argv[1]))
|
||||
p.update(baseUrl=sys.argv[2], auth={"mode": "literal", "token": "sk-real"}, configured=True)
|
||||
json.dump(p, open(sys.argv[1], "w"), indent=2)
|
||||
PY
|
||||
run_cm models --preset custom
|
||||
expect_out 'gw/model-b' 'a keyed endpoint lists its models with the key'
|
||||
|
||||
cp "$P/custom.json" "$P/proxy.json"
|
||||
python3 "$J" set-url "$P/proxy.json" "$PROXY_URL" >/dev/null
|
||||
run_cm models --preset proxy --refresh
|
||||
expect_rc 1 'a proxy has no list to fetch'
|
||||
expect_out 'model ids can still be typed by hand' 'and that is explained'
|
||||
|
||||
run_cm models --preset zai
|
||||
expect_out 'glm-5.3' 'the static zai list'
|
||||
run_cm models --preset zai --json
|
||||
expect_out '"glm-4.7"' '--json prints the cached node'
|
||||
|
||||
run_cm models --preset nope --refresh
|
||||
expect_rc 1 'an unknown preset is refused'
|
||||
|
||||
for word in keyRef token auth; do
|
||||
case "$(cat "$C")" in *"\"$word\""*) fail "the cache holds a $word" ;; *) pass ;; esac
|
||||
done
|
||||
|
||||
finish
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preflight: what refuses a switch, and why - against fake servers.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
start_fake
|
||||
|
||||
code() { preflight_code "$@" | cut -d' ' -f1; }
|
||||
|
||||
expect_eq "$(code ollama)" needs-setup 'a shipped preset is set up before use'
|
||||
|
||||
ready_preset ollama "$OLLAMA_URL"
|
||||
expect_eq "$(code ollama)" ok 'ollama answers'
|
||||
cp "$P/ollama.json" "$P/dead.json"
|
||||
python3 "$J" set-url "$P/dead.json" http://127.0.0.1:1 >/dev/null
|
||||
expect_eq "$(code ollama dead)" server-unreachable 'a dead ollama is refused'
|
||||
|
||||
ready_preset lmstudio "$LMSTUDIO_URL"
|
||||
expect_eq "$(code lmstudio)" ok 'lm studio answers'
|
||||
|
||||
# custom: every tier empty first, then no address, then the key, then a proxy
|
||||
ready_preset custom
|
||||
expect_eq "$(preflight_code custom)" 'no-models edit-preset' 'a preset with no models is refused'
|
||||
python3 "$J" set-tier "$P/custom.json" opus gw/model-a
|
||||
expect_eq "$(preflight_code custom)" 'no-url set-url' 'a custom endpoint needs an address'
|
||||
python3 "$J" set-url "$P/custom.json" "$KEYED_URL" >/dev/null
|
||||
python3 "$J" set-auth "$P/custom.json" none >/dev/null
|
||||
expect_eq "$(code custom)" server-auth 'a keyed endpoint refuses the placeholder'
|
||||
cp "$P/custom.json" "$P/proxy.json"
|
||||
python3 "$J" set-url "$P/proxy.json" "$PROXY_URL" >/dev/null
|
||||
expect_eq "$(code custom proxy)" ok 'a proxy with no model list passes the lenient probe'
|
||||
|
||||
run_cm preset new b1 --provider openrouter --blank
|
||||
expect_eq "$(code openrouter b1)" no-models 'a blank preset cannot be switched to'
|
||||
|
||||
finish
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preset lifecycle: names, new/rename/rm, and the per-provider default.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
|
||||
run_cm preset show ../../etc/passwd
|
||||
expect_rc 1 'a path is not a preset name'
|
||||
expect_out 'invalid preset name' 'and it says why'
|
||||
run_cm preset new .hidden
|
||||
expect_rc 1 'a leading dot is refused'
|
||||
run_cm preset new a/b
|
||||
expect_rc 1 'a slash is refused'
|
||||
|
||||
run_cm preset new cheap
|
||||
expect_rc 0 'new copies default'
|
||||
expect_eq "$(jget "$P/cheap.json" provider)" openrouter 'the copy keeps the provider'
|
||||
run_cm preset new z2 --provider zai
|
||||
expect_out "from 'zai'" "--provider copies that provider's default"
|
||||
run_cm preset new z3 --provider zai default
|
||||
expect_rc 1 'copying across providers is refused'
|
||||
run_cm preset new b1 --provider ollama --blank
|
||||
expect_eq "$(jget "$P/b1.json" auth.token)" ollama "a blank preset carries its provider's token"
|
||||
run_cm preset new b2 --blank
|
||||
expect_rc 1 '--blank needs --provider'
|
||||
run_cm preset new x --provider foo
|
||||
expect_rc 1 'an unknown provider is refused'
|
||||
run_cm preset set nope opus x
|
||||
expect_rc 1 'setting a tier on a missing preset is refused'
|
||||
expect_no_file "$P/nope.json" 'and does not create it'
|
||||
|
||||
run_cm preset rename cheap cheap2
|
||||
expect_file "$P/cheap2.json" 'rename moves the file'
|
||||
expect_no_file "$P/cheap.json" 'and removes the old name'
|
||||
run_cm preset rename cheap2 default
|
||||
expect_rc 1 'renaming onto an existing preset is refused'
|
||||
set_state openrouter cheap2
|
||||
run_cm preset rename cheap2 cheap3
|
||||
expect_eq "$(jget "$CM_ROOT/state.json" preset)" cheap3 'state follows the active preset'
|
||||
run_cm preset rm cheap3
|
||||
expect_rc 1 'the active preset cannot be deleted'
|
||||
set_state anthropic
|
||||
|
||||
run_cm preset default openrouter cheap3
|
||||
expect_eq "$("$CM" preset default openrouter)" cheap3 'a chosen default is used'
|
||||
run_cm preset rename cheap3 cheap4
|
||||
expect_eq "$(jget "$CM_ROOT/defaults.json" openrouter)" cheap4 'the choice follows a rename'
|
||||
run_cm preset rm cheap4
|
||||
expect_eq "$(jget "$CM_ROOT/defaults.json" openrouter)" '' 'deleting clears the choice'
|
||||
expect_eq "$("$CM" preset default openrouter)" default 'and the built-in applies again'
|
||||
run_cm preset default openrouter zai
|
||||
expect_rc 1 "a default from another provider is refused"
|
||||
|
||||
"$CM" preset new c5 >/dev/null 2>&1
|
||||
"$CM" preset default openrouter c5 >/dev/null 2>&1
|
||||
rm -f "$P/c5.json"
|
||||
expect_eq "$("$CM" preset default openrouter)" default 'a vanished choice falls back'
|
||||
|
||||
run_cm preset rm z2
|
||||
run_cm preset rm zai
|
||||
expect_out 'that was the last zai preset' 'removing the last preset warns'
|
||||
|
||||
finish
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Every provider in providers.json is a mode: listed, dispatched, aliased.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
|
||||
run_cm --help
|
||||
for id in openrouter zai lmstudio ollama custom; do
|
||||
expect_out "claude-mode $id [preset]" "usage lists $id"
|
||||
done
|
||||
|
||||
run_cm presets
|
||||
expect_out 'ollama' 'presets lists ollama'
|
||||
expect_out 'custom' 'presets lists custom'
|
||||
|
||||
for word in zai Z.AI z-ai lm-studio OLLAMA; do
|
||||
code="$(preflight_code "$word")"
|
||||
expect_eq "${code%% *}" needs-setup "preflight resolves '$word'"
|
||||
done
|
||||
|
||||
run_cm nope
|
||||
expect_rc 1 'an unknown command fails'
|
||||
expect_out "unknown command 'nope'" 'and says so'
|
||||
|
||||
run_cm preset default
|
||||
expect_out 'ollama ollama (built-in)' 'ollama has a built-in default'
|
||||
expect_out 'custom custom (built-in)' 'custom has a built-in default'
|
||||
|
||||
finish
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# Broken transcripts: the scan, dismissing and restoring, the age rule, repair.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
|
||||
proj="$CLAUDE_CONFIG_DIR/projects/-tmp-fixture"
|
||||
mkdir -p "$proj"
|
||||
transcript() { # <session-id> [age-in-hours]
|
||||
printf '%s\n' \
|
||||
'{"type":"assistant","message":{"id":"msg_1","model":"claude-opus-5","content":[{"type":"text","text":"hi"}]}}' \
|
||||
'{"type":"assistant","message":{"id":"gen-1-a","model":"deepseek/x","content":[{"type":"text","text":"yo"}]}}' \
|
||||
> "$proj/$1.jsonl"
|
||||
python3 -c 'import os, sys, time; t = time.time() - float(sys.argv[2]) * 3600; os.utime(sys.argv[1], (t, t))' \
|
||||
"$proj/$1.jsonl" "${2:-0}"
|
||||
}
|
||||
transcript aaaa-new
|
||||
transcript bbbb-old 240
|
||||
transcript dddd-fix 1
|
||||
printf '%s\n' '{"type":"assistant","message":{"id":"msg_1"}}' > "$proj/cccc-ok.jsonl"
|
||||
|
||||
ids() { # <broken|ignored> - the session ids in that list of the scan
|
||||
"$CM" repair-session --json "${@:2}" | python3 -c '
|
||||
import json, sys
|
||||
print(" ".join(x["sessionId"] for x in json.load(sys.stdin)[sys.argv[1]]))' "$1"
|
||||
}
|
||||
|
||||
expect_eq "$(ids broken)" 'aaaa-new dddd-fix' 'broken transcripts are found'
|
||||
expect_eq "$(ids ignored)" 'bbbb-old' 'one untouched for 10 days is hidden'
|
||||
expect_eq "$(ids broken --max-age 0)" 'aaaa-new bbbb-old dddd-fix' '--max-age 0 shows all'
|
||||
|
||||
run_cm repair-session --ignore aaaa-new
|
||||
expect_out 'hidden aaaa-new' 'a session is dismissed'
|
||||
expect_eq "$(ids broken)" 'dddd-fix' 'and leaves the broken list'
|
||||
run_cm repair-session --all
|
||||
expect_out '2 hidden: 1 ignored, 1 older than 7 days' 'hidden sessions are always counted'
|
||||
run_cm repair-session --ignored
|
||||
expect_out 'still broken' 'the dismissed list reconciles against the disk'
|
||||
run_cm repair-session --ignore zzzz
|
||||
expect_rc 1 'an unknown session cannot be dismissed'
|
||||
run_cm repair-session --unignore aaaa-new
|
||||
expect_eq "$(ids broken)" 'aaaa-new dddd-fix' 'restoring brings it back'
|
||||
|
||||
run_cm repair-session --ignore dddd-fix
|
||||
run_cm repair-session dddd-fix --apply
|
||||
expect_rc 0 'a broken session is repaired'
|
||||
expect_eq "$(ls "$proj" | grep -c 'dddd-fix.jsonl.pre-repair-backup')" 1 'the original is backed up'
|
||||
expect_eq "$(ids ignored)" 'bbbb-old' 'a repair clears its dismissal'
|
||||
|
||||
run_cm repair-session --ignore bbbb-old
|
||||
rm -f "$proj/bbbb-old.jsonl"
|
||||
run_cm repair-session --ignore aaaa-new
|
||||
expect_out 'forgot 1 whose transcript no longer exists' 'entries for deleted transcripts are pruned'
|
||||
run_cm repair-session --unignore-all
|
||||
expect_out 'restored every dismissed session' 'unignore-all empties the list'
|
||||
|
||||
finish
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# A real switch - into the sandbox's own settings.json - and editing the preset
|
||||
# in use. --yes because the machine running the tests may well have Claude Code
|
||||
# sessions open; with --yes the CLI proceeds and leaves them alone.
|
||||
. "$(dirname "$0")/../lib.sh"
|
||||
start_fake
|
||||
S="$CLAUDE_CONFIG_DIR/settings.json"
|
||||
|
||||
ready_preset ollama "$OLLAMA_URL"
|
||||
run_cm ollama --yes
|
||||
expect_rc 0 'switch to ollama'
|
||||
expect_eq "$(jget "$S" env.ANTHROPIC_BASE_URL)" "$OLLAMA_URL" 'base url written'
|
||||
expect_eq "$(jget "$S" env.ANTHROPIC_AUTH_TOKEN)" ollama 'placeholder token written'
|
||||
expect_eq "$(jget "$S" env.ANTHROPIC_DEFAULT_OPUS_MODEL)" qwen3-coder 'tier mapped'
|
||||
expect_eq "$(jget "$S" env.CLAUDE_CODE_MAX_CONTEXT_TOKENS)" 65536 'context declared'
|
||||
expect_eq "$(jget "$S" env.CLAUDE_CODE_ATTRIBUTION_HEADER)" 0 'extra env written'
|
||||
expect_eq "$(jget "$CM_ROOT/state.json" mode)" ollama 'state records the mode'
|
||||
expect_eq "$(jget "$CM_ROOT/health.json" preset)" ollama 'health.json follows'
|
||||
|
||||
run_cm anthropic --yes
|
||||
expect_rc 0 'switch back to anthropic'
|
||||
expect_eq "$(jget "$S" env.ANTHROPIC_BASE_URL)" '' 'gateway env removed'
|
||||
expect_eq "$(jget "$S" env.CLAUDE_CODE_ATTRIBUTION_HEADER)" '' 'including the extra env'
|
||||
|
||||
# The preset in use: a tier edit re-applies without asking about sessions.
|
||||
set_state openrouter default
|
||||
run_cm preset set default sonnet test/model-c --force
|
||||
expect_rc 0 'a tier edit on the active preset re-applies'
|
||||
expect_no_out 'refusing to switch while sessions are running' 'without the sessions prompt'
|
||||
expect_eq "$(jget "$S" env.ANTHROPIC_DEFAULT_SONNET_MODEL)" test/model-c 'and reaches settings.json'
|
||||
|
||||
run_cm preset set default opus anthropic/claude-opus-5 --force
|
||||
expect_rc 1 'the cost guard still refuses an Anthropic model'
|
||||
expect_out 'saved, but re-applying the active preset failed' 'and says the edit was saved'
|
||||
|
||||
finish
|
||||
Reference in New Issue
Block a user