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:
smoido
2026-09-15 01:42:38 +03:00
co-authored by Claude Opus 5
parent b5824c6611
commit 51e42823d1
22 changed files with 1437 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Matches what is already in the tree. Everything is LF, the PowerShell and
# .cmd files included: PowerShell 5.1 and cmd.exe both read LF, while a CRLF
# shell script fails its #! line. The PowerShell files are also kept ASCII-only,
# because 5.1 reads a .ps1 without a BOM in the ANSI codepage.
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{qml,js,json}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+8
View File
@@ -0,0 +1,8 @@
# LF in every working tree, Windows checkouts included. A CRLF shell script
# fails its #! line, and the packages are built from a Windows checkout
# (scripts/build-package.ps1 normalises too, as a backstop).
* text=auto eol=lf
*.png binary
*.jpg binary
*.ico binary
+2
View File
@@ -1,3 +1,5 @@
dist/ dist/
*.bak *.bak
*.bak.* *.bak.*
__pycache__/
*.pyc
+68
View File
@@ -0,0 +1,68 @@
# Changelog
All notable changes to claude-mode. Versions follow [semver](https://semver.org):
a minor version adds behaviour, a patch fixes it. The version lives in `VERSION`
and in the bar widget's `omarchy/smoido.claude-mode/manifest.json`, and
`scripts/bump-version.sh` changes both.
## 1.12.0 — 2026-09-15
- **Providers are data.** Every gateway provider is an entry in `providers.json`:
its endpoint, auth, model-list format, pre-switch probe, setup, doctor checks
and presentation. The bash CLI, the Windows build and the bar widget all read
it, and a provider that reuses existing behaviour needs no code.
- **Ollama** mode: local server, one model for every tier, `doctor` reads the
context each loaded model actually runs with (Ollama defaults to 4096 tokens
and truncates silently).
- **Custom** mode: any Anthropic-compatible endpoint; refused until it has an
address; a proxy with no model list is not treated as broken.
- Preflight refuses a preset with no server address. The Windows build gains
the no-address and no-models guards it never had.
## 1.11.0 — 2026-09-15
- Preset editor in the bar panel: edit a tier from a searchable, cached model
list; create, duplicate, rename and delete presets.
- `claude-mode preset default <provider> <name>` chooses which preset
`claude-mode <provider>` picks (`defaults.json`).
- `models --preset/--refresh/--json`, and a model cache every fetch keeps fresh.
- A tier edit to the active preset re-applies without the running-sessions
prompt (it moves neither endpoint nor key).
- Preset names are validated (`preset show ../../etc/passwd` used to print it).
- The key helper reads the active preset in one open, so renaming it cannot
catch a running session mid-lookup.
- The panel moved to `KeyboardPanel`: no text field in the old popup could
ever receive typing.
## 1.10.0 — 2026-09-14
- Dismiss a broken session (`repair-session --ignore`), and hide broken ones
untouched for 7 days; hidden ones are always counted, never silently dropped.
- The panel offers Ignore beside Repair, with a collapsed hidden section.
## 1.9.3 — 2026-09-13
- `set-key [ref] [key]` takes the ref it was given, and a key inline; the
Windows build builds the SecureString for an inline key too.
## 1.9.2 — 2026-09-12
- Transcript lookup uses the project directory Claude Code actually uses: every
non-alphanumeric character becomes a dash, and the physical path is slugged.
## 1.9.1 — 2026-09-12
- Runs on macOS: bash 3.2, arrow keys, honest session reporting without `/proc`.
- The `apiKeyHelper` path is quoted, and the helper says why it failed.
## 1.9.0 — 2026-09-01
- One-line installs from the repository (`irm | iex`, `curl | bash`).
## 1.8.x and earlier — 2026-08-30 to 2026-08-31
- Linux port with a theme-aware TUI, switch preflight and session control.
- Omarchy bar widget: shows the active provider and switches from the bar.
- Asks before switching while sessions are live; repairs sessions a switch
broke, keeping the cut turns as Markdown and handing them back.
- One preset per mode, with a first-run setup that fills it in.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 smoido
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+18
View File
@@ -0,0 +1,18 @@
# A thin wrapper. The real commands live in scripts/ and the installers, so
# everything also works where make is not installed.
.PHONY: test test-windows install install-widget version
test:
scripts/test.sh
test-windows:
scripts/test.sh --windows
install:
bash linux/install.sh
install-widget:
bash omarchy/install.sh
version:
@cat VERSION
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Set the version everywhere it is recorded: VERSION (read by both CLIs and
# stamped into health.json) and the bar widget's manifest. tests/static.sh
# fails when the two disagree - they drifted apart once already (1.8.0 against
# 1.9.3) with nothing to notice.
#
# scripts/bump-version.sh 1.13.0
set -euo pipefail
cd "$(dirname "$0")/.."
new="${1:-}"
if ! [[ "$new" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "usage: scripts/bump-version.sh <major.minor.patch> (now $(cat VERSION))" >&2
exit 1
fi
printf '%s\n' "$new" > VERSION
python3 - "$new" <<'PY'
import json, re, sys
path = "omarchy/smoido.claude-mode/manifest.json"
text = open(path, encoding="utf-8").read()
# A targeted substitution rather than a json.dump, so the file's own layout
# (key order, spacing, the kinds array on one line) is left alone.
new = re.sub(r'("version"\s*:\s*")[^"]*(")', lambda m: m.group(1) + sys.argv[1] + m.group(2), text, count=1)
json.loads(new)
open(path, "w", encoding="utf-8").write(new)
PY
echo "version $new - add a CHANGELOG.md entry for it"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Everything that can be checked from this machine, in one command.
#
# scripts/test.sh static checks, Python unit tests, CLI tests
# scripts/test.sh --windows [host] ...and the PowerShell suite on a Windows host
# over SSH (default host: winbox)
#
# Nothing here touches the real install. The CLI tests run in a sandbox with its
# own HOME, CM_ROOT and CLAUDE_CONFIG_DIR and a file-only vault (tests/lib.sh),
# and the Windows suite points USERPROFILE at a temp folder (tests/windows/run.ps1).
set -uo pipefail
cd "$(dirname "$0")/.."
windows=0
host=winbox
while [ $# -gt 0 ]; do
case "$1" in
--windows)
windows=1
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then host="$2"; shift; fi ;;
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
*) echo "unknown option '$1'" >&2; exit 2 ;;
esac
shift
done
failed=''
step() {
local name="$1"; shift
"$@" || failed="$failed $name"
}
printf '== static\n'
step static bash tests/static.sh
printf '\n== python\n'
step python env PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests/python -p 'test_*.py'
printf '\n== cli\n'
for t in tests/cli/test_*.sh; do
step "cli/$(basename "$t" .sh)" bash "$t"
done
if [ "$windows" -eq 1 ]; then
printf '\n== windows (%s)\n' "$host"
step windows bash tests/windows/run-remote.sh "$host"
fi
printf '\n'
if [ -z "$failed" ]; then
echo 'all passed'
else
echo "FAILED:$failed"
exit 1
fi
+27
View File
@@ -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
+45
View File
@@ -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
+46
View File
@@ -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
+34
View File
@@ -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
+61
View File
@@ -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
+27
View File
@@ -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
+55
View File
@@ -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
+36
View File
@@ -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
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Fake provider servers for the CLI and Windows tests, all on 127.0.0.1.
ollama GET /api/tags tagged names only, as the real server lists them
GET /api/ps qwen3-coder:latest loaded with a 4096-token context
POST /api/show a 262144-token maximum
lmstudio GET /api/v0/models
keyed GET /v1/models, only with `Bearer sk-real` or `x-api-key: sk-real`
proxy 404 for everything - a Messages-only proxy with no model list
Ports are chosen by the OS. The first line printed is a JSON map of kind to
port, which the harness reads before running anything.
"""
import json
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def send(self, code, obj):
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
kind, path = self.server.kind, self.path
if kind == "ollama" and path == "/api/tags":
return self.send(200, {"models": [
{"name": "qwen3-coder:latest", "model": "qwen3-coder:latest",
"details": {"parameter_size": "30.5B", "quantization_level": "Q4_K_M", "family": "qwen3moe"}},
{"name": "glm-4.7:cloud", "model": "glm-4.7:cloud", "details": {}}]})
if kind == "ollama" and path == "/api/ps":
return self.send(200, {"models": [
{"name": "qwen3-coder:latest", "model": "qwen3-coder:latest", "context_length": 4096}]})
if kind == "lmstudio" and path == "/api/v0/models":
return self.send(200, {"data": [
{"id": "qwen3-coder-30b", "state": "loaded", "max_context_length": 262144},
{"id": "gemma-small", "state": "not-loaded", "max_context_length": 8192}]})
if kind == "keyed" and path == "/v1/models":
if self.headers.get("authorization") == "Bearer sk-real" or self.headers.get("x-api-key") == "sk-real":
return self.send(200, {"data": [{"id": "gw/model-a"}, {"id": "gw/model-b"}]})
return self.send(401, {"error": "unauthorized"})
return self.send(404, {"error": "not found"})
def do_POST(self):
self.rfile.read(int(self.headers.get("content-length") or 0))
if self.server.kind == "ollama" and self.path == "/api/show":
return self.send(200, {"model_info": {"general.architecture": "qwen3moe",
"qwen3moe.context_length": 262144}})
return self.send(404, {"error": "not found"})
def main():
ports = {}
for kind in ("ollama", "lmstudio", "keyed", "proxy"):
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
server.kind = kind
ports[kind] = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
print(json.dumps(ports), flush=True)
threading.Event().wait()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(0)
Executable
+117
View File
@@ -0,0 +1,117 @@
# Shared by tests/cli/test_*.sh - source it first.
#
# Sourcing gives the test its own sandbox: a HOME, CM_ROOT and CLAUDE_CONFIG_DIR
# of its own, the repository's scripts installed into it the way linux/install.sh
# lays them out, and the vault forced to the plain-file backend. Nothing on the
# machine running the tests is read or written - not the real keyring, not
# ~/.claude-mode, not ~/.claude/settings.json - and a switch performed by a test
# only rewrites the sandbox's settings.json.
#
# Assertions count rather than stop, so one run reports every failure; the
# file ends with `finish`, whose status is the test's.
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
T_NAME="$(basename "$0" .sh)"
T_PASS=0
T_FAIL=0
OUT=''
RC=0
FAKE_PID=''
SB="$(mktemp -d "${TMPDIR:-/tmp}/cm-test.XXXXXX")"
mkdir -p "$SB/root/bin" "$SB/root/presets" "$SB/home/.claude/projects"
cp "$REPO/linux/claude-mode" "$REPO/linux/cm-json.py" "$REPO/linux/cm-vault.sh" \
"$REPO/linux/claude-key-helper.sh" "$SB/root/bin/"
if [ -d "$REPO/linux/lib" ]; then cp -R "$REPO/linux/lib" "$SB/root/bin/"; fi
chmod +x "$SB/root/bin/claude-mode" "$SB/root/bin/claude-key-helper.sh"
cp "$REPO"/presets/*.json "$SB/root/presets/"
cp "$REPO/providers.json" "$REPO/VERSION" "$SB/root/"
export HOME="$SB/home"
export CM_ROOT="$SB/root"
export CLAUDE_CONFIG_DIR="$SB/home/.claude"
export CLAUDE_MODE_VAULT=file
unset CM_IGNORE_AGE_DAYS CLAUDE_MODE_THEME
CM="$SB/root/bin/claude-mode"
J="$SB/root/bin/cm-json.py"
P="$SB/root/presets"
set_state() {
printf '{"mode":"%s","preset":"%s","writtenEnvKeys":[]}\n' "$1" "${2:-}" > "$CM_ROOT/state.json"
}
set_state anthropic
t_cleanup() {
if [ -n "$FAKE_PID" ]; then kill "$FAKE_PID" 2>/dev/null; fi
rm -rf "$SB"
}
trap t_cleanup EXIT
# Starts tests/fake_server.py and sets OLLAMA_URL, LMSTUDIO_URL, KEYED_URL and
# PROXY_URL from the ports it chose.
start_fake() {
python3 "$REPO/tests/fake_server.py" > "$SB/ports" 2>/dev/null &
FAKE_PID=$!
local i
for i in $(seq 1 50); do
[ -s "$SB/ports" ] && break
sleep 0.1
done
[ -s "$SB/ports" ] || { echo "$T_NAME: the fake server did not start" >&2; exit 1; }
eval "$(python3 -c '
import json, sys
for kind, port in json.load(open(sys.argv[1])).items():
print("%s_URL=http://127.0.0.1:%d" % (kind.upper(), port))
' "$SB/ports")"
}
ESC="$(printf '\033')"
# run_cm <args...>: the CLI's combined output, colour codes removed, in OUT;
# its exit status in RC.
run_cm() {
OUT="$("$CM" "$@" 2>&1)"
RC=$?
OUT="$(printf '%s' "$OUT" | sed "s/${ESC}\[[0-9;]*m//g")"
}
pass() { T_PASS=$((T_PASS + 1)); }
fail() {
T_FAIL=$((T_FAIL + 1))
printf ' FAIL %s: %s\n' "$T_NAME" "$*"
if [ -n "$OUT" ]; then printf '%s\n' "$OUT" | tail -n 8 | sed 's/^/ | /'; fi
}
expect_rc() { if [ "$RC" -eq "$1" ]; then pass; else fail "$2 (exit $RC, wanted $1)"; fi; }
expect_out() { case "$OUT" in *"$1"*) pass ;; *) fail "$2 (output lacks: $1)" ;; esac; }
expect_no_out() { case "$OUT" in *"$1"*) fail "$2 (output has: $1)" ;; *) pass ;; esac; }
expect_eq() { if [ "$1" = "$2" ]; then pass; else fail "$3 (got '$1', wanted '$2')"; fi; }
expect_file() { if [ -e "$1" ]; then pass; else fail "$2 (no $1)"; fi; }
expect_no_file(){ if [ -e "$1" ]; then fail "$2 ($1 exists)"; else pass; fi; }
jget() { python3 "$J" get "$1" "$2"; }
# The preflight verdict for a mode (and preset) as "<code> <remedyKind>".
preflight_code() {
"$CM" preflight "$@" 2>/dev/null | python3 -c '
import json, sys
d = json.load(sys.stdin)
print(d["code"], d["remedyKind"])'
}
# Point a shipped preset at a fake server and mark it set up, so preflight
# gets past "not set up yet" to the checks under test.
ready_preset() {
python3 "$J" set-flag "$P/$1.json" configured true
[ -n "${2:-}" ] && python3 "$J" set-url "$P/$1.json" "$2" >/dev/null
return 0
}
finish() {
printf '%-22s %3d passed' "$T_NAME" "$T_PASS"
if [ "$T_FAIL" -gt 0 ]; then printf ', %d FAILED' "$T_FAIL"; fi
printf '\n'
[ "$T_FAIL" -eq 0 ]
}
+393
View File
@@ -0,0 +1,393 @@
"""Unit tests for linux/cm-json.py - the JSON engine behind the POSIX CLI.
Standard library only. cm-json.py is loaded from the repository, so it reads the
repository's providers.json (it looks one level above itself). Every test that
writes works in a temporary directory.
"""
import contextlib
import importlib.util
import io
import json
import os
import shutil
import sys
import tempfile
import time
import unittest
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_spec = importlib.util.spec_from_file_location("cm_json", os.path.join(REPO, "linux", "cm-json.py"))
cm = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(cm)
def call(fn, argv, stdin=""):
"""Run a cm-json command function; return what it printed."""
out, old = io.StringIO(), sys.stdin
sys.stdin = io.StringIO(stdin)
try:
with contextlib.redirect_stdout(out):
fn(argv)
finally:
sys.stdin = old
return out.getvalue()
def call_json(fn, argv, stdin=""):
return json.loads(call(fn, argv, stdin))
class TempDir(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="cm-json-test.")
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def path(self, *parts):
return os.path.join(self.tmp, *parts)
def write_json(self, rel, data):
p = self.path(rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "w", encoding="utf-8") as fh:
json.dump(data, fh)
return p
# ---------------------------------------------------------------------------
# providers.json
# ---------------------------------------------------------------------------
class Providers(unittest.TestCase):
def test_order(self):
self.assertEqual([p["id"] for p in cm.providers()],
["openrouter", "zai", "lmstudio", "ollama", "custom"])
def test_builtin_defaults(self):
self.assertEqual(cm.builtin_default_preset(), {
"openrouter": "default", "zai": "zai", "lmstudio": "lmstudio",
"ollama": "ollama", "custom": "custom"})
def test_tsv_column_contract(self):
# The bash CLI addresses these columns by number (see prov_field in
# linux/claude-mode). Reordering them silently breaks it; append only.
self.assertEqual([name for name, _ in cm.PROVIDER_TSV], [
"id", "aliases", "title", "label", "color", "defaultPreset",
"serverEditable", "probe", "probePaths", "catalogueKind", "perServer",
"setupKey", "setupModels", "keyUrl", "guardrail", "doctor",
"defaultKeyRef", "literalToken", "defaultBaseUrl", "serverHint", "serverStart"])
for line in call(cm.cmd_provider_tsv, []).splitlines():
self.assertEqual(len(line.split("\t")), len(cm.PROVIDER_TSV), line)
def test_resolve(self):
for word, want in [("zai", "zai"), ("Z.AI", "zai"), ("z-ai", "zai"),
("lm-studio", "lmstudio"), ("OLLAMA", "ollama")]:
self.assertEqual(call(cm.cmd_provider_resolve, [word]).strip(), want)
for word in ("anthropic", "nope", ""):
with self.assertRaises(SystemExit):
call(cm.cmd_provider_resolve, [word])
def test_static_catalogue(self):
self.assertEqual(call(cm.cmd_provider_static, ["zai"]).splitlines()[0].split("\t")[0], "glm-5.3")
class Scaffold(unittest.TestCase):
def blank(self, **over):
base = {"provider": None, "description": "new preset", "baseUrl": None, "auth": None,
"models": {t: "" for t in cm.TIERS}, "subagentModel": "inherit",
"gatewayModelDiscovery": False, "contextTokens": None}
base.update(over)
return json.dumps(base, indent=2) + "\n"
# The original three, exactly as the hard-coded scaffold produced them
# before providers.json existed.
def test_openrouter(self):
self.assertEqual(call(cm.cmd_scaffold, ["openrouter"]), self.blank(
provider="openrouter", baseUrl="https://openrouter.ai/api",
auth={"mode": "vault", "keyRef": "openrouter"}, gatewayModelDiscovery=True,
contextTokens=1000000))
def test_zai(self):
want = json.loads(self.blank(
provider="zai", baseUrl="https://api.z.ai/api/anthropic",
auth={"mode": "vault", "keyRef": "zai"}, contextTokens=1000000))
want["extraEnv"] = {"API_TIMEOUT_MS": "3000000", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"}
self.assertEqual(call(cm.cmd_scaffold, ["zai"]), json.dumps(want, indent=2) + "\n")
def test_lmstudio(self):
want = json.loads(self.blank(
provider="lmstudio", baseUrl="http://127.0.0.1:1234",
auth={"mode": "literal", "token": "lmstudio"}, contextTokens=262144))
want["extraEnv"] = {"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"}
self.assertEqual(call(cm.cmd_scaffold, ["lmstudio"]), json.dumps(want, indent=2) + "\n")
def test_new_providers(self):
o = json.loads(call(cm.cmd_scaffold, ["ollama"]))
self.assertEqual((o["baseUrl"], o["auth"], o["contextTokens"]),
("http://127.0.0.1:11434", {"mode": "literal", "token": "ollama"}, 65536))
c = json.loads(call(cm.cmd_scaffold, ["custom"]))
self.assertEqual((c["baseUrl"], c["auth"]), ("", {"mode": "vault", "keyRef": "custom"}))
def test_unknown(self):
with self.assertRaises(SystemExit):
call(cm.cmd_scaffold, ["nope"])
# ---------------------------------------------------------------------------
# Catalogues
# ---------------------------------------------------------------------------
class Parsers(unittest.TestCase):
def test_openrouter(self):
src = {"data": [{"id": "b/x", "context_length": 1000, "pricing": {"prompt": "0.000001", "completion": "0.000002"}},
{"id": "a/y", "context_length": 5, "pricing": {}}]}
self.assertEqual(call(cm.cmd_or_models, [], json.dumps(src)).splitlines(),
["a/y\t5\t\t", "b/x\t1000\t1.0\t2.0"])
def test_lmstudio(self):
src = {"data": [{"id": "m", "state": "loaded", "max_context_length": 8192}]}
self.assertEqual(call(cm.cmd_lms_models, [], json.dumps(src)), "m\tloaded\t8192\n")
def test_ollama_placeholders(self):
# "-" rather than empty, so bash's tab-IFS read cannot merge columns.
src = {"models": [{"name": "glm:cloud", "details": {}},
{"name": "q:latest", "details": {"parameter_size": "7B", "quantization_level": "Q4"}}]}
self.assertEqual(call(cm.cmd_ollama_models, [], json.dumps(src)).splitlines(),
["glm:cloud\t-\t-\t-", "q:latest\t7B\tQ4\t-"])
def test_openai_both_shapes(self):
self.assertEqual(call(cm.cmd_openai_models, [], json.dumps({"data": [{"id": "b"}, {"id": "a"}]})), "a\nb\n")
self.assertEqual(call(cm.cmd_openai_models, [], json.dumps([{"id": "x"}])), "x\n")
class Cache(TempDir):
def cache(self, provider, ok, base, tsv):
call(cm.cmd_cache_models, [self.path("c.json"), provider, "1" if ok else "0", base], tsv)
with open(self.path("c.json")) as fh:
return json.load(fh)["providers"][provider]
def test_per_server_and_failure(self):
node = self.cache("ollama", True, "http://a:1/", "q:latest\t7B\tQ4\t-\n")
self.assertEqual((node["baseUrl"], node["models"]), ("http://a:1", [{"id": "q:latest", "note": "7B Q4"}]))
node = self.cache("ollama", False, "http://a:1", "")
self.assertEqual((node["ok"], len(node["models"])), (False, 1)) # a stale list beats none
node = self.cache("ollama", False, "http://b:2", "")
self.assertEqual((node["baseUrl"], node["models"]), ("http://b:2", [])) # another server's list is wrong
def test_openrouter_prices_and_no_secrets(self):
node = self.cache("openrouter", True, "", "a/y\t5\t0.1\t0.2\n")
self.assertEqual(node["models"], [{"id": "a/y", "contextTokens": 5, "priceIn": 0.1, "priceOut": 0.2}])
self.assertNotIn("baseUrl", node)
with open(self.path("c.json")) as fh:
text = fh.read()
for word in ("keyRef", "token", "auth"):
self.assertNotIn('"%s"' % word, text)
# ---------------------------------------------------------------------------
# Session transcripts: scan, dismiss, repair
# ---------------------------------------------------------------------------
GOOD = {"type": "assistant", "message": {"id": "msg_1", "model": "claude-opus-5", "content": [{"type": "text", "text": "hi"}]}}
FOREIGN = {"type": "assistant", "message": {"id": "gen-1-a", "model": "deepseek/x", "content": [{"type": "text", "text": "yo"}]}}
class Sessions(TempDir):
def setUp(self):
super().setUp()
self.projects = self.path("projects")
os.makedirs(os.path.join(self.projects, "-p"))
self.ignored = self.path("ignored.json")
def transcript(self, sid, lines, age_days=0):
p = os.path.join(self.projects, "-p", sid + ".jsonl")
with open(p, "w") as fh:
fh.write("\n".join(json.dumps(x) for x in lines) + "\n")
t = time.time() - age_days * 86400
os.utime(p, (t, t))
return p
def scan(self, max_age=""):
return call_json(cm.cmd_scan_sessions, [self.projects, max_age, self.ignored])
def test_scan_routes_by_reason(self):
self.transcript("new", [GOOD, FOREIGN])
self.transcript("old", [GOOD, FOREIGN], age_days=10)
self.transcript("healthy", [GOOD])
self.transcript("native", [FOREIGN]) # ran entirely on a gateway: not damage
d = self.scan()
self.assertEqual([b["sessionId"] for b in d["broken"]], ["new"])
self.assertEqual([(i["sessionId"], i["reason"]) for i in d["ignored"]], [("old", "stale")])
self.assertEqual(self.scan("0")["count"], 2)
def test_dismiss_list_prune(self):
self.transcript("new", [GOOD, FOREIGN])
gone = self.transcript("gone", [GOOD, FOREIGN])
call(cm.cmd_ignore_session, [self.ignored, self.projects, "add", "new"])
call(cm.cmd_ignore_session, [self.ignored, self.projects, "add", "gone"])
self.assertEqual([(i["sessionId"], i["reason"]) for i in self.scan()["ignored"]],
[("gone", "dismissed"), ("new", "dismissed")])
rows = call_json(cm.cmd_ignore_session, [self.ignored, self.projects, "list"])["sessions"]
self.assertTrue(all(r["broken"] and r["exists"] for r in rows))
os.remove(gone)
out = call_json(cm.cmd_ignore_session, [self.ignored, self.projects, "remove", "new"])
self.assertEqual((out["changed"], out["pruned"], out["count"]), (True, 1, 0))
with self.assertRaises(SystemExit):
call(cm.cmd_ignore_session, [self.ignored, self.projects, "add", "../x"])
with self.assertRaises(SystemExit):
call(cm.cmd_ignore_session, [self.ignored, self.projects, "add", "nosuch"])
def test_repair_truncates_and_hands_back(self):
p = self.transcript("fix", [GOOD, FOREIGN])
v = call_json(cm.cmd_repair_session, [p, "--apply"])
self.assertTrue(v["applied"] and v["reinjected"])
self.assertTrue(os.path.exists(v["backup"]) and os.path.exists(v["recovered"]))
with open(p) as fh:
lines = [json.loads(x) for x in fh.read().splitlines()]
self.assertEqual(lines[0]["message"]["id"], "msg_1")
self.assertTrue(lines[1]["isMeta"])
self.assertIn("<recovered-transcript>", lines[1]["message"]["content"])
self.assertTrue(call_json(cm.cmd_repair_session, [p])["healthy"])
# ---------------------------------------------------------------------------
# Presets: rename, defaults, auth
# ---------------------------------------------------------------------------
class Presets(TempDir):
def setUp(self):
super().setUp()
self.presets = self.path("presets")
shutil.copytree(os.path.join(REPO, "presets"), self.presets)
self.state = self.write_json("state.json", {"mode": "openrouter", "preset": "default", "writtenEnvKeys": []})
self.defaults = self.write_json("defaults.json", {"openrouter": "default"})
def load(self, p):
with open(p) as fh:
return json.load(fh)
def test_rename_repoints_state_and_default(self):
out = call_json(cm.cmd_preset_rename, [self.presets, "default", "daily", self.state, self.defaults])
self.assertEqual(out, {"renamed": True, "active": True, "default": True})
self.assertEqual(self.load(self.state)["preset"], "daily")
self.assertEqual(self.load(self.defaults)["openrouter"], "daily")
self.assertFalse(os.path.exists(os.path.join(self.presets, "default.json")))
with self.assertRaises(SystemExit):
call(cm.cmd_preset_rename, [self.presets, "daily", "zai", self.state])
with self.assertRaises(SystemExit):
call(cm.cmd_preset_rename, [self.presets, "nosuch", "x", self.state])
def test_set_default_and_clear(self):
call(cm.cmd_set_default, [self.defaults, "zai", "zai"])
call(cm.cmd_set_default, [self.defaults, "openrouter", ""])
self.assertEqual(self.load(self.defaults), {"zai": "zai"})
def test_auth_of(self):
self.assertEqual(call(cm.cmd_auth_of, [os.path.join(self.presets, "zai.json")]), "vault\tzai\n")
self.assertEqual(call(cm.cmd_auth_of, [os.path.join(self.presets, "ollama.json")]), "literal\topenrouter\n")
with self.assertRaises(SystemExit) as e:
call(cm.cmd_auth_of, [os.path.join(self.presets, "nosuch.json")])
self.assertEqual(e.exception.code, 1)
def test_set_auth_uses_the_providers_names(self):
p = os.path.join(self.presets, "ollama.json")
call(cm.cmd_set_auth, [p, "key"])
self.assertEqual(self.load(p)["auth"], {"mode": "vault", "keyRef": "ollama"})
call(cm.cmd_set_auth, [p, "none"])
self.assertEqual(self.load(p)["auth"], {"mode": "literal", "token": "ollama"})
class Health(Presets):
def health(self, mode="anthropic", preset=""):
call(cm.cmd_health, [self.tmp, "/nonexistent", mode, preset, "", "file", "9.9.9"])
return self.load(self.path("health.json"))
def test_effective_defaults(self):
self.assertEqual(self.health()["defaultPresetFor"]["openrouter"], "default")
shutil.copy(os.path.join(self.presets, "default.json"), os.path.join(self.presets, "cheap.json"))
self.write_json("defaults.json", {"openrouter": "cheap"})
h = self.health()
self.assertEqual((h["defaultPresetFor"]["openrouter"], h["defaultPresetChosen"]), ("cheap", {"openrouter": "cheap"}))
os.remove(os.path.join(self.presets, "cheap.json")) # a vanished choice falls back
self.assertEqual(self.health()["defaultPresetFor"]["openrouter"], "default")
os.remove(os.path.join(self.presets, "default.json")) # and with the built-in gone, the first by name
shutil.copy(os.path.join(self.presets, "zai.json"), os.path.join(self.presets, "alpha.json"))
alpha = self.load(os.path.join(self.presets, "alpha.json"))
alpha["provider"] = "openrouter"
self.write_json("presets/alpha.json", alpha)
self.assertEqual(self.health()["defaultPresetFor"]["openrouter"], "alpha")
def test_publishes_providers(self):
h = self.health()
self.assertEqual(h["version"], "9.9.9")
byid = {p["id"]: p for p in h["providers"]}
self.assertEqual(list(byid), ["openrouter", "zai", "lmstudio", "ollama", "custom"])
self.assertTrue(byid["ollama"]["serverEditable"] and byid["ollama"]["perServerCatalogue"])
self.assertFalse(byid["zai"]["serverEditable"])
self.assertEqual(byid["custom"]["defaultBaseUrl"], "")
self.assertTrue(byid["openrouter"]["logo"])
class OllamaCtx(unittest.TestCase):
def test_show(self):
src = {"model_info": {"general.architecture": "q", "q.context_length": 262144}}
self.assertEqual(call(cm.cmd_ollama_ctx, ["show"], json.dumps(src)), "262144\n")
def test_ps_matches_latest(self):
src = {"models": [{"name": "qwen3-coder:latest", "model": "qwen3-coder:latest", "context_length": 4096}]}
self.assertEqual(call(cm.cmd_ollama_ctx, ["ps", "qwen3-coder"], json.dumps(src)), "4096\n")
self.assertEqual(call(cm.cmd_ollama_ctx, ["ps", "other"], json.dumps(src)), "")
# ---------------------------------------------------------------------------
# apply - the write into settings.json
# ---------------------------------------------------------------------------
class Apply(TempDir):
def preset(self, **over):
base = {"provider": "openrouter", "baseUrl": "https://gw", "auth": {"mode": "vault", "keyRef": "openrouter"},
"models": {"opus": "x/opus", "sonnet": "x/sonnet"}, "contextTokens": 1000}
base.update(over)
return self.write_json("p.json", base)
def apply(self, mode, preset="", helper="/opt/key helper/h.sh"):
return call(cm.cmd_apply, [self.path("settings.json"), self.path("state.json"), mode, preset, helper])
def settings(self):
with open(self.path("settings.json")) as fh:
return json.load(fh)
def test_writes_env_and_quotes_the_helper(self):
self.apply("openrouter", self.preset())
s = self.settings()
self.assertEqual(s["env"]["ANTHROPIC_BASE_URL"], "https://gw")
self.assertEqual(s["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"], "x/opus")
self.assertEqual(s["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], "1000")
self.assertEqual(s["env"]["ANTHROPIC_API_KEY"], "")
self.assertEqual(s["apiKeyHelper"], "'/opt/key helper/h.sh'")
def test_cost_guard(self):
with self.assertRaises(SystemExit) as e:
self.apply("openrouter", self.preset(models={"opus": "anthropic/claude-opus-5"}))
self.assertIn("refusing to switch", str(e.exception))
self.apply("openrouter", self.preset(models={"opus": "anthropic/claude-opus-5"}, allowAnthropicModels=True))
def test_extra_env_is_removed_on_the_way_out(self):
self.apply("openrouter", self.preset(extraEnv={"SOME_CUSTOM_KEY": "1"}))
self.assertIn("SOME_CUSTOM_KEY", self.settings()["env"])
self.apply("anthropic")
s = self.settings()
self.assertNotIn("env", s)
self.assertNotIn("apiKeyHelper", s)
def test_literal_token_and_provider_check(self):
self.apply("ollama", self.preset(provider="ollama", auth={"mode": "literal", "token": "ollama"}))
self.assertEqual(self.settings()["env"]["ANTHROPIC_AUTH_TOKEN"], "ollama")
with self.assertRaises(SystemExit):
self.apply("zai", self.preset())
if __name__ == "__main__":
unittest.main()
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Checks that need nothing but the tree: script syntax, JSON validity, the
# version recorded in both places, and providers.json against the kinds of
# behaviour the code knows. qmllint and shellcheck run when installed and are
# skipped - out loud - when not.
set -uo pipefail
cd "$(dirname "$0")/.."
fails=0
err="$(mktemp)"
tmp="$(mktemp -d)"
trap 'rm -rf "$err" "$tmp"' EXIT
check() {
local what="$1"; shift
if "$@" >"$err" 2>&1; then
printf ' ok %s\n' "$what"
else
printf ' FAIL %s\n' "$what"
sed 's/^/ | /' "$err" | head -n 20
fails=$((fails + 1))
fi
}
bash_syntax() {
local f
for f in linux/claude-mode linux/*.sh linux/lib/*.sh omarchy/install.sh scripts/*.sh \
tests/*.sh tests/cli/*.sh tests/windows/*.sh; do
[ -e "$f" ] || continue
bash -n "$f" || { echo "$f"; return 1; }
done
}
check 'shell scripts parse (bash -n)' bash_syntax
check 'python compiles' env PYTHONPYCACHEPREFIX="$tmp" python3 -m py_compile \
linux/cm-json.py tests/fake_server.py tests/python/test_cm_json.py
check 'JSON files parse' python3 -c '
import json, sys, glob
for f in ["providers.json", "omarchy/smoido.claude-mode/manifest.json"] + glob.glob("presets/*.json"):
try:
json.load(open(f, encoding="utf-8"))
except ValueError as e:
sys.exit("%s: %s" % (f, e))'
check 'VERSION matches the widget manifest' python3 -c '
import json
v = open("VERSION").read().strip()
m = json.load(open("omarchy/smoido.claude-mode/manifest.json"))["version"]
assert v == m, "VERSION is %s but manifest.json says %s - use scripts/bump-version.sh" % (v, m)'
check 'CHANGELOG has an entry for VERSION' python3 -c '
v = open("VERSION").read().strip()
assert ("## %s " % v) in open("CHANGELOG.md", encoding="utf-8").read(), "no CHANGELOG.md entry for " + v'
# The values each field may take are the ones the code has a branch for. A new
# kind of behaviour needs code in cm-json.py, the bash CLI and claude-mode.ps1
# before it can appear here - this is where that is enforced.
check 'providers.json is consistent with the code and the presets' python3 - <<'PY'
import json, os, sys
KINDS = {"openrouter", "lmstudio", "ollama", "openai", "static"}
PROBES = {"always", "lenient", "local"}
KEYS = {"required", "optional"}
MODELS = {"per-tier", "one-for-all"}
CHECKS = {"openrouter-key", "guardrail", "message-check", "catalogue-models",
"ollama-context", "lmstudio-templates"}
COLORS = {"cyan", "green", "yellow", "magenta", "white", "gray", "dkcyan"}
COMMANDS = {"menu", "status", "anthropic", "presets", "preset", "set-key", "models",
"doctor", "health", "preflight", "setup", "sessions", "repair-session",
"repair", "help"}
errors = []
def bad(pid, msg): errors.append("%s: %s" % (pid, msg))
doc = json.load(open("providers.json", encoding="utf-8"))
seen = {}
for p in doc["providers"]:
pid = p.get("id", "?")
for name in [pid] + list(p.get("aliases") or []):
if name in COMMANDS:
bad(pid, "'%s' would shadow the '%s' command" % (name, name))
if name in seen:
bad(pid, "'%s' is already used by %s" % (name, seen[name]))
seen[name] = pid
for field in ("title", "label", "blurb", "defaultPreset", "preset", "catalogue", "setup", "logo"):
if not p.get(field):
bad(pid, "missing " + field)
cat, srv, setup = p.get("catalogue") or {}, p.get("server") or {}, p.get("setup") or {}
if cat.get("kind") not in KINDS:
bad(pid, "catalogue.kind %r is not one of %s" % (cat.get("kind"), sorted(KINDS)))
if cat.get("kind") == "static" and not cat.get("static"):
bad(pid, "a static catalogue needs a static list")
if srv.get("probe", "local") not in PROBES:
bad(pid, "server.probe %r is not one of %s" % (srv.get("probe"), sorted(PROBES)))
if srv.get("probe") in ("always", "lenient") and not srv.get("paths"):
bad(pid, "a probed server needs server.paths")
if setup.get("key", "required") not in KEYS:
bad(pid, "setup.key %r is not one of %s" % (setup.get("key"), sorted(KEYS)))
if setup.get("models", "per-tier") not in MODELS:
bad(pid, "setup.models %r is not one of %s" % (setup.get("models"), sorted(MODELS)))
for c in p.get("doctor") or []:
if c not in CHECKS:
bad(pid, "unknown doctor check %r" % c)
if p.get("color", "gray") not in COLORS:
bad(pid, "color %r is not one of %s" % (p.get("color"), sorted(COLORS)))
auth = (p.get("preset") or {}).get("auth") or {}
if auth.get("mode") not in ("vault", "literal"):
bad(pid, "preset.auth.mode must be vault or literal")
preset = os.path.join("presets", "%s.json" % p.get("defaultPreset"))
if not os.path.exists(preset):
bad(pid, "default preset %s does not exist" % preset)
elif json.load(open(preset, encoding="utf-8")).get("provider") != pid:
bad(pid, "default preset %s belongs to another provider" % preset)
for f in sorted(os.listdir("presets")):
pp = json.load(open(os.path.join("presets", f), encoding="utf-8"))
if pp.get("provider") not in {p["id"] for p in doc["providers"]}:
errors.append("presets/%s: provider %r is not in providers.json" % (f, pp.get("provider")))
if errors:
sys.exit("\n".join(errors))
PY
QMLLINT="$(command -v qmllint || true)"
[ -z "$QMLLINT" ] && [ -x /usr/lib/qt6/bin/qmllint ] && QMLLINT=/usr/lib/qt6/bin/qmllint
if [ -n "$QMLLINT" ]; then
# The shell's own modules (qs.*, Quickshell) do not resolve outside it, so
# only real syntax errors count; unresolved-type warnings are expected.
qml_syntax() {
local f out
for f in omarchy/smoido.claude-mode/*.qml; do
out="$("$QMLLINT" "$f" 2>&1 | grep -iE 'error|expected token|syntax|unexpected|duplicate' || true)"
[ -z "$out" ] || { echo "$f"; echo "$out"; return 1; }
done
}
check 'QML has no syntax errors (qmllint)' qml_syntax
else
printf ' skip QML syntax (qmllint not installed)\n'
fi
if command -v shellcheck >/dev/null 2>&1; then
check 'shellcheck (errors only)' shellcheck -S error -s bash \
linux/claude-mode linux/*.sh omarchy/install.sh scripts/*.sh tests/*.sh tests/cli/*.sh
else
printf ' skip shellcheck (not installed)\n'
fi
[ "$fails" -eq 0 ]
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Runs tests/windows/run.ps1 on a Windows host over SSH (default: winbox):
# copies the payload to the host's %TEMP%, runs it there, and removes it again.
# Needs key-based SSH to the host and python on the host (for the fake servers).
# The suite itself runs in a USERPROFILE sandbox; see run.ps1.
set -uo pipefail
cd "$(dirname "$0")/../.."
host="${1:-winbox}"
name='cm-test-payload'
ssh_() { ssh -o BatchMode=yes -o LogLevel=ERROR -o ConnectTimeout=10 "$host" "$@"; }
parent="$(mktemp -d)"
trap 'rm -rf "$parent"' EXIT
stage="$parent/$name"
mkdir -p "$stage/presets" "$stage/bin"
cp claude-mode.ps1 providers.json VERSION tests/fake_server.py tests/windows/run.ps1 "$stage/"
cp presets/*.json "$stage/presets/"
cp bin/* "$stage/bin/"
rtemp="$(ssh_ 'Write-Output $env:TEMP' | tr -d '\r')" || { echo "cannot reach $host" >&2; exit 1; }
[ -n "$rtemp" ] || { echo "no %TEMP% on $host" >&2; exit 1; }
ssh_ "Remove-Item -Recurse -Force '$rtemp\\$name' -ErrorAction SilentlyContinue" || true
scp -q -r -o BatchMode=yes -o LogLevel=ERROR "$stage" "$host:${rtemp//\\//}/" || { echo "copy to $host failed" >&2; exit 1; }
ssh_ "powershell -NoProfile -ExecutionPolicy Bypass -File '$rtemp\\$name\\run.ps1'"
rc=$?
ssh_ "Remove-Item -Recurse -Force '$rtemp\\$name' -ErrorAction SilentlyContinue" || true
exit "$rc"
+127
View File
@@ -0,0 +1,127 @@
# Windows suite for claude-mode.ps1. Run on a Windows host by
# tests/windows/run-remote.sh, from a copy of the payload in that host's %TEMP%.
#
# Everything happens in a sandbox: USERPROFILE is pointed at a temp folder for
# this process and its children, and the script derives every path from it, so
# the host's real ~\.claude-mode, vault and Claude settings are never read or
# written. The fake provider servers run on the host's own loopback.
# ASCII only: Windows PowerShell 5.1 reads a .ps1 without a BOM as ANSI.
$ErrorActionPreference = 'Continue'
$src = $PSScriptRoot
$sb = Join-Path $env:TEMP ('cm-test-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
$root = Join-Path $sb '.claude-mode'
$real = $env:USERPROFILE
$script:pass = 0
$script:fail = 0
function Pass { $script:pass++ }
function Fail([string] $what, $out) {
$script:fail++
Write-Host " FAIL windows: $what"
if ($out) { @($out) | Select-Object -Last 8 | ForEach-Object { Write-Host " | $_" } }
}
function Expect-Match($out, [string] $pattern, [string] $what) {
if ((@($out) -join "`n") -match $pattern) { Pass } else { Fail $what $out }
}
function Expect-Eq($got, $want, [string] $what) {
if ("$got" -eq "$want") { Pass } else { Fail "$what (got '$got', wanted '$want')" $null }
}
New-Item -ItemType Directory -Force -Path "$root\presets", "$root\bin", "$sb\.claude" | Out-Null
Copy-Item "$src\claude-mode.ps1", "$src\providers.json", "$src\VERSION" $root
Copy-Item "$src\presets\*.json" "$root\presets"
Copy-Item "$src\bin\*" "$root\bin"
$cm = Join-Path $root 'claude-mode.ps1'
$tokens = $null; $errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($cm, [ref]$tokens, [ref]$errors)
Expect-Eq $errors.Count 0 'claude-mode.ps1 parses'
foreach ($e in $errors) { Write-Host " | line $($e.Extent.StartLineNumber): $($e.Message)" }
$portsFile = Join-Path $sb 'ports.json'
$fake = Start-Process python -ArgumentList ('"' + (Join-Path $src 'fake_server.py') + '"') `
-NoNewWindow -RedirectStandardOutput $portsFile -PassThru
for ($i = 0; $i -lt 50; $i++) {
if ((Test-Path $portsFile) -and (Get-Item $portsFile).Length -gt 0) { break }
Start-Sleep -Milliseconds 100
}
$ports = Get-Content $portsFile -TotalCount 1 | ConvertFrom-Json
$ollama = "http://127.0.0.1:$($ports.ollama)"
$keyed = "http://127.0.0.1:$($ports.keyed)"
$env:USERPROFILE = $sb
$script:rc = 0
function CM {
param([Parameter(ValueFromRemainingArguments = $true)] [string[]] $a)
$o = & powershell -NoProfile -ExecutionPolicy Bypass -File $cm @a 2>&1 | ForEach-Object { "$_" }
$script:rc = $LASTEXITCODE
return $o
}
function Set-PresetJson([string] $name, [scriptblock] $edit) {
$p = Join-Path $root "presets\$name.json"
$j = Get-Content $p -Raw | ConvertFrom-Json
& $edit $j
$j | ConvertTo-Json -Depth 6 | Set-Content $p -Encoding UTF8
}
$o = CM help
foreach ($id in 'openrouter', 'zai', 'lmstudio', 'ollama', 'custom') {
Expect-Match $o "claude-mode $id \[preset\]" "usage lists $id"
}
$o = CM custom
Expect-Eq $rc 1 'custom without an address is refused'
Expect-Match $o 'has no server address' 'and says why'
Copy-Item "$root\presets\custom.json" "$root\presets\blank.json"
Set-PresetJson 'blank' { param($j) $j.baseUrl = 'https://llm.example.com' }
$o = CM custom blank
Expect-Eq $rc 1 'a preset with no models is refused'
Expect-Match $o 'has no models set' 'and says why'
Set-PresetJson 'ollama' { param($j) $j.baseUrl = $ollama }
$o = CM ollama
Expect-Eq $rc 0 'switch to ollama'
$envBlock = (Get-Content "$sb\.claude\settings.json" -Raw | ConvertFrom-Json).env
Expect-Eq $envBlock.ANTHROPIC_BASE_URL $ollama 'base url written'
Expect-Eq $envBlock.ANTHROPIC_AUTH_TOKEN 'ollama' 'placeholder token written'
Expect-Eq $envBlock.CLAUDE_CODE_MAX_CONTEXT_TOKENS '65536' 'context declared'
$o = CM models
Expect-Match $o 'qwen3-coder:latest' 'ollama models listed'
$o = CM doctor
Expect-Match $o 'Ollama reachable' 'doctor finds the server'
Expect-Match $o 'opus\s+qwen3-coder\s+\[30\.5B Q4_K_M\]' 'a bare name matches its :latest tag'
Expect-Match $o 'loaded with a 4096-token context' 'the short server context is caught'
Expect-Match $o 'OLLAMA_CONTEXT_LENGTH=65536' 'and the fix is named'
Set-PresetJson 'custom' {
param($j)
$j.baseUrl = $keyed
$j.auth = [pscustomobject]@{ mode = 'literal'; token = 'sk-real' }
$j.models.opus = 'gw/model-a'
}
$o = CM custom
Expect-Eq $rc 0 'switch to a custom endpoint'
$o = CM models
Expect-Match $o 'gw/model-b' 'a keyed endpoint lists its models'
$o = CM 'Z.AI'
Expect-Match $o "no key stored for ref 'zai'" 'an alias resolves to its provider'
$o = CM nope
Expect-Eq $rc 1 'an unknown command fails'
$want = '{"provider":"zai","description":"new preset","baseUrl":"https://api.z.ai/api/anthropic","auth":{"mode":"vault","keyRef":"zai"},"models":{"opus":"","sonnet":"","haiku":"","fable":""},"subagentModel":"inherit","gatewayModelDiscovery":false,"contextTokens":1000000,"extraEnv":{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1"}}'
$got = & powershell -NoProfile -ExecutionPolicy Bypass -Command ". '$cm' help *> `$null; New-PresetScaffold 'zai' | ConvertTo-Json -Depth 5 -Compress"
Expect-Eq $got $want 'the zai scaffold is unchanged'
Stop-Process -Id $fake.Id -Force -ErrorAction SilentlyContinue
$env:USERPROFILE = $real
Start-Sleep -Milliseconds 300
Remove-Item -Recurse -Force $sb -ErrorAction SilentlyContinue
$line = '{0,-22} {1,3} passed' -f 'windows', $script:pass
if ($script:fail -gt 0) { $line += ", $($script:fail) FAILED" }
Write-Host $line
if ($script:fail -gt 0) { exit 1 }
exit 0