From 8eced9d8536bcb6aaed69b7bbc3968b7ca66e007 Mon Sep 17 00:00:00 2001 From: Kenny-Heitritter Date: Fri, 21 Aug 2026 11:57:39 -0500 Subject: [PATCH 1/2] Harden Windows PowerShell compatibility --- README.md | 19 ++- docs/windows-vm-e2e.md | 293 ++++++++++++++++++++++++++++++++++ install.ps1 | 104 ++++++++---- install.sh | 15 ++ tests/claude-compat.Tests.ps1 | 41 +++++ tests/install-profiles.sh | 9 +- tests/migration.sh | 9 +- 7 files changed, 455 insertions(+), 35 deletions(-) create mode 100644 docs/windows-vm-e2e.md diff --git a/README.md b/README.md index 26497f9..6d29d9f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ Claude Code installation. The installer never downgrades a newer or unrecognized version. If your version cannot run `claude mcp login`, authenticate through Claude Code's `/mcp` menu. +Maintainers can reproduce the Windows compatibility matrix in a real Windows +11 VM by following [the Windows VM E2E guide](docs/windows-vm-e2e.md). + ## Start a session Start an interactive session with the active organization and default model. @@ -198,11 +201,14 @@ because one process cannot use several context limits safely. ### Model thinking -Claude Code sends adaptive thinking, display policy, and effort for current -Opus models. +Claude Code currently sends fixed-budget thinking for qBraid's custom Opus 4.8, +Opus 5, and Sonnet 4.6 model IDs, while those upstream models accept adaptive +thinking only. The per-launch proxy removes the incompatible `thinking` and +`output_config` fields for those exact IDs so requests remain valid. They +currently run without extended thinking; Haiku's legacy thinking and GPT +reasoning are unaffected. -The older global thinking-disable workaround is gone. The 128-tool limit is -independent of thinking. +The 128-tool limit is independent of thinking. ## Keep plain `claude` unchanged @@ -213,11 +219,12 @@ a project setting can replace the base URL and exfiltrate a reusable key. ## How it works Claude Code sends Anthropic Messages requests to the qBraid gateway. The local -proxy translates only the GPT routes. +proxy translates GPT routes and removes incompatible fixed-thinking fields from +known adaptive-only Claude model IDs. ```text qbraid-code ── loopback CLIProxyAPI - ├─ Claude passthrough ── qBraid gateway + ├─ Claude normalization ── qBraid gateway └─ GPT translation ──── qBraid gateway ``` diff --git a/docs/windows-vm-e2e.md b/docs/windows-vm-e2e.md new file mode 100644 index 0000000..a1db8a1 --- /dev/null +++ b/docs/windows-vm-e2e.md @@ -0,0 +1,293 @@ +# Windows VM end-to-end testing + +This guide reproduces the Windows 11 environment used to test the PowerShell +installer against real Claude Code releases and the live qBraid gateway. It is +for maintainers testing `install.ps1`, not for end-user installation. + +The tested host was Arch Linux with Docker, hardware virtualization, and +read/write access to `/dev/kvm`. The guest used Windows PowerShell 5.1, which is +important: it exposed compatibility failures that did not reproduce in modern +PowerShell. + +## Tested configuration + +| Setting | Value | +|---|---| +| VM image | `dockurr/windows` 5.14 | +| Tested image digest | `sha256:20b398ab935465f97ec8ab06489f7a85a5ad58e74e036ce66cc3c9172e7dbea8` | +| Windows release | Windows 11 LTSC (`VERSION=11l`) | +| RAM / CPUs / disk | 8 GiB / 4 / 64 GiB | +| Persistent volume | `qbraid-code-windows-e2e` mounted at `/storage` | +| Container | `qbraid-code-windows-e2e` | +| noVNC / RDP / SSH | host ports 8006 / 3389 / 2222 | +| Windows account | dockurr test default `Docker` / `admin` | +| PowerShell | Windows PowerShell 5.1.26100.1591 | + +The default password is appropriate only for a disposable test VM. The commands +below bind every published port to `127.0.0.1`. Change the password before +making the VM reachable from another host. + +## 1. Check the host + +```bash +docker version +test -r /dev/kvm && test -w /dev/kvm +test -c /dev/net/tun +``` + +Create the persistent volume and start the VM. The digest below pins the image +used by the successful run; `dockurr/windows` can be substituted when testing a +newer image deliberately. + +```bash +docker volume create qbraid-code-windows-e2e + +docker run -d \ + --name qbraid-code-windows-e2e \ + --device=/dev/kvm \ + --device=/dev/net/tun \ + --cap-add NET_ADMIN \ + -e VERSION=11l \ + -e RAM_SIZE=8G \ + -e CPU_CORES=4 \ + -e DISK_SIZE=64G \ + -p 127.0.0.1:8006:8006 \ + -p 127.0.0.1:3389:3389/tcp \ + -v qbraid-code-windows-e2e:/storage \ + -v "$PWD:/shared:ro" \ + dockurr/windows@sha256:20b398ab935465f97ec8ab06489f7a85a5ad58e74e036ce66cc3c9172e7dbea8 +``` + +Watch startup with `docker logs -f qbraid-code-windows-e2e`. Open +`http://localhost:8006` for the noVNC console and let Windows finish its first +boot. The image creates and signs into the `Docker` account automatically. + +## 2. Enable SSH once + +In noVNC, open **Windows PowerShell as Administrator** and run: + +```powershell +Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 +Start-Service sshd +Set-Service sshd -StartupType Automatic + +if (-not (Get-NetFirewallRule -Name OpenSSH-Server-In-TCP -ErrorAction SilentlyContinue)) { + New-NetFirewallRule ` + -Name OpenSSH-Server-In-TCP ` + -DisplayName 'OpenSSH Server (sshd)' ` + -Enabled True ` + -Direction Inbound ` + -Protocol TCP ` + -Action Allow ` + -LocalPort 22 +} +``` + +Recreate the container with SSH published. The Windows installation remains in +the named volume. + +```bash +docker stop qbraid-code-windows-e2e +docker rm qbraid-code-windows-e2e + +docker run -d \ + --name qbraid-code-windows-e2e \ + --device=/dev/kvm \ + --device=/dev/net/tun \ + --cap-add NET_ADMIN \ + -e VERSION=11l \ + -e RAM_SIZE=8G \ + -e CPU_CORES=4 \ + -e DISK_SIZE=64G \ + -p 127.0.0.1:8006:8006 \ + -p 127.0.0.1:3389:3389/tcp \ + -p 127.0.0.1:2222:22/tcp \ + -v qbraid-code-windows-e2e:/storage \ + -v "$PWD:/shared:ro" \ + dockurr/windows@sha256:20b398ab935465f97ec8ab06489f7a85a5ad58e74e036ce66cc3c9172e7dbea8 +``` + +Wait for SSH to answer before continuing: + +```bash +until timeout 2 bash -c '/dev/null; do + sleep 5 +done +``` + +## 3. Create the SSH helper + +The test host did not need a native SSH client. This small helper image keeps +the dependency isolated: + +```bash +docker build -t qbraid-code-win-ssh - <<'EOF' +FROM alpine:3.22 +RUN apk add --no-cache openssh-client sshpass +EOF +``` + +Verify Windows PowerShell 5.1 is reachable: + +```bash +docker run --rm --network host qbraid-code-win-ssh \ + sshpass -p admin ssh \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -p 2222 Docker@127.0.0.1 \ + powershell.exe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' +``` + +## 4. Copy the working tree + +Run these commands from the repository root: + +```bash +docker run --rm --network host -v "$PWD:/src:ro" qbraid-code-win-ssh sh -lc ' + sshpass -p admin ssh \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -p 2222 Docker@127.0.0.1 \ + powershell.exe -NoProfile -Command \ + "New-Item -ItemType Directory -Force C:\Users\Docker\qbraid-code-e2e\tests" + + sshpass -p admin scp -q \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -P 2222 \ + /src/*.ps1 /src/*.cmd \ + Docker@127.0.0.1:C:/Users/Docker/qbraid-code-e2e/ + + sshpass -p admin scp -q \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -P 2222 \ + /src/tests/*.ps1 \ + Docker@127.0.0.1:C:/Users/Docker/qbraid-code-e2e/tests/ +' +``` + +For an interactive Windows shell: + +```bash +docker run --rm -it --network host qbraid-code-win-ssh \ + sshpass -p admin ssh -tt \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -p 2222 Docker@127.0.0.1 \ + powershell.exe -NoProfile -ExecutionPolicy Bypass +``` + +Do not put a real qBraid API key in the repository or in a committed test +script. Enter a disposable test key at the installer's prompt, or set it only in +the interactive Windows process: + +```powershell +$env:QBRAID_API_KEY = Read-Host 'Disposable qBraid API key' +$env:QBRAID_CODE_MODEL = 'claude-opus-5' +$env:Path = "$env:USERPROFILE\.local\bin;$env:Path" +``` + +## 5. Run the compatibility matrix + +Install a specific Claude Code release with Anthropic's installer: + +```powershell +$installer = [scriptblock]::Create((Invoke-RestMethod -Uri 'https://claude.ai/install.ps1')) +& $installer '2.1.186' +claude --version +``` + +For each row below, install the starting release, set the policy, record +`claude --version`, run the local qbraid-code installer, and verify the version +afterward. + +| Starting release | Policy | Expected result | +|---|---|---| +| `2.1.179` | `continue` | Remains 2.1.179; setup and model request succeed with reduced MCP guidance | +| `2.1.179` | `upgrade` | Moves to Anthropic `stable`; setup and model request succeed | +| `2.1.186` | `fail` | Remains 2.1.186; setup and model request succeed | +| `2.1.228` | `fail` | Remains 2.1.228; setup and model request succeed | +| `2.1.238` | `fail` | Remains 2.1.238; setup and model request succeed | + +The stable channel resolved to 2.1.228 during the recorded run. Treat the +channel as moving; the important assertion is that an upgrade reaches a +compatible release and that already-compatible or newer versions are not +replaced. + +```powershell +$env:QBRAID_CODE_CLAUDE_POLICY = 'fail' +$before = claude --version + +& C:\Users\Docker\qbraid-code-e2e\install.ps1 + +$after = claude --version +if ($before -ne $after) { + throw "Claude version changed unexpectedly: $before -> $after" +} + +qbraid-code --doctor +qbraid-code -p 'Reply with exactly: WINDOWS_E2E_OK' +``` + +For the old-version upgrade row, use `upgrade` and assert that `$after` is a +supported stable version instead of asserting equality. For the `continue` row, +`qbraid-code --doctor` deliberately reports the below-minimum release while the +real model request still proves the degraded path works. + +MCP OAuth is a separate browser-backed check. API-key validity does not +authenticate the MCP endpoint. Run `claude mcp login qbraid` only when a test +qBraid browser account is available; otherwise verify registration and expect +doctor to report that authentication is still needed. + +## 6. Mirror the Windows CI checks + +In the interactive Windows shell: + +```powershell +$root = 'C:\Users\Docker\qbraid-code-e2e' +$files = @(Get-ChildItem "$root\*.ps1") + + @(Get-ChildItem "$root\tests\*.ps1") + +foreach ($file in $files) { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + $file.FullName, [ref]$null, [ref]$errors) | Out-Null + if ($errors) { throw "Parse failure in $($file.Name): $errors" } +} + +if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) { + Install-PackageProvider NuGet -Force | Out-Null + Install-Module PSScriptAnalyzer -Force -Scope CurrentUser +} + +$results = Invoke-ScriptAnalyzer -Path $root -Severity Error,Warning -Recurse +$results | Format-Table -AutoSize +if ($results | Where-Object Severity -eq Error) { + throw 'PSScriptAnalyzer reported an error' +} + +& "$root\tests\claude-compat.Tests.ps1" +& "$root\tests\windows-profiles.ps1" +``` + +The recorded run parsed every script, reported no analyzer errors, and passed +all 45 PowerShell compatibility assertions. It also generated fresh profile +configuration and completed real gateway requests with Claude Code 2.1.179, +2.1.186, 2.1.228, and 2.1.238. + +## 7. Stop or destroy the VM + +Stop the container while keeping the installed Windows volume: + +```bash +docker stop qbraid-code-windows-e2e +``` + +Delete everything, including the Windows installation: + +```bash +docker rm -f qbraid-code-windows-e2e +docker volume rm qbraid-code-windows-e2e +docker image rm qbraid-code-win-ssh +``` diff --git a/install.ps1 b/install.ps1 index 1dfee80..ef4e577 100644 --- a/install.ps1 +++ b/install.ps1 @@ -54,6 +54,40 @@ $ClaudeJson = Join-Path $env:USERPROFILE '.claude.json' function Say { param($m) Write-Host "==> $m" -ForegroundColor White } function Ok { param($m) Write-Host " + $m" -ForegroundColor Green } function Warn { param($m) Write-Host " ! $m" -ForegroundColor Yellow } +function Write-RawText { + param([string]$Path, [string]$Text) + # Windows PowerShell 5.1 emits a BOM for `Set-Content -Encoding UTF8`, + # and a BOM on a .cmd makes cmd.exe fail to parse its first line. + if ($Path.EndsWith('.cmd')) { + $Text = ($Text -replace "`r`n", "`n") -replace "`n", "`r`n" + } + [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) +} +function Invoke-NativeQuietly { + param([string]$FilePath, [string[]]$ArgumentList) + # Windows PowerShell 5.1 promotes a native process's stderr to an error + # record. Under this installer's Stop preference, an expected non-zero + # probe would otherwise terminate the script before LASTEXITCODE is read. + $savedPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + & $FilePath @ArgumentList *> $null + return $LASTEXITCODE + } finally { + $ErrorActionPreference = $savedPreference + } +} +function Get-EnvMap { + param([string]$Path) + $values = @{} + if (-not (Test-Path -LiteralPath $Path)) { return $values } + foreach ($line in Get-Content -LiteralPath $Path) { + if ($line -match '^\s*([A-Z_]+)\s*=\s*(.*)$') { + $values[$Matches[1]] = $Matches[2] + } + } + return $values +} function Test-InteractiveConsole { try { return -not [Console]::IsInputRedirected } catch { return $false } } @@ -81,19 +115,16 @@ function Get-Prop { function Confirm-Step { param([string]$Question, [string]$Default = 'y') $hint = if ($Default -eq 'y') { '[Y/n]' } else { '[y/N]' } - $reply = (Read-Host "$Question $hint").Trim().ToLower() + $reply = Read-Host "$Question $hint" + # Redirected or exhausted stdin makes Read-Host return null in Windows + # PowerShell 5.1. Treat it like an empty answer rather than aborting a + # nearly completed installation by calling methods on null. + if ($null -eq $reply) { $reply = '' } + $reply = $reply.Trim().ToLower() if ([string]::IsNullOrEmpty($reply)) { $reply = $Default } return ($reply -eq 'y' -or $reply -eq 'yes') } -function Write-RawText { - param([string]$Path, [string]$Text) - if ($Path.EndsWith('.cmd')) { - $Text = ($Text -replace "`r`n", "`n") -replace "`n", "`r`n" - } - [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) -} - function Read-PidFile { param([string]$Path) $value = 0 @@ -319,12 +350,12 @@ if (-not $Profile) { $Profile = 'default' } if ($Profile -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$') { Die "invalid profile '$Profile'" } $ProfileRoot = Join-Path $ProfilesDir $Profile $ProfileDir = $ProfileRoot -$legacyEnv = Join-Path $HomeDir 'env' +$legacyEnvPath = Join-Path $HomeDir 'env' $defaultDir = Join-Path $ProfilesDir 'default' -if ((Test-Path $legacyEnv) -and -not (Test-Path $defaultDir)) { +if ((Test-Path $legacyEnvPath) -and -not (Test-Path $defaultDir)) { $migrationDir = Join-Path $ProfilesDir (".default.migrate.$PID.$([guid]::NewGuid().ToString('N'))") New-Item -ItemType Directory -Force -Path $migrationDir | Out-Null - $legacyLines = @(Get-Content $legacyEnv) + $legacyLines = @(Get-Content $legacyEnvPath) $legacyToken = '' $safeLines = @($legacyLines | Where-Object { if ($_ -match '^QBRAID_CODE_TOKEN=(.*)$') { $legacyToken = $Matches[1]; $false } else { $true } }) if ($legacyToken) { @@ -349,13 +380,13 @@ if ((Test-Path $legacyEnv) -and -not (Test-Path $defaultDir)) { } function Remove-LegacyPlaintextToken { if (-not (Test-Path $defaultDir)) { return } - if (Test-Path $legacyEnv) { - $legacyLines = @(Get-Content $legacyEnv) + if (Test-Path $legacyEnvPath) { + $legacyLines = @(Get-Content $legacyEnvPath) if (@($legacyLines | Where-Object { $_ -match '^QBRAID_CODE_TOKEN=' }).Count -gt 0) { $lines = @($legacyLines | Where-Object { $_ -notmatch '^QBRAID_CODE_TOKEN=' }) - $cleanEnv = "$legacyEnv.clean.$PID.$([guid]::NewGuid().ToString('N'))" + $cleanEnv = "$legacyEnvPath.clean.$PID.$([guid]::NewGuid().ToString('N'))" Write-RawText $cleanEnv (($lines -join "`n") + "`n") - Move-Item $cleanEnv $legacyEnv -Force + Move-Item $cleanEnv $legacyEnvPath -Force } } $legacyConfig = Join-Path $HomeDir 'proxy-config.yaml' @@ -431,11 +462,11 @@ $globalProfilePath = Join-Path $HomeDir 'global-profile' if (Test-Path $Settings) { try { $legacySettings = Get-Content $Settings -Raw | ConvertFrom-Json - $legacyEnv = Get-Prop $legacySettings 'env' - $legacyBase = Get-Prop $legacyEnv 'ANTHROPIC_BASE_URL' + $legacyClaudeEnv = Get-Prop $legacySettings 'env' + $legacyBase = Get-Prop $legacyClaudeEnv 'ANTHROPIC_BASE_URL' if ($legacyBase -like '*api-v2.qbraid.com*') { foreach ($key in @('ANTHROPIC_BASE_URL','ANTHROPIC_AUTH_TOKEN','ANTHROPIC_MODEL','ANTHROPIC_SMALL_FAST_MODEL','QBRAID_CODE_PROFILE','QBRAID_CODE_HOME')) { - $legacyEnv.PSObject.Properties.Remove($key) + $legacyClaudeEnv.PSObject.Properties.Remove($key) } Write-RawText $Settings ($legacySettings | ConvertTo-Json -Depth 20) Warn 'removed unsafe legacy plain-Claude gateway settings; use qbraid-code' @@ -705,8 +736,6 @@ if ($PSScriptRoot -and (Test-Path (Join-Path $PSScriptRoot 'qbraid-code.cmd'))) $SrcDir = $PSScriptRoot } -# Set-Content -Encoding UTF8 emits a BOM on Windows PowerShell 5.1, and a BOM -# on a .cmd makes cmd.exe fail to parse its first line. Write bytes directly. function Fetch-File { param([string]$Name, [string]$Dest) if ($SrcDir) { @@ -849,6 +878,18 @@ if ($ProxyBin) { $yaml += " - name: `"$gm`"" $yaml += " alias: `"$gm`"" } + $yaml += 'payload:' + $yaml += ' # Claude Code currently emits fixed-budget thinking for these custom model' + $yaml += ' # IDs, but their upstream APIs accept adaptive thinking only. Omit the' + $yaml += ' # incompatible fields rather than let every request fail with HTTP 400.' + $yaml += ' filter:' + $yaml += ' - models:' + $yaml += ' - name: "claude-opus-4-8"' + $yaml += ' - name: "claude-opus-5"' + $yaml += ' - name: "claude-sonnet-4-6"' + $yaml += ' params:' + $yaml += ' - "thinking"' + $yaml += ' - "output_config"' Write-RawText (Join-Path $ProfileDir 'proxy-template.yaml') (($yaml -join "`n") + "`n") Ok "proxy configured: all $($gptModels.Count + $claudeModels.Count) models on one endpoint (starts on demand)" } @@ -906,15 +947,16 @@ Ok "statusline enabled in $Settings" Say 'qBraid MCP' $mcpRegistered = $false if ($script:ClaudeMcpGet) { - claude mcp get $McpName *> $null - if ($LASTEXITCODE -eq 0) { + if ((Invoke-NativeQuietly 'claude' @('mcp', 'get', $McpName)) -eq 0) { $mcpRegistered = $true Ok 'already registered' } } if (-not $mcpRegistered -and (Test-ClaudeRequiredCapabilities)) { - claude mcp add --transport http $McpName $McpUrl --scope user *> $null - if ($LASTEXITCODE -ne 0) { Die 'could not register the qBraid MCP server.' } + $mcpAddExitCode = Invoke-NativeQuietly 'claude' @( + 'mcp', 'add', '--transport', 'http', $McpName, $McpUrl, '--scope', 'user' + ) + if ($mcpAddExitCode -ne 0) { Die 'could not register the qBraid MCP server.' } $mcpRegistered = $true Ok "registered $McpUrl" } elseif (-not $mcpRegistered) { @@ -933,8 +975,16 @@ if (-not $mcpRegistered) { } elseif (Confirm-Step 'Sign in to the qBraid MCP now? (opens a browser)' 'y') { # Unlike the piped bash path, `iex` keeps the console attached, so the # OAuth prompt can read the redirect URL directly. - claude mcp login $McpName - if ($LASTEXITCODE -ne 0) { Warn "MCP sign-in did not complete. Run ``claude mcp login $McpName`` later." } + $savedPreference = $ErrorActionPreference + $mcpLoginExitCode = 1 + try { + $ErrorActionPreference = 'Continue' + claude mcp login $McpName + $mcpLoginExitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $savedPreference + } + if ($mcpLoginExitCode -ne 0) { Warn "MCP sign-in did not complete. Run ``claude mcp login $McpName`` later." } } else { Warn "skipped. Run ``claude mcp login $McpName`` when you want the qBraid tools." } diff --git a/install.sh b/install.sh index bdc9d7f..4709e21 100755 --- a/install.sh +++ b/install.sh @@ -1037,6 +1037,21 @@ PEOF [ -n "$gm" ] || continue printf ' - name: "%s"\n alias: "%s"\n' "$gm" "$gm" done + cat <<'PEOF' +payload: + # Claude Code currently emits fixed-budget thinking for these custom model + # IDs, but their upstream APIs accept adaptive thinking only. CLIProxyAPI's + # filter phase is the reliable compatibility boundary: omit the incompatible + # fields rather than let every request fail with HTTP 400. + filter: + - models: + - name: "claude-opus-4-8" + - name: "claude-opus-5" + - name: "claude-sonnet-4-6" + params: + - "thinking" + - "output_config" +PEOF } > "$PROFILE_DIR/proxy-template.yaml" chmod 600 "$PROFILE_DIR/proxy-template.yaml" umask "$OLD_UMASK" diff --git a/tests/claude-compat.Tests.ps1 b/tests/claude-compat.Tests.ps1 index 2dc4679..ccc47c5 100644 --- a/tests/claude-compat.Tests.ps1 +++ b/tests/claude-compat.Tests.ps1 @@ -8,6 +8,10 @@ $ast = [System.Management.Automation.Language.Parser]::ParseFile( if ($errors) { throw "could not parse install.ps1: $errors" } $needed = @( + 'Write-RawText', + 'Invoke-NativeQuietly', + 'Get-EnvMap', + 'Confirm-Step', 'ConvertFrom-ClaudeVersionString', 'Compare-ClaudeVersion', 'Test-ClaudeUpgradeSafe', @@ -44,6 +48,43 @@ function Assert-Equal { } } +$writeRawTextDefinition = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Write-RawText' +}, $true) | Select-Object -First 1 +$firstWriteRawTextCall = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.GetCommandName() -eq 'Write-RawText' +}, $true) | Sort-Object { $_.Extent.StartOffset } | Select-Object -First 1 +Assert-Equal 'Write-RawText is defined before its first call' ` + ($writeRawTextDefinition.Extent.StartOffset -lt $firstWriteRawTextCall.Extent.StartOffset) $true +Assert-Equal 'expected native failure is returned instead of terminating' ` + (Invoke-NativeQuietly 'cmd.exe' @('/c', 'echo expected 1>&2 & exit /b 7')) 7 +$getEnvMapDefinition = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Get-EnvMap' +}, $true) | Select-Object -First 1 +$firstGetEnvMapCall = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.GetCommandName() -eq 'Get-EnvMap' +}, $true) | Sort-Object { $_.Extent.StartOffset } | Select-Object -First 1 +Assert-Equal 'Get-EnvMap is defined before its first call' ` + ($getEnvMapDefinition.Extent.StartOffset -lt $firstGetEnvMapCall.Extent.StartOffset) $true +$installerText = Get-Content $source -Raw +foreach ($adaptiveModel in @('claude-opus-4-8', 'claude-opus-5', 'claude-sonnet-4-6')) { + Assert-Equal "filter fixed thinking for $adaptiveModel" ` + ($installerText -match [regex]::Escape("- name: `"$adaptiveModel`"")) $true +} +Assert-Equal 'thinking compatibility filter removes request fields' ` + ($installerText -match '- \"thinking\"' -and $installerText -match '- \"output_config\"') $true +function global:Read-Host { return $null } +Assert-Equal 'exhausted stdin uses the confirmation default' (Confirm-Step 'Continue?' 'y') $true +Remove-Item Function:\Read-Host + $script:ClaudeMinVersion = '2.1.186' $script:ClaudeTestedMax = '2.1.238' diff --git a/tests/install-profiles.sh b/tests/install-profiles.sh index 0298fe9..cb9b304 100755 --- a/tests/install-profiles.sh +++ b/tests/install-profiles.sh @@ -49,7 +49,7 @@ done case "$url" in *billing/credits/balance*) org=org-alpha; case "$cfg" in *key-beta*) org=org-beta ;; esac; body="{\"data\":{\"organizationId\":\"$org\",\"qbraidCredits\":100}}"; code=200 ;; *organizations/current*) body='{"data":{"name":"Verified Lab"}}'; code=200 ;; *'/quota') body='{"plan":"pro"}'; code=200 ;; - *'/ai/models') body='{"data":[{"id":"claude-haiku-4-5","context_window":200000},{"id":"gpt-5.4","context_window":400000}]}'; code=200 ;; + *'/ai/models') body='{"data":[{"id":"claude-haiku-4-5","context_window":200000},{"id":"claude-opus-4-8","context_window":1000000},{"id":"claude-opus-5","context_window":1000000},{"id":"claude-sonnet-4-6","context_window":1000000},{"id":"gpt-5.4","context_window":400000}]}'; code=200 ;; *'/v1/messages') body='{"content":[{"text":"OK"}]}'; code=200 ;; *api.github.com*) exit 22 ;; *) body='{}'; code=404 ;; esac @@ -112,6 +112,13 @@ if ! grep -q 'ANTHROPIC_' "$HOME_ROOT/.claude/settings.json" && ! grep -q 'key-alpha\|key-beta' "$HOME_ROOT/.claude/settings.json"; then ok 'unsafe legacy plain-Claude credentials are removed' else bad 'plain Claude credential cleanup'; fi +if grep -q 'name: "claude-opus-4-8"' "$BETA_DIR/proxy-template.yaml" && + grep -q 'name: "claude-opus-5"' "$BETA_DIR/proxy-template.yaml" && + grep -q 'name: "claude-sonnet-4-6"' "$BETA_DIR/proxy-template.yaml" && + grep -q '^[[:space:]]*- "thinking"$' "$BETA_DIR/proxy-template.yaml" && + grep -q '^[[:space:]]*- "output_config"$' "$BETA_DIR/proxy-template.yaml"; then + ok 'adaptive-only Claude models filter incompatible fixed thinking' +else bad 'Claude thinking compatibility filter'; fi if QBRAID_API_KEY=key-alpha bash install.sh --profile beta > "$TMP/cross.out" 2> "$TMP/cross.err"; then bad 'profile accepted a different organization' diff --git a/tests/migration.sh b/tests/migration.sh index d762820..c174fbe 100755 --- a/tests/migration.sh +++ b/tests/migration.sh @@ -21,6 +21,13 @@ printf 'QBRAID_CODE_BASE_URL=https://example.invalid\nQBRAID_CODE_TOKEN=legacy-t printf 'legacy-cache\n' > "$ROOT/credits.cache" printf 'legacy-auth\n' > "$ROOT/proxy-auth/session" printf 'api-key: legacy-token\n' > "$ROOT/proxy-config.yaml" +NO_SS_BIN="$TMP/no-secret-service" +mkdir -p "$NO_SS_BIN" +cat > "$NO_SS_BIN/secret-tool" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF +chmod +x "$NO_SS_BIN/secret-tool" pass=0; fail=0 for slug in default a A0 'team.one' 'team_name' 'team-name' 12345678901234567890123456789012; do @@ -37,7 +44,7 @@ else fail=$((fail + 1)); printf ' FAIL C-locale label handling split UTF-8\n' fi -adopt_legacy_profile "$ROOT" +PATH="$NO_SS_BIN:$PATH" adopt_legacy_profile "$ROOT" if ! grep -q 'legacy-token' "$ROOT/profiles/default/env" && grep -q 'QBRAID_CODE_SECRET_BACKEND=file' "$ROOT/profiles/default/env" && [ "$(cat "$ROOT/secrets/default")" = legacy-token ] && From 8bcf74b977226dfc5d568c95ef1d1024807c1691 Mon Sep 17 00:00:00 2001 From: Kenny-Heitritter Date: Fri, 21 Aug 2026 12:00:53 -0500 Subject: [PATCH 2/2] Fix PowerShell test exit status --- tests/claude-compat.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/claude-compat.Tests.ps1 b/tests/claude-compat.Tests.ps1 index ccc47c5..b07b8f5 100644 --- a/tests/claude-compat.Tests.ps1 +++ b/tests/claude-compat.Tests.ps1 @@ -215,3 +215,4 @@ Remove-Item Function:\claude Write-Host "`n$($script:Passed) passed, $($script:Failed) failed" if ($script:Failed -ne 0) { exit 1 } +exit 0