[CmdletBinding()] param( [ValidateRange(1, 60)] [int]$LogWindowMinutes = 5 ) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $projectRoot = Split-Path -Parent $PSScriptRoot Set-Location -LiteralPath $projectRoot function Assert-HttpStatus { param( [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Uri ) try { $response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 5 } catch { throw "$Name is unavailable at $Uri. $($_.Exception.Message)" } if ($response.StatusCode -ne 200) { throw "$Name returned HTTP $($response.StatusCode) at $Uri." } Write-Host "[PASS] $Name returned HTTP 200." } function Assert-DependencyMetrics { param( [Parameter(Mandatory)] [string]$Service, [Parameter(Mandatory)] [string]$Uri ) $content = (Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 5).Content $dependencyLines = @($content -split "`n" | Where-Object { $_ -match '^voice_platform_dependency_up\{' }) if ($dependencyLines.Count -eq 0) { throw "$Service did not expose dependency health metrics." } $down = @($dependencyLines | Where-Object { $_ -match '\}\s+0(?:\.0+)?\s*$' }) if ($down.Count -gt 0) { throw "$Service has unavailable dependencies:`n$($down -join "`n")" } Write-Host "[PASS] $Service dependencies are available ($($dependencyLines.Count) checked)." } $requiredServices = @( 'api', 'callbacks', 'worker', 'web', 'mock-provider', 'mock-alert-receiver', 'postgres', 'redis', 'minio', 'keycloak' ) $runningServices = @(docker compose ps --services --status running) $missingServices = @($requiredServices | Where-Object { $_ -notin $runningServices }) if ($missingServices.Count -gt 0) { throw "Required services are not running: $($missingServices -join ', ')." } Write-Host "[PASS] All required local services are running." Assert-HttpStatus -Name 'API readiness' -Uri 'http://127.0.0.1:58080/health/ready' Assert-HttpStatus -Name 'Callbacks readiness' -Uri 'http://127.0.0.1:58081/health/ready' Assert-HttpStatus -Name 'Worker readiness' -Uri 'http://127.0.0.1:58082/health/ready' Assert-HttpStatus -Name 'Web readiness' -Uri 'http://127.0.0.1:3100/health/ready' Assert-HttpStatus -Name 'Mock provider liveness' -Uri 'http://127.0.0.1:59200/health/live' Assert-HttpStatus -Name 'Mock alert receiver liveness' -Uri 'http://127.0.0.1:59210/health/live' Assert-DependencyMetrics -Service 'API' -Uri 'http://127.0.0.1:58080/metrics' Assert-DependencyMetrics -Service 'Worker' -Uri 'http://127.0.0.1:58082/metrics' $outboxCounts = docker compose exec -T postgres psql -U voice -d voice_platform -tAc ` "SELECT count(*) FILTER (WHERE status IN ('pending', 'processing')), count(*) FILTER (WHERE status = 'dead') FROM outbox_event;" $outboxCounts = $outboxCounts.Trim() if ($outboxCounts -ne '0|0') { throw "Outbox has open/dead work: $outboxCounts." } Write-Host '[PASS] Outbox has no pending, processing, or dead events.' $recentLogs = docker compose logs --since "${LogWindowMinutes}m" api callbacks worker mock-provider mock-alert-receiver 2>&1 $errorLogs = @($recentLogs | Select-String -Pattern 'panic|fatal|level[=:]error|"level":"error"' -CaseSensitive:$false) if ($errorLogs.Count -gt 0) { throw "Recent service logs contain errors:`n$($errorLogs -join "`n")" } Write-Host "[PASS] No fatal or error-level logs in the last $LogWindowMinutes minute(s)." Write-Host 'Lightweight functional readiness check passed.'