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>
128 lines
5.5 KiB
PowerShell
128 lines
5.5 KiB
PowerShell
# 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
|