texts fixes, search bar fixes, global menu fixes.
This commit is contained in:
284
devops/compose/scripts/header-search-smoke.ps1
Normal file
284
devops/compose/scripts/header-search-smoke.ps1
Normal file
@@ -0,0 +1,284 @@
|
||||
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."
|
||||
@@ -4,24 +4,41 @@ param(
|
||||
[string]$ComposeFile = "docker-compose.stella-ops.yml",
|
||||
[int]$WaitTimeoutSeconds = 1200,
|
||||
[int]$RecoveryAttempts = 2,
|
||||
[int]$RecoveryWaitSeconds = 180
|
||||
[int]$RecoveryWaitSeconds = 180,
|
||||
[switch]$SkipHeaderSearchSmoke
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$configPath = switch ($Mode) {
|
||||
"microservice" { "./router-gateway-local.json" }
|
||||
"reverseproxy" { "./router-gateway-local.reverseproxy.json" }
|
||||
$composeDirectory = Split-Path -Parent $PSScriptRoot
|
||||
$resolvedComposeFile = if ([System.IO.Path]::IsPathRooted($ComposeFile)) {
|
||||
$ComposeFile
|
||||
} else {
|
||||
Join-Path -Path $composeDirectory -ChildPath $ComposeFile
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $resolvedComposeFile)) {
|
||||
throw "Compose file not found: $resolvedComposeFile"
|
||||
}
|
||||
|
||||
$configFileName = switch ($Mode) {
|
||||
"microservice" { "router-gateway-local.json" }
|
||||
"reverseproxy" { "router-gateway-local.reverseproxy.json" }
|
||||
default { throw "Unsupported mode: $Mode" }
|
||||
}
|
||||
|
||||
$configPath = Join-Path -Path $composeDirectory -ChildPath $configFileName
|
||||
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||
throw "Gateway config file not found: $configPath"
|
||||
}
|
||||
|
||||
Write-Host "Redeploy mode: $Mode"
|
||||
Write-Host "Gateway config: $configPath"
|
||||
Write-Host "Compose file: $ComposeFile"
|
||||
Write-Host "Compose file: $resolvedComposeFile"
|
||||
|
||||
$env:ROUTER_GATEWAY_CONFIG = $configPath
|
||||
$env:ROUTER_GATEWAY_CONFIG = "./$configFileName"
|
||||
|
||||
function Invoke-Compose {
|
||||
param(
|
||||
@@ -30,7 +47,7 @@ function Invoke-Compose {
|
||||
[switch]$IgnoreExitCode
|
||||
)
|
||||
|
||||
& docker compose -f $ComposeFile @Args
|
||||
& docker compose --project-directory $composeDirectory -f $resolvedComposeFile @Args
|
||||
$exitCode = $LASTEXITCODE
|
||||
if (-not $IgnoreExitCode -and $exitCode -ne 0) {
|
||||
throw "docker compose $($Args -join ' ') failed with exit code $exitCode."
|
||||
@@ -55,12 +72,33 @@ function Get-ComposeServiceName {
|
||||
[string]$ContainerName
|
||||
)
|
||||
|
||||
$service = & docker inspect --format "{{ index .Config.Labels \"com.docker.compose.service\" }}" $ContainerName 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($service)) {
|
||||
$inspectJson = & docker inspect $ContainerName 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or $null -eq $inspectJson) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $service.Trim()
|
||||
try {
|
||||
$inspect = $inspectJson | ConvertFrom-Json
|
||||
if ($null -eq $inspect) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$first = if ($inspect -is [System.Array]) { $inspect[0] } else { $inspect }
|
||||
$labels = $first.Config.Labels
|
||||
if ($null -eq $labels) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$service = $labels."com.docker.compose.service"
|
||||
if ([string]::IsNullOrWhiteSpace($service)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $service.Trim()
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForContainerHealth {
|
||||
@@ -126,4 +164,14 @@ if ($remainingUnhealthy.Count -gt 0) {
|
||||
throw "Redeploy completed with unresolved unhealthy containers: $($remainingUnhealthy -join ', ')"
|
||||
}
|
||||
|
||||
if (-not $SkipHeaderSearchSmoke) {
|
||||
$headerSearchSmokeScript = Join-Path -Path $PSScriptRoot -ChildPath "header-search-smoke.ps1"
|
||||
if (-not (Test-Path -LiteralPath $headerSearchSmokeScript)) {
|
||||
throw "Header search smoke script not found: $headerSearchSmokeScript"
|
||||
}
|
||||
|
||||
Write-Host "Running header search route smoke checks..."
|
||||
& $headerSearchSmokeScript
|
||||
}
|
||||
|
||||
Write-Host "Redeploy complete for mode '$Mode'."
|
||||
|
||||
Reference in New Issue
Block a user