From 84d56bf901969c604c19e94134e9ac98e0b222cf Mon Sep 17 00:00:00 2001 From: KoalaTrapLord <265133236+KoalaTrapLord@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:50:04 -0400 Subject: [PATCH] =?UTF-8?q?Add=20Test-PolicyServer.ps1=20=E2=80=94=20smoke?= =?UTF-8?q?-test=20health=20+=20decide=20endpoint=20from=20PowerShell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test-PolicyServer.ps1 | 263 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 Test-PolicyServer.ps1 diff --git a/Test-PolicyServer.ps1 b/Test-PolicyServer.ps1 new file mode 100644 index 0000000..5fb6041 --- /dev/null +++ b/Test-PolicyServer.ps1 @@ -0,0 +1,263 @@ +<# +.SYNOPSIS + Smoke-tests the Commander AI Lab policy server from PowerShell. + Checks: server reachable → /api/policy/health → /api/policy/decide. + Prints a plain-English pass/fail for every step so you know + exactly where the pipeline is broken. + +.USAGE + # From repo root (server already running in another window): + .\Test-PolicyServer.ps1 + + # Custom host/port: + .\Test-PolicyServer.ps1 -BaseUrl http://localhost:9000 + + # Pretty-print the full decide response: + .\Test-PolicyServer.ps1 -Verbose +#> +param( + [string]$BaseUrl = "http://localhost:8000", + [switch]$Verbose +) + +$ErrorActionPreference = "Stop" + +# ─── helpers ──────────────────────────────────────────────────────────────── + +function Write-Pass { param([string]$msg) Write-Host " [PASS] $msg" -ForegroundColor Green } +function Write-Fail { param([string]$msg) Write-Host " [FAIL] $msg" -ForegroundColor Red } +function Write-Info { param([string]$msg) Write-Host " [INFO] $msg" -ForegroundColor Cyan } +function Write-Warn { param([string]$msg) Write-Host " [WARN] $msg" -ForegroundColor Yellow } +function Write-Step { param([string]$msg) Write-Host "`n── $msg" -ForegroundColor White } + +$allPassed = $true + +function Invoke-Step { + param( + [string] $Name, + [scriptblock] $Block + ) + Write-Step $Name + try { + & $Block + } catch { + Write-Fail $_.Exception.Message + $script:allPassed = $false + } +} + +# ─── Step 1: Server reachable ──────────────────────────────────────────────── + +Invoke-Step "Step 1 — Server reachable at $BaseUrl" { + $resp = Invoke-WebRequest -Uri "$BaseUrl/docs" -UseBasicParsing -TimeoutSec 5 -ErrorAction SilentlyContinue + if ($resp -and $resp.StatusCode -eq 200) { + Write-Pass "FastAPI server is up (GET /docs → 200)" + } else { + # /docs might be disabled; try root + $resp2 = Invoke-WebRequest -Uri "$BaseUrl/" -UseBasicParsing -TimeoutSec 5 -ErrorAction SilentlyContinue + if ($resp2 -and $resp2.StatusCode -lt 500) { + Write-Pass "FastAPI server is up (GET / → $($resp2.StatusCode))" + } else { + throw "Cannot reach $BaseUrl — is lab_api.py / uvicorn running?" + } + } +} + +# ─── Step 2: /api/policy/health ───────────────────────────────────────────── + +$modelLoaded = $false + +Invoke-Step "Step 2 — GET $BaseUrl/api/policy/health" { + $resp = Invoke-RestMethod -Uri "$BaseUrl/api/policy/health" -Method Get -TimeoutSec 10 + + if ($Verbose) { $resp | ConvertTo-Json -Depth 5 | Write-Host } + + if ($resp.ready -eq $true) { + Write-Pass "Model is loaded and ready (ready=true)" + $script:modelLoaded = $true + } elseif ($resp.status -eq "degraded") { + Write-Fail "Health endpoint returned status=degraded / ready=false" + Write-Warn " → The policy model is NOT loaded." + Write-Warn " → Start the server with --load-policy or call POST /api/ml/load_model first." + Write-Warn " → Check that models/ contains a valid checkpoint (.pt file)." + $script:allPassed = $false + } else { + Write-Warn "Unexpected health response — dumping:" + $resp | ConvertTo-Json -Depth 5 | Write-Host + $script:allPassed = $false + } +} + +# ─── Step 3: /api/policy/decide (synthetic snapshot) ───────────────────────── + +Invoke-Step "Step 3 — POST $BaseUrl/api/policy/decide (synthetic 4-player snapshot)" { + + if (-not $script:modelLoaded) { + Write-Warn "Skipping /decide test because model is not loaded (Step 2 failed)." + Write-Warn "Fix Step 2 first, then re-run this script." + return + } + + # Minimal but schema-valid DecideRequest. + # Matches the PlayerZoneState + DecideRequest pydantic models in routes/policy.py. + $body = @{ + game_id = "ps1-smoke-test" + turn = 3 + phase = "MAIN1" + active_player = 0 + playstyle = "midrange" + temperature = 1.0 + greedy = $false + players = @( + @{ + seat = 0 + name = "TestPlayer0" + life = 40 + manaAvailable = 3 + landCount = 3 + creaturesOnField = 1 + totalPowerOnBoard = 2 + hand = @("Cultivate", "Solemn Simulacrum", "Sol Ring") + battlefield = @("Forest", "Forest", "Forest", "Birds of Paradise") + graveyard = @() + commandZone = @("Atraxa, Praetors' Voice") + }, + @{ + seat = 1 + name = "TestPlayer1" + life = 38 + manaAvailable = 2 + landCount = 2 + creaturesOnField = 0 + totalPowerOnBoard = 0 + hand = @() + handCount = 4 + battlefield = @("Island", "Swamp") + graveyard = @() + commandZone = @("Ur-Dragon") + }, + @{ + seat = 2 + name = "TestPlayer2" + life = 35 + manaAvailable = 4 + landCount = 4 + creaturesOnField = 2 + totalPowerOnBoard = 5 + hand = @() + handCount = 3 + battlefield = @("Mountain", "Mountain", "Mountain", "Mountain", "Goblin Guide", "Monastery Swiftspear") + graveyard = @() + commandZone = @("Krenko, Mob Boss") + }, + @{ + seat = 3 + name = "TestPlayer3" + life = 40 + manaAvailable = 0 + landCount = 0 + creaturesOnField = 0 + totalPowerOnBoard = 0 + hand = @() + handCount = 7 + battlefield = @() + graveyard = @() + commandZone = @("Prossh, Skyraider of Kher") + } + ) + } | ConvertTo-Json -Depth 10 + + $resp = Invoke-RestMethod ` + -Uri "$BaseUrl/api/policy/decide" ` + -Method Post ` + -Body $body ` + -ContentType "application/json" ` + -TimeoutSec 30 + + if ($Verbose) { + Write-Host "" + Write-Host " Full /decide response:" -ForegroundColor White + $resp | ConvertTo-Json -Depth 6 | Write-Host + } + + # Validate shape of response + $ok = $true + if ([string]::IsNullOrEmpty($resp.action)) { + Write-Fail " response.action is empty" + $ok = $false + } + if ($null -eq $resp.confidence) { + Write-Fail " response.confidence is missing" + $ok = $false + } + if ($resp.PSObject.Properties["error"] -and -not [string]::IsNullOrEmpty($resp.error)) { + Write-Fail " Server returned an error: $($resp.error)" + $ok = $false + } + + if ($ok) { + $confPct = [math]::Round($resp.confidence * 100, 1) + Write-Pass "Decision received → action='$($resp.action)' confidence=$($confPct)% latency=$($resp.inference_ms)ms src=$($resp.vector_source)" + + # Top-3 probabilities + if ($resp.probabilities) { + $top3 = $resp.probabilities.PSObject.Properties | + Sort-Object { $_.Value } -Descending | + Select-Object -First 3 + Write-Info "Top-3 probabilities:" + foreach ($p in $top3) { + $pct = [math]::Round($p.Value * 100, 1) + Write-Info " $($p.Name.PadRight(20)) $pct%" + } + } + } else { + $script:allPassed = $false + } +} + +# ─── Step 4: Verify the decide log line appeared ────────────────────────────── + +Invoke-Step "Step 4 — Confirm [routes.policy.decide] log line guidance" { + Write-Info "The /decide endpoint logs at INFO level — you should see a line like:" + Write-Info " [decide] turn=3 phase=main_1 → cast_ramp (conf=0.810) | top5=[...] | src=encoder" + Write-Info "" + Write-Info "If that line is MISSING from your server console:" + Write-Info " 1. Confirm uvicorn is started with --log-level info (default)" + Write-Info " 2. Check that lab_api.py calls register_policy_routes(app, policy_service)" + Write-Info " 3. Confirm routes/policy.py is included in the app router (see routes/__init__.py)" + Write-Info " 4. The log name is 'routes.policy.decide' — ensure logging isn't filtered" + Write-Pass "Guidance printed" +} + +# ─── Step 5: Why Forge still ignores the policy server ─────────────────────── + +Invoke-Step "Step 5 — Forge IPC connection status (informational)" { + Write-Warn "Forge does NOT call /api/policy/decide automatically." + Write-Warn "routes/game.py POST /api/game/start has a TODO: 'Wire Forge engine launch here'." + Write-Warn "" + Write-Warn "Until the IPC bridge is wired, you will only see /decide hits from:" + Write-Warn " - This smoke-test script" + Write-Warn " - overnight-run.py (batch simulation path)" + Write-Warn " - A manual curl/Invoke-RestMethod call" + Write-Warn "" + Write-Warn "Forge (Commander) launches its own built-in AI and plays lands/passes" + Write-Warn "because it has no HTTP client pointed at localhost:8000." + Write-Warn "" + Write-Warn "Next step: wire GameSession launch inside POST /api/game/start," + Write-Warn "or add a PolicyClient HTTP hook in Forge's AiController." + Write-Pass "Status explained" +} + +# ─── Summary ───────────────────────────────────────────────────────────────── + +Write-Host "" +if ($allPassed) { + Write-Host "══════════════════════════════════════════" -ForegroundColor Green + Write-Host " ALL STEPS PASSED — policy server is live" -ForegroundColor Green + Write-Host "══════════════════════════════════════════" -ForegroundColor Green +} else { + Write-Host "══════════════════════════════════════════" -ForegroundColor Red + Write-Host " ONE OR MORE STEPS FAILED — see output above" -ForegroundColor Red + Write-Host "══════════════════════════════════════════" -ForegroundColor Red + exit 1 +}