261 lines
9.9 KiB
PowerShell
261 lines
9.9 KiB
PowerShell
<#
|
|
.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.
|
|
|
|
.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" {
|
|
$r1 = $null
|
|
try { $r1 = Invoke-WebRequest -Uri "$BaseUrl/docs" -UseBasicParsing -TimeoutSec 5 } catch {}
|
|
if ($r1 -and $r1.StatusCode -eq 200) {
|
|
Write-Pass "FastAPI server is up (GET /docs -> 200)"
|
|
return
|
|
}
|
|
$r2 = $null
|
|
try { $r2 = Invoke-WebRequest -Uri "$BaseUrl/" -UseBasicParsing -TimeoutSec 5 } catch {}
|
|
if ($r2 -and $r2.StatusCode -lt 500) {
|
|
Write-Pass "FastAPI server is up (GET / -> $($r2.StatusCode))"
|
|
return
|
|
}
|
|
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 returned status=degraded / ready=false"
|
|
Write-Warn " -> Policy model is NOT loaded."
|
|
Write-Warn " -> Start the server with --load-policy, or call POST /api/ml/load_model first."
|
|
Write-Warn " -> Confirm models/ contains a valid .pt checkpoint."
|
|
$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 -- model not loaded (Step 2 failed)."
|
|
Write-Warn "Fix Step 2 first, then re-run."
|
|
return
|
|
}
|
|
|
|
# Schema-valid DecideRequest matching routes/policy.py PlayerZoneState.
|
|
# Note: apostrophes inside card names must be escaped as backtick-single-quote
|
|
# when inside a double-quoted PowerShell string, OR just use the ASCII form here.
|
|
$body = [ordered]@{
|
|
game_id = "ps1-smoke-test"
|
|
turn = 3
|
|
phase = "MAIN1"
|
|
active_player = 0
|
|
playstyle = "midrange"
|
|
temperature = 1.0
|
|
greedy = $false
|
|
players = @(
|
|
[ordered]@{
|
|
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")
|
|
},
|
|
[ordered]@{
|
|
seat = 1
|
|
name = "TestPlayer1"
|
|
life = 38
|
|
manaAvailable = 2
|
|
landCount = 2
|
|
creaturesOnField = 0
|
|
totalPowerOnBoard = 0
|
|
handCount = 4
|
|
hand = @()
|
|
battlefield = @("Island", "Swamp")
|
|
graveyard = @()
|
|
commandZone = @("Ur-Dragon")
|
|
},
|
|
[ordered]@{
|
|
seat = 2
|
|
name = "TestPlayer2"
|
|
life = 35
|
|
manaAvailable = 4
|
|
landCount = 4
|
|
creaturesOnField = 2
|
|
totalPowerOnBoard = 5
|
|
handCount = 3
|
|
hand = @()
|
|
battlefield = @("Mountain", "Mountain", "Mountain", "Mountain", "Goblin Guide", "Monastery Swiftspear")
|
|
graveyard = @()
|
|
commandZone = @("Krenko, Mob Boss")
|
|
},
|
|
[ordered]@{
|
|
seat = 3
|
|
name = "TestPlayer3"
|
|
life = 40
|
|
manaAvailable = 0
|
|
landCount = 0
|
|
creaturesOnField = 0
|
|
totalPowerOnBoard = 0
|
|
handCount = 7
|
|
hand = @()
|
|
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
|
|
}
|
|
|
|
$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 error: $($resp.error)"
|
|
$ok = $false
|
|
}
|
|
|
|
if ($ok) {
|
|
$confPct = [math]::Round($resp.confidence * 100, 1)
|
|
Write-Pass "Decision received -> action='$($resp.action)' conf=$($confPct)% latency=$($resp.inference_ms)ms src=$($resp.vector_source)"
|
|
|
|
if ($resp.probabilities) {
|
|
$top3 = $resp.probabilities.PSObject.Properties |
|
|
Sort-Object { $_.Value } -Descending |
|
|
Select-Object -First 3
|
|
Write-Info "Top-3 action probabilities:"
|
|
foreach ($p in $top3) {
|
|
$pct = [math]::Round($p.Value * 100, 1)
|
|
Write-Info (" " + $p.Name.PadRight(22) + "$pct%")
|
|
}
|
|
}
|
|
} else {
|
|
$script:allPassed = $false
|
|
}
|
|
}
|
|
|
|
# ---- Step 4: log line guidance ----------------------------------------------
|
|
|
|
Invoke-Step "Step 4 -- Confirm [routes.policy.decide] log line in server console" {
|
|
Write-Info "After Step 3 you should see in the uvicorn window:"
|
|
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:"
|
|
Write-Info " 1. Confirm uvicorn started with --log-level info (the default)"
|
|
Write-Info " 2. Confirm lab_api.py calls register_policy_routes(app, policy_svc)"
|
|
Write-Info " 3. Confirm routes/policy.py is included via routes/__init__.py"
|
|
Write-Info " 4. Logger name is [routes.policy.decide] -- check no filter silences it"
|
|
Write-Pass "Guidance printed"
|
|
}
|
|
|
|
# ---- Step 5: why Forge ignores the policy server ----------------------------
|
|
|
|
Invoke-Step "Step 5 -- Forge IPC status (informational)" {
|
|
Write-Warn "Forge does NOT call /api/policy/decide on its own."
|
|
Write-Warn "routes/game.py POST /api/game/start has a TODO: wire Forge engine launch."
|
|
Write-Warn ""
|
|
Write-Warn "Until wired, /decide is only hit by:"
|
|
Write-Warn " - This script"
|
|
Write-Warn " - overnight-run.py (batch sim path)"
|
|
Write-Warn " - Manual Invoke-RestMethod / curl calls"
|
|
Write-Warn ""
|
|
Write-Warn "Forge launches its built-in AI and plays lands because it has"
|
|
Write-Warn "no HTTP client pointed at localhost:8000."
|
|
Write-Warn ""
|
|
Write-Warn "Next step: wire GameSession inside POST /api/game/start,"
|
|
Write-Warn "or add a PolicyClient hook in Forge AiController.java."
|
|
Write-Pass "Status explained"
|
|
}
|
|
|
|
# ---- Summary ----------------------------------------------------------------
|
|
|
|
Write-Host ""
|
|
if ($allPassed) {
|
|
Write-Host "==========================================" -ForegroundColor Green
|
|
Write-Host " ALL STEPS PASSED -- policy server live" -ForegroundColor Green
|
|
Write-Host "==========================================" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "==========================================" -ForegroundColor Red
|
|
Write-Host " ONE OR MORE STEPS FAILED -- see above" -ForegroundColor Red
|
|
Write-Host "==========================================" -ForegroundColor Red
|
|
exit 1
|
|
}
|