Files
ai-contract-platform/scripts/run-phase3-closed-loop.ps1
T

219 lines
8.0 KiB
PowerShell

param(
[int]$BackendPort = 8003,
[int]$FrontendPort = 3006,
[switch]$SkipBaseline
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$frontendDir = Join-Path $repoRoot "frontend"
$backendDir = Join-Path $repoRoot "backend"
$backendPython = Join-Path $backendDir ".venv\Scripts\python.exe"
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
$artifactDir = Join-Path $repoRoot "project-docs\qa-artifacts\phase3-closed-loop\$timestamp"
$runtimeDir = Join-Path $artifactDir "runtime"
$frontendLog = Join-Path $artifactDir "frontend-dev-server.log"
$backendLog = Join-Path $artifactDir "backend-server.log"
$sharedAiLog = Join-Path $artifactDir "shared-ai-stub.log"
$npmAuditLog = Join-Path $artifactDir "npm-audit-high.json"
$pipCheckLog = Join-Path $artifactDir "pip-check.txt"
$backendUrl = "http://127.0.0.1:$BackendPort"
$webUrl = "http://127.0.0.1:$FrontendPort"
$sqliteDbPath = Join-Path $runtimeDir "phase3-contract-platform.db"
$sqliteDbUrl = ("sqlite+pysqlite:///" + (($sqliteDbPath -replace "\\", "/")))
$localStorageDir = Join-Path $runtimeDir "storage"
$sharedAiPort = 18011
$sharedAiBaseUrl = "http://127.0.0.1:$sharedAiPort"
$sharedAiApiPrefix = "/api/v0.1"
$sharedAiAppId = "ai-contract-platform"
$sharedAiApiKey = "shared-ai-platform-admin-dev-token"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null
New-Item -ItemType Directory -Force -Path $localStorageDir | Out-Null
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
throw "gstack is required for all AI-assisted work in this repo."
}
function Wait-HttpReady {
param(
[string]$Url,
[int]$TimeoutSeconds = 90
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
try {
$response = Invoke-WebRequest -Uri $Url -TimeoutSec 3
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
return
}
} catch {
Start-Sleep -Seconds 2
}
} while ((Get-Date) -lt $deadline)
throw "Timed out waiting for $Url"
}
function Stop-Listener {
param([int]$Port)
$listener = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue |
Where-Object { $_.State -eq "Listen" } |
Select-Object -First 1
if ($listener) {
Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue
}
}
function Stop-ExistingNextDevProcess {
param([string]$TargetDirectory)
$normalizedTarget = $TargetDirectory.ToLowerInvariant()
$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
$_.Name -eq "node.exe" -and
$_.CommandLine -and
$_.CommandLine.ToLowerInvariant().Contains($normalizedTarget) -and
(
$_.CommandLine -match "next\\dist\\bin\\next" -or
$_.CommandLine -match "start-server\.js"
)
}
foreach ($process in $processes) {
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
}
}
$frontendProcess = $null
$backendProcess = $null
$sharedAiProcess = $null
try {
if (-not (Test-Path (Join-Path $frontendDir "node_modules"))) {
Push-Location $frontendDir
npm.cmd install --no-package-lock
if ($LASTEXITCODE -ne 0) { throw "npm install failed" }
Pop-Location
}
if (-not (Test-Path (Join-Path $frontendDir "node_modules\playwright"))) {
Push-Location $frontendDir
npm.cmd install --no-save --no-package-lock playwright
if ($LASTEXITCODE -ne 0) { throw "playwright install failed" }
Pop-Location
}
if (-not $SkipBaseline) {
Push-Location $frontendDir
npm.cmd run lint
if ($LASTEXITCODE -ne 0) { throw "frontend lint failed" }
npm.cmd run build
if ($LASTEXITCODE -ne 0) { throw "frontend build failed" }
npm.cmd audit --audit-level=high --json > $npmAuditLog
if ($LASTEXITCODE -ne 0) { throw "frontend npm audit high threshold failed" }
Pop-Location
Push-Location $repoRoot
.\backend\.venv\Scripts\python.exe -m pytest -q backend/tests
if ($LASTEXITCODE -ne 0) { throw "backend pytest failed" }
.\backend\.venv\Scripts\python.exe -m pip check > $pipCheckLog
if ($LASTEXITCODE -ne 0) { throw "backend pip check failed" }
Pop-Location
}
Stop-Listener -Port $BackendPort
Stop-ExistingNextDevProcess -TargetDirectory $frontendDir
Stop-Listener -Port $FrontendPort
Stop-Listener -Port $sharedAiPort
$env:PYTHONPATH = $backendDir
$env:DATABASE_URL = $sqliteDbUrl
$env:FILE_STORAGE_BACKEND = "local"
$env:LOCAL_STORAGE_DIR = $localStorageDir
$env:SHARED_AI_PLATFORM_BASE_URL = $sharedAiBaseUrl
$env:SHARED_AI_PLATFORM_API_PREFIX = $sharedAiApiPrefix
$env:SHARED_AI_PLATFORM_APP_ID = $sharedAiAppId
$env:SHARED_AI_PLATFORM_API_KEY = $sharedAiApiKey
$env:SHARED_AI_PLATFORM_TIMEOUT_SECONDS = "180"
$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_ENABLED = "true"
$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_FALLBACK_TO_LOCAL = "false"
$sharedAiCommand = "`$env:SLICE4_SHARED_AI_STUB_PORT='$sharedAiPort'; node .\\frontend\\scripts\\slice4-shared-ai-stub.mjs *> '$sharedAiLog'"
$sharedAiProcess = Start-Process -FilePath "powershell.exe" `
-ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $sharedAiCommand) `
-WorkingDirectory $repoRoot `
-WindowStyle Hidden `
-PassThru
Wait-HttpReady -Url "$sharedAiBaseUrl/health" -TimeoutSeconds 60
@'
from app.db import Base, engine
import app.models # noqa: F401
Base.metadata.create_all(bind=engine)
print("PHASE3_SQLITE_SCHEMA_READY")
'@ | & $backendPython -
if ($LASTEXITCODE -ne 0) { throw "phase3 sqlite schema bootstrap failed" }
$backendCommand = @"
`$env:PYTHONPATH='$backendDir'
`$env:DATABASE_URL='$sqliteDbUrl'
`$env:FILE_STORAGE_BACKEND='local'
`$env:LOCAL_STORAGE_DIR='$localStorageDir'
`$env:SHARED_AI_PLATFORM_BASE_URL='$sharedAiBaseUrl'
`$env:SHARED_AI_PLATFORM_API_PREFIX='$sharedAiApiPrefix'
`$env:SHARED_AI_PLATFORM_APP_ID='$sharedAiAppId'
`$env:SHARED_AI_PLATFORM_API_KEY='$sharedAiApiKey'
`$env:SHARED_AI_PLATFORM_TIMEOUT_SECONDS='180'
`$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_ENABLED='true'
`$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_FALLBACK_TO_LOCAL='false'
& '$backendPython' -m uvicorn app.main:app --host 127.0.0.1 --port $BackendPort *> '$backendLog'
"@
$backendProcess = Start-Process -FilePath "powershell.exe" `
-ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $backendCommand) `
-WorkingDirectory $backendDir `
-WindowStyle Hidden `
-PassThru
Wait-HttpReady -Url "$backendUrl/health" -TimeoutSeconds 90
Wait-HttpReady -Url "$backendUrl/api/v1/system/readiness" -TimeoutSeconds 90
$frontendCommand = "`$env:NEXT_PUBLIC_API_BASE_URL='/api/v1'; `$env:BACKEND_BASE_URL='$backendUrl'; npm.cmd run dev -- --hostname 127.0.0.1 --port $FrontendPort *> '$frontendLog'"
$frontendProcess = Start-Process -FilePath "powershell.exe" `
-ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $frontendCommand) `
-WorkingDirectory $frontendDir `
-WindowStyle Hidden `
-PassThru
Wait-HttpReady -Url "$webUrl/login" -TimeoutSeconds 120
Push-Location $frontendDir
$env:PHASE3_WEB_URL = $webUrl
$env:PHASE3_API_URL = "$webUrl/api/v1"
$env:PHASE3_HEALTH_URL = "$backendUrl/health"
$env:PHASE3_READINESS_URL = "$backendUrl/api/v1/system/readiness"
$env:PHASE3_ARTIFACT_DIR = $artifactDir
$env:AI_CONTRACT_ADMIN_PASSWORD = "123456"
node .\scripts\phase3-closed-loop-e2e.mjs
if ($LASTEXITCODE -ne 0) { throw "phase3 closed-loop e2e failed" }
Pop-Location
Write-Host "PHASE3_CLOSED_LOOP_PASS $artifactDir"
} finally {
if ($frontendProcess -and -not $frontendProcess.HasExited) {
Stop-Process -Id $frontendProcess.Id -Force -ErrorAction SilentlyContinue
}
if ($backendProcess -and -not $backendProcess.HasExited) {
Stop-Process -Id $backendProcess.Id -Force -ErrorAction SilentlyContinue
}
if ($sharedAiProcess -and -not $sharedAiProcess.HasExited) {
Stop-Process -Id $sharedAiProcess.Id -Force -ErrorAction SilentlyContinue
}
Stop-Listener -Port $FrontendPort
Stop-Listener -Port $BackendPort
Stop-Listener -Port $sharedAiPort
}