285 lines
9.5 KiB
PowerShell
285 lines
9.5 KiB
PowerShell
param(
|
|
[string]$GatewayBaseUrl = "https://stella-ops.local",
|
|
[string]$AdvisoryAiBaseUrl = "http://advisoryai.stella-ops.local",
|
|
[string]$Tenant = "stellaops",
|
|
[int]$TimeoutSeconds = 20,
|
|
[switch]$SkipUiResponsivenessProbe
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
function Invoke-Probe {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Name,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Method,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[string]$Body
|
|
)
|
|
|
|
$headersFile = [System.IO.Path]::GetTempFileName()
|
|
$bodyFile = [System.IO.Path]::GetTempFileName()
|
|
try {
|
|
$curlArgs = @(
|
|
"-k"
|
|
"-sS"
|
|
"--noproxy"
|
|
"*"
|
|
"-D"
|
|
$headersFile
|
|
"-o"
|
|
$bodyFile
|
|
"-X"
|
|
$Method
|
|
"--max-time"
|
|
$TimeoutSeconds.ToString([System.Globalization.CultureInfo]::InvariantCulture)
|
|
"-H"
|
|
"Accept: application/json"
|
|
"-H"
|
|
"X-Tenant: $Tenant"
|
|
"-H"
|
|
"X-StellaOps-Tenant: $Tenant"
|
|
"-H"
|
|
"X-Stella-Tenant: $Tenant"
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($Body)) {
|
|
$curlArgs += @(
|
|
"-H"
|
|
"Content-Type: application/json"
|
|
"--data-raw"
|
|
$Body
|
|
)
|
|
}
|
|
|
|
$url = "$GatewayBaseUrl$Path"
|
|
$curlArgs += @("-w", "%{http_code}", $url)
|
|
|
|
$statusText = (& curl.exe @curlArgs).Trim()
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "curl failed for $Name ($Method $Path) with exit code $LASTEXITCODE."
|
|
}
|
|
|
|
if ($statusText -notmatch "^\d{3}$") {
|
|
throw "Unable to parse HTTP status for $Name ($Method $Path). Raw status text: '$statusText'."
|
|
}
|
|
|
|
$statusCode = [int]$statusText
|
|
$contentTypeLine = Get-Content -LiteralPath $headersFile |
|
|
Where-Object { $_ -match "^[Cc]ontent-[Tt]ype:" } |
|
|
Select-Object -Last 1
|
|
$contentTypeLine = [string]$contentTypeLine
|
|
$contentType = if ([string]::IsNullOrWhiteSpace($contentTypeLine)) {
|
|
""
|
|
} else {
|
|
(($contentTypeLine -replace "^[Cc]ontent-[Tt]ype:\s*", "") -replace "\r", "").Trim()
|
|
}
|
|
|
|
$responseBody = [string](Get-Content -LiteralPath $bodyFile -Raw -ErrorAction SilentlyContinue)
|
|
if ($null -eq $responseBody) {
|
|
$responseBody = ""
|
|
}
|
|
if ($responseBody.Length -gt 800) {
|
|
$responseBody = $responseBody.Substring(0, 800)
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Name = $Name
|
|
Method = $Method
|
|
Path = $Path
|
|
Url = $url
|
|
StatusCode = $statusCode
|
|
ContentType = $contentType
|
|
BodyPreview = $responseBody
|
|
}
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $headersFile -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $bodyFile -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Invoke-UiResponsivenessProbe {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BaseUrl
|
|
)
|
|
|
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\\..\\..")
|
|
$webNodeModules = Join-Path $repoRoot "src\\Web\\StellaOps.Web\\node_modules"
|
|
if (-not (Test-Path -LiteralPath $webNodeModules)) {
|
|
throw "UI responsiveness probe requires $webNodeModules. Install web dependencies first."
|
|
}
|
|
|
|
$probeScriptPath = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), ".cjs")
|
|
$probeScript = @'
|
|
const { chromium } = require("playwright");
|
|
|
|
const targetUrl = process.argv[2];
|
|
if (!targetUrl) {
|
|
throw new Error("Missing target URL argument.");
|
|
}
|
|
|
|
const evaluateWithTimeout = async (page, timeoutMs) => {
|
|
return Promise.race([
|
|
page.evaluate(() => ({
|
|
readyState: document.readyState,
|
|
title: document.title,
|
|
location: window.location.href,
|
|
appRootPresent: !!document.querySelector("app-root"),
|
|
globalSearchInputCount: document.querySelectorAll('app-global-search input[type="text"]').length,
|
|
})),
|
|
new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error(`ui-evaluate-timeout-${timeoutMs}ms`)), timeoutMs);
|
|
}),
|
|
]);
|
|
};
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] });
|
|
try {
|
|
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
await page.waitForTimeout(3000);
|
|
const probe = await evaluateWithTimeout(page, 10000);
|
|
console.log(JSON.stringify(probe));
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error?.stack ?? String(error));
|
|
process.exit(1);
|
|
});
|
|
'@
|
|
|
|
$originalNodePath = $env:NODE_PATH
|
|
try {
|
|
Set-Content -LiteralPath $probeScriptPath -Value $probeScript -Encoding UTF8
|
|
$env:NODE_PATH = $webNodeModules
|
|
$probeOutput = (& node $probeScriptPath $BaseUrl 2>&1)
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "UI responsiveness probe failed for $BaseUrl. Output: $probeOutput"
|
|
}
|
|
|
|
$probeJsonLine = [string]($probeOutput | Select-Object -Last 1)
|
|
$probe = $probeJsonLine | ConvertFrom-Json
|
|
if ([string]::IsNullOrWhiteSpace($probe.title)) {
|
|
throw "UI responsiveness probe returned an empty document title for $BaseUrl."
|
|
}
|
|
|
|
Write-Host "[OK] UI responsiveness probe: title='$($probe.title)' readyState=$($probe.readyState) url=$($probe.location)"
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $probeScriptPath -ErrorAction SilentlyContinue
|
|
if ($null -eq $originalNodePath) {
|
|
Remove-Item Env:NODE_PATH -ErrorAction SilentlyContinue
|
|
} else {
|
|
$env:NODE_PATH = $originalNodePath
|
|
}
|
|
}
|
|
}
|
|
|
|
$rebuildProbeHeaders = @(
|
|
"-H", "Accept: application/json",
|
|
"-H", "Content-Type: application/json",
|
|
"-H", "X-StellaOps-Tenant: $Tenant",
|
|
"-H", "X-StellaOps-Scopes: advisory-ai:admin advisory-ai:operate advisory-ai:view",
|
|
"-H", "X-StellaOps-Actor: header-search-smoke"
|
|
)
|
|
|
|
$rebuildHeadersFile = [System.IO.Path]::GetTempFileName()
|
|
$rebuildBodyFile = [System.IO.Path]::GetTempFileName()
|
|
try {
|
|
$rebuildStatus = (& curl.exe -sS --noproxy "*" -D $rebuildHeadersFile -o $rebuildBodyFile -X POST --max-time $TimeoutSeconds @rebuildProbeHeaders -w "%{http_code}" "$AdvisoryAiBaseUrl/v1/search/index/rebuild").Trim()
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "curl failed for unified search rebuild probe."
|
|
}
|
|
|
|
if ($rebuildStatus -notmatch "^\d{3}$") {
|
|
throw "Unable to parse HTTP status for unified search rebuild probe. Raw status text: '$rebuildStatus'."
|
|
}
|
|
|
|
$rebuildStatusCode = [int]$rebuildStatus
|
|
$rebuildBodyRaw = [string](Get-Content -LiteralPath $rebuildBodyFile -Raw -ErrorAction SilentlyContinue)
|
|
if ($null -eq $rebuildBodyRaw) {
|
|
$rebuildBodyRaw = ""
|
|
}
|
|
|
|
if ($rebuildStatusCode -ne 200) {
|
|
throw "Unified search rebuild probe failed: status=$rebuildStatusCode body='$rebuildBodyRaw'."
|
|
}
|
|
|
|
$rebuildPayload = $rebuildBodyRaw | ConvertFrom-Json
|
|
$chunkCount = [int]$rebuildPayload.chunkCount
|
|
if ($chunkCount -le 0) {
|
|
throw "Unified search rebuild returned no chunks (chunkCount=$chunkCount). Check KnowledgeSearch connection and adapter ingestion wiring."
|
|
}
|
|
|
|
Write-Host "[OK] Unified search ingestion rebuild: status=$rebuildStatusCode chunkCount=$chunkCount"
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $rebuildHeadersFile -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $rebuildBodyFile -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$probes = @(
|
|
@{
|
|
Name = "Platform envsettings route"
|
|
Method = "GET"
|
|
Path = "/platform/envsettings.json"
|
|
Body = $null
|
|
AllowedStatusCodes = @(200)
|
|
},
|
|
@{
|
|
Name = "Platform i18n route"
|
|
Method = "GET"
|
|
Path = "/platform/i18n/en-US.json"
|
|
Body = $null
|
|
AllowedStatusCodes = @(200)
|
|
},
|
|
@{
|
|
Name = "Unified search query route"
|
|
Method = "POST"
|
|
Path = "/api/v1/search/query"
|
|
Body = '{"q":"header route smoke","limit":5}'
|
|
AllowedStatusCodes = @(200, 400, 401, 403, 422, 429)
|
|
},
|
|
@{
|
|
Name = "Advisory search history route"
|
|
Method = "GET"
|
|
Path = "/api/v1/advisory-ai/search/history"
|
|
Body = $null
|
|
AllowedStatusCodes = @(200, 204, 400, 401, 403)
|
|
}
|
|
)
|
|
|
|
foreach ($probe in $probes) {
|
|
$result = Invoke-Probe -Name $probe.Name -Method $probe.Method -Path $probe.Path -Body $probe.Body
|
|
|
|
if ($result.StatusCode -eq 200 -and $result.ContentType -like "text/html*") {
|
|
throw "$($probe.Name) returned HTML (likely SPA fallback) for $($probe.Path)."
|
|
}
|
|
|
|
if (-not $probe.AllowedStatusCodes.Contains($result.StatusCode)) {
|
|
throw "$($probe.Name) failed for $($probe.Path): status=$($result.StatusCode), contentType='$($result.ContentType)', body='$($result.BodyPreview)'."
|
|
}
|
|
|
|
if ($probe.Path -eq "/platform/i18n/en-US.json" -and $result.ContentType -notlike "application/json*") {
|
|
throw "$($probe.Name) must return JSON. Received contentType='$($result.ContentType)'."
|
|
}
|
|
|
|
Write-Host "[OK] $($probe.Name): status=$($result.StatusCode) path=$($probe.Path)"
|
|
}
|
|
|
|
if (-not $SkipUiResponsivenessProbe) {
|
|
Invoke-UiResponsivenessProbe -BaseUrl $GatewayBaseUrl
|
|
}
|
|
|
|
Write-Host "Header search routing smoke checks passed."
|