Files
git.stella-ops.org/devops/compose/scripts/header-search-smoke.ps1

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."