param( [string]$BackendBaseUrl = "http://192.168.3.90:8001", [int]$BackendPort = 8002, [int]$FrontendPort = 3005, [switch]$UseLocalBackend, [switch]$SkipBaseline ) $ErrorActionPreference = "Stop" $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $frontendDir = Join-Path $repoRoot "frontend" $backendDir = Join-Path $repoRoot "backend" $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" $artifactDir = Join-Path $repoRoot "project-docs\qa-artifacts\phase2-closed-loop\$timestamp" $frontendLog = Join-Path $artifactDir "frontend-dev-server.log" $backendLog = Join-Path $artifactDir "backend-server.log" $localBackendUrl = "http://127.0.0.1:$BackendPort" $backendUrl = if ($UseLocalBackend) { $localBackendUrl } else { $BackendBaseUrl.TrimEnd('/') } $webUrl = "http://127.0.0.1:$FrontendPort" $runLocalBackend = [bool]$UseLocalBackend if (-not $runLocalBackend) { $BackendPort = 0 } New-Item -ItemType Directory -Force -Path $artifactDir | 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 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 package install failed" } Pop-Location } Push-Location $frontendDir npx.cmd playwright install chromium if ($LASTEXITCODE -ne 0) { throw "playwright browser 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" } Pop-Location Push-Location $repoRoot .\backend\.venv\Scripts\python.exe -m pytest -q backend/tests if ($LASTEXITCODE -ne 0) { throw "backend pytest failed" } Pop-Location } if ($runLocalBackend) { Stop-Listener -Port $BackendPort } Stop-ExistingNextDevProcess -TargetDirectory $frontendDir Stop-Listener -Port $FrontendPort if ($runLocalBackend) { $backendCommand = "`$env:PYTHONPATH='$backendDir'; .\.venv\Scripts\python.exe -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 if (-not $runLocalBackend) { "REMOTE_BACKEND_MODE $backendUrl" | Set-Content -Path $backendLog -Encoding UTF8 } $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:PHASE2_WEB_URL = $webUrl $env:PHASE2_API_URL = "$backendUrl/api/v1" $env:PHASE2_HEALTH_URL = "$backendUrl/health" $env:PHASE2_ARTIFACT_DIR = $artifactDir node .\scripts\phase2-closed-loop-e2e.mjs if ($LASTEXITCODE -ne 0) { throw "phase2 closed-loop e2e failed" } Pop-Location Write-Host "PHASE2_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 } Stop-Listener -Port $FrontendPort if ($runLocalBackend) { Stop-Listener -Port $BackendPort } }