# Development How to change claude-mode without breaking it. How the pieces fit is in [architecture.md](architecture.md); why they are shaped that way, in [design.md](design.md). ## Running the tests ```bash scripts/test.sh # static checks, Python unit tests, CLI tests (~30s) scripts/test.sh --windows # ...and the Windows suite on winbox over SSH scripts/test.sh --windows host # ...on another Windows host make test # the same, if make is installed ``` Nothing needs installing beyond bash, python3 and curl. `qmllint` and `shellcheck` are used when present and skipped, out loud, when not. | suite | what it covers | |---|---| | `tests/static.sh` | every script parses; JSON is valid; `VERSION` matches the widget manifest and has a `CHANGELOG.md` entry; `providers.json` uses only the kinds, probes and checks the code implements, every default preset exists, no id or alias shadows a command; Markdown links resolve; QML has no syntax errors | | `tests/python/` | `linux/cm-json.py`, function by function: providers and the column contract the shell depends on, the original three scaffolds byte for byte, every catalogue parser, the cache, session scan/dismiss/repair, rename, defaults, health, the cost guard | | `tests/cli/` | the POSIX CLI end to end, in a sandbox, against fake servers: providers, preflight, presets, models, doctor, a real switch, sessions, and the key helper under a rename race | | `tests/windows/` | `claude-mode.ps1` on a real Windows PowerShell 5.1, in a sandbox, against the same fake servers | **The tests never touch a real install.** Each CLI test gets its own `HOME`, `CM_ROOT` and `CLAUDE_CONFIG_DIR` with the repository's scripts installed into it, and the vault forced to the plain-file backend (`CLAUDE_MODE_VAULT=file`), so your keyring, `~/.claude-mode` and `~/.claude/settings.json` are never read or written; a switch a test performs rewrites the sandbox's `settings.json`. The Windows suite points `USERPROFILE` at a temp folder for its process, which moves every path the script uses. `tests/fake_server.py` stands in for Ollama, LM Studio, a keyed gateway and a listing-less proxy, on ports the OS picks. The Windows suite needs key-based SSH to the host and python on the host (for the fake servers). It copies a payload to the host's `%TEMP%`, runs it, and removes it. **Do not test by switching your own machine.** A switch rewrites the live `settings.json` that every Claude Code session — including one you may be working in — depends on. Test in the sandbox; `tests/lib.sh` shows how to get one by hand. ### Writing a test A CLI test sources `tests/lib.sh`, which provides the sandbox, `run_cm` (output in `$OUT`, colour stripped; status in `$RC`), `expect_rc` / `expect_out` / `expect_eq` / `expect_file`, `start_fake` (sets `OLLAMA_URL`, `LMSTUDIO_URL`, `KEYED_URL`, `PROXY_URL`), `ready_preset`, `set_state` and `preflight_code`, and ends with `finish`. Assertions count rather than stop, so one run reports every failure. A test earns its place by failing when the thing it describes breaks — check that it does, by breaking it. ## Conventions The code follows these already; they are written down so it keeps doing so. **Portability of the POSIX port.** It runs on the bash 3.2 macOS ships: no associative arrays, no `mapfile`, no fractional `read -t`, no GNU-only `stat` or `find` flags. `linux/claude-mode` uses no `awk` (absent from minimal images). python3 is the only dependency, and handles every piece of JSON — bash only ever sees flat lines of text. **Never read TSV with `IFS=$'\t' read`** when a column can be empty: tab counts as whitespace to `read`, so empty fields merge and later columns shift. Use `cut` (`tsv_field`), or have the producer print `-` for empty. **Windows PowerShell 5.1.** No `?:` or `??`; `"$id:latest"` parses as a drive-qualified variable, so write `"${id}:latest"`. The `.ps1` files are ASCII only, because 5.1 reads a file without a BOM in the ANSI codepage. **Files are LF everywhere**, `.ps1` and `.cmd` included (`.gitattributes`, `.editorconfig`). **Comments say why**, and name the thing that went wrong when a line exists because of it. What the code does is left to the code. **Keys never leave the vault**: not into `settings.json`, `health.json`, the model cache, logs, or test output. ## Providers Adding a provider that reuses existing behaviour is an entry in `providers.json` and a preset in `presets/` — see [providers.md](providers.md#adding-a-provider). The static check enforces the entry's shape; add a CLI test if the provider does anything the others do not. A new *kind* of behaviour needs a parser in `cm-json.py`, a branch in the bash CLI and in `claude-mode.ps1`, and its value added to the check in `tests/static.sh`. `cm-json.py`'s `PROVIDER_TSV` is a contract: the shell addresses its columns by number. Append only, and update the column list in `linux/claude-mode` and the unit test that pins it. ## The bar widget - Install with `bash omarchy/install.sh`, then `omarchy restart shell`: an edited QML file does not reliably reload in a running shell, and `Modes.js`, a `.pragma library`, never does. - Anything with a text field must live in a `KeyboardPanel`. A `PopupCard` is an xdg-popup, which never receives the keyboard, so its fields look fine and take no typing. - The panel's height clamps to the screen rather than scrolling: long content needs its own bounded, scrolling area, or it is cut off. - Provider presentation comes from `health.json`, not from `Modes.js` — new providers need no QML change. - There are no automated UI tests. `tests/static.sh` catches QML syntax errors; everything else is checked by eye, on the real bar. ## Releasing 1. `scripts/test.sh --windows` — everything green. 2. `scripts/bump-version.sh X.Y.Z`, and a `CHANGELOG.md` entry for it (the static check fails without one). 3. Commit. 4. Install on your own machine: `bash linux/install.sh`, `bash omarchy/install.sh`, `omarchy restart shell`, and look at the bar. 5. Push. The Windows package is built on a Windows machine with `scripts/build-package.ps1`, which refuses to package a script that does not parse. Semver: a minor version adds behaviour, a patch fixes it.