Skip to content

fix: codex login on Windows hit nested quoting hell #12

fix: codex login on Windows hit nested quoting hell

fix: codex login on Windows hit nested quoting hell #12

Workflow file for this run

name: Windows smoke test
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
smoke:
name: start.bat end-to-end on Windows
runs-on: windows-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
shell: pwsh
run: npm ci
- name: Lint Windows launchers (.bat / .ps1)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$targets = @(
"start.bat", "stop.bat",
"scripts/dev/start-local-agent.ps1",
"scripts/dev/stop-local-agent.ps1",
"scripts/windows/capture-screen.ps1"
)
$failed = $false
foreach ($target in $targets) {
if (-not (Test-Path $target)) {
Write-Host "skip missing: $target"
continue
}
$bytes = [System.IO.File]::ReadAllBytes($target)
$text = [System.Text.Encoding]::ASCII.GetString($bytes)
# 1. Pure ASCII. cmd.exe on zh-CN runs code page 936 and will
# misdecode UTF-8 multi-byte sequences in .bat files,
# producing '文件名、目录名或卷标语法不正确'. Applying the
# same rule to .ps1 keeps us consistent and protects
# Windows PowerShell 5.1 hosts that can't read BOM-less
# UTF-8 reliably.
foreach ($b in $bytes) {
if ($b -gt 0x7f) {
Write-Error "$target contains non-ASCII byte 0x$("{0:X2}" -f $b) - use ASCII-only for Windows compatibility"
$failed = $true
break
}
}
# 2. CRLF line endings. LF-only .bat files confuse some cmd.exe
# versions, and Windows PowerShell 5.1 parses CRLF more
# reliably than LF.
if ($text -notmatch "`r`n") {
Write-Error "$target has no CRLF line endings"
$failed = $true
}
# 3. No Unix-style /dev/null redirection. cmd.exe would create
# an actual file named 'null' under a 'dev' directory.
if ($text -match "/dev/null") {
Write-Error "$target contains Unix-style '/dev/null' - use 'NUL' instead"
$failed = $true
}
}
if ($failed) { exit 1 }
Write-Host "All Windows launchers passed ASCII / CRLF / NUL lint."
- name: Run start.bat end-to-end
shell: cmd
run: |
start.bat
- name: Capture PID before stop.bat
shell: pwsh
run: |
$pidFile = "runtime/agent/agent.pid"
if (-not (Test-Path $pidFile)) {
Write-Error "agent.pid missing after start.bat — agent never wrote it"
exit 1
}
$capturedPid = (Get-Content $pidFile -Raw).Trim()
Write-Host "agent PID before stop: $capturedPid"
"AGENT_PID_BEFORE_STOP=$capturedPid" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Run stop.bat end-to-end
shell: cmd
run: |
stop.bat
- name: Verify stop.bat killed the full process tree
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
# 1. The tracked node.exe must be gone.
$tracked = [int]$env:AGENT_PID_BEFORE_STOP
try {
$null = Get-Process -Id $tracked -ErrorAction Stop
Write-Error "stop.bat left node.exe PID $tracked running"
exit 1
} catch {
Write-Host "tracked node.exe PID $tracked is gone"
}
# 2. The pid / port files must be cleaned up.
if (Test-Path "runtime/agent/agent.pid") {
Write-Error "stop.bat left runtime/agent/agent.pid behind"
exit 1
}
if (Test-Path "runtime/agent/agent.port") {
Write-Error "stop.bat left runtime/agent/agent.port behind"
exit 1
}
# 3. The runtime/agent folder must be deletable — i.e. no
# wrapper powershell.exe is still holding agent.log open.
try {
Copy-Item -Recurse -Force "runtime/agent" "runtime/agent.stop-test-copy"
Remove-Item -Recurse -Force "runtime/agent"
Write-Host "runtime/agent deleted cleanly — no leaked handles"
Move-Item "runtime/agent.stop-test-copy" "runtime/agent"
} catch {
Write-Error "runtime/agent could not be deleted after stop.bat: $_"
exit 1
}
- name: Start agent via start-local-agent.ps1 (foreground)
shell: pwsh
run: |
$ErrorActionPreference = "Continue"
Write-Host "Running start-local-agent.ps1 in foreground to surface errors..."
& "$env:GITHUB_WORKSPACE\scripts\dev\start-local-agent.ps1"
Write-Host "start-local-agent.ps1 exit code: $LASTEXITCODE"
- name: Wait for agent to be ready
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$portFile = "runtime/agent/agent.port"
$port = $null
for ($i = 0; $i -lt 90; $i++) {
if (Test-Path $portFile) {
$port = (Get-Content $portFile -Raw).Trim()
if ($port) { break }
}
Start-Sleep -Seconds 2
}
if (-not $port) {
Write-Error "agent.port file never appeared — start.bat failed to start the agent"
exit 1
}
Write-Host "Agent port: $port"
"AGENT_PORT=$port" | Out-File -FilePath $env:GITHUB_ENV -Append
$url = "http://127.0.0.1:$port/api/config"
for ($i = 0; $i -lt 60; $i++) {
try {
$resp = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 3
if ($resp.StatusCode -eq 200) {
Write-Host "GET /api/config -> 200 OK"
Write-Host $resp.Content
exit 0
}
} catch {
Start-Sleep -Seconds 2
}
}
Write-Error "Agent never responded on /api/config within 2 minutes"
exit 1
- name: Smoke test /desktop route
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$url = "http://127.0.0.1:$env:AGENT_PORT/desktop"
$resp = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 10
if ($resp.StatusCode -ne 200) {
Write-Error "/desktop returned $($resp.StatusCode)"
exit 1
}
Write-Host "/desktop -> 200 OK ($($resp.Content.Length) bytes)"
- name: Smoke test login (POST /api/pair)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$tokenFile = "runtime/agent/app_data/pairing-token.txt"
if (-not (Test-Path $tokenFile)) {
Write-Error "pairing-token.txt missing — agent never initialized session store"
exit 1
}
$token = (Get-Content $tokenFile -Raw).Trim()
Write-Host "Pairing token: $token"
$url = "http://127.0.0.1:$env:AGENT_PORT/api/pair"
# 1. Wrong token -> 401
try {
$bad = Invoke-WebRequest -UseBasicParsing -Uri $url -Method POST `
-ContentType "application/json" `
-Body '{"token":"wrong-token"}' -TimeoutSec 5
Write-Error "Wrong token should have failed, got $($bad.StatusCode)"
exit 1
} catch {
if ($_.Exception.Response.StatusCode.value__ -ne 401) {
Write-Error "Wrong token expected 401, got $($_.Exception.Response.StatusCode.value__)"
exit 1
}
Write-Host "Wrong token -> 401 (as expected)"
}
# 2. Correct token -> 200 { ok: true }
$body = @{ token = $token } | ConvertTo-Json -Compress
$ok = Invoke-WebRequest -UseBasicParsing -Uri $url -Method POST `
-ContentType "application/json" `
-Body $body -TimeoutSec 5
if ($ok.StatusCode -ne 200) {
Write-Error "/api/pair returned $($ok.StatusCode)"
exit 1
}
Write-Host "/api/pair -> 200 OK: $($ok.Content)"
- name: Run unit tests
shell: pwsh
run: npm test
- name: Stop agent
if: always()
shell: pwsh
run: |
$pidFile = "runtime/agent/agent.pid"
if (Test-Path $pidFile) {
$agentPid = (Get-Content $pidFile -Raw).Trim()
try {
Stop-Process -Id $agentPid -Force -ErrorAction Stop
Write-Host "Stopped agent PID $agentPid"
} catch {
Write-Host "Agent PID $agentPid already gone"
}
}
- name: Upload agent log
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-log
path: |
runtime/agent/agent.log
runtime/agent/agent.port
runtime/agent/agent.pid
if-no-files-found: warn