Public source validation / validate (push) Failing after 3m8s
176 lines
10 KiB
PowerShell
176 lines
10 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
|
Set-Location (Split-Path -Parent $PSScriptRoot)
|
|
|
|
function Get-FreeLoopbackPort {
|
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
|
$listener.Start()
|
|
try { return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() }
|
|
}
|
|
|
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw 'Docker is required for the isolated integration smoke.' }
|
|
$goBin = Join-Path $env:LOCALAPPDATA 'ITWorx-Pulse\toolchains\go1.26.6\bin'
|
|
if (-not (Get-Command go -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $goBin 'go.exe'))) { $env:PATH = $goBin + ';' + $env:PATH }
|
|
|
|
$project = 'itworx-pulse-smoke-' + $PID
|
|
$webPort = Get-FreeLoopbackPort
|
|
$apiPort = Get-FreeLoopbackPort
|
|
$dbPort = Get-FreeLoopbackPort
|
|
$baseURL = "http://127.0.0.1:$webPort"
|
|
$compose = @('-p', $project, '-f', 'deploy/compose.yaml', '-f', 'deploy/compose.dev.yaml', '-f', 'deploy/compose.smoke.yaml')
|
|
$succeeded = $false
|
|
|
|
$env:PULSE_POSTGRES_PASSWORD = 'smoke-process-only'
|
|
$env:PULSE_DATABASE_URL = 'postgres://pulse:smoke-process-only@pulse-postgres:5432/pulse?sslmode=disable'
|
|
$env:PULSE_ENV = 'development'
|
|
$env:PULSE_AUTH_MODE = 'mock'
|
|
$env:PULSE_SESSION_IDLE_TTL = '30s'
|
|
$env:PULSE_SESSION_ABSOLUTE_TTL = '3m'
|
|
$env:PULSE_DEV_WEB_PORT = [string]$webPort
|
|
$env:PULSE_DEV_API_PORT = [string]$apiPort
|
|
$env:PULSE_DEV_DB_PORT = [string]$dbPort
|
|
$env:PULSE_SMOKE_WEBHOOK_TOKEN = 'smoke-runtime-only'
|
|
|
|
try {
|
|
Write-Host "== isolated stack ($project) ==" -ForegroundColor Cyan
|
|
docker compose @compose config --quiet
|
|
docker compose @compose build --quiet
|
|
# Migrate and seed before the worker starts. Discovery claims one-minute
|
|
# windows, so restarting a worker after an initial empty run is inherently
|
|
# timing-dependent and can correctly retain the first claim until the next
|
|
# window.
|
|
docker compose @compose up -d --wait --wait-timeout 240 pulse-postgres
|
|
docker compose @compose run --rm pulse-migrate
|
|
|
|
Write-Host '== agent snapshot -> discovery -> inventory ==' -ForegroundColor Cyan
|
|
@'
|
|
INSERT INTO data_sources (id,type,name,configuration_ref)
|
|
VALUES ('b1011111-1111-4111-8111-111111111111','unraid','Smoke container agent','smoke/container-agent')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
INSERT INTO agent_snapshots (agent_id,capability,observed_at,received_at,payload)
|
|
VALUES (
|
|
'pulse-smoke-agent',
|
|
'containers',
|
|
now(),
|
|
now(),
|
|
jsonb_build_object(
|
|
'Source', jsonb_build_object('id','pulse-smoke-agent','type','agent'),
|
|
'Containers', jsonb_build_array(jsonb_build_object(
|
|
'ID','runtime-smoke-1',
|
|
'Name','smoke-api',
|
|
'Image','itworx/pulse-smoke',
|
|
'ImageDigest','sha256:smoke',
|
|
'State','running',
|
|
'Health','healthy',
|
|
'Project','smoke',
|
|
'Labels',jsonb_build_object('com.docker.compose.project','smoke','com.docker.compose.service','api')
|
|
)),
|
|
'ObservedAt', to_jsonb(now()),
|
|
'ReceivedAt', to_jsonb(now())
|
|
)
|
|
)
|
|
ON CONFLICT (agent_id,capability) DO UPDATE
|
|
SET observed_at=EXCLUDED.observed_at,received_at=EXCLUDED.received_at,payload=EXCLUDED.payload;
|
|
INSERT INTO agent_snapshots (agent_id,capability,observed_at,received_at,payload)
|
|
VALUES
|
|
('pulse-smoke-agent','array',now(),now(),jsonb_build_object(
|
|
'source',jsonb_build_object('id','pulse-smoke-agent','type','fixture'),
|
|
'state','operational','parity',jsonb_build_object('present',true,'state','idle'),
|
|
'members',jsonb_build_array(
|
|
jsonb_build_object('id','disk1','name','Disk 1','role','data','state','online','capacityBytes',1000000),
|
|
jsonb_build_object('id','parity','name','Parity','role','parity','state','online','capacityBytes',1000000)
|
|
),'observedAt',to_jsonb(now()),'receivedAt',to_jsonb(now())
|
|
)),
|
|
('pulse-smoke-agent','disks',now(),now(),jsonb_build_object(
|
|
'source',jsonb_build_object('id','pulse-smoke-agent','type','fixture'),
|
|
'disks',jsonb_build_array(jsonb_build_object('id','disk1','name','Disk 1','role','data','state','online','sizeBytes',1000000,'usedBytes',400000)),
|
|
'observedAt',to_jsonb(now()),'receivedAt',to_jsonb(now())
|
|
)),
|
|
('pulse-smoke-agent','pools',now(),now(),jsonb_build_object(
|
|
'source',jsonb_build_object('id','pulse-smoke-agent','type','fixture'),
|
|
'pools',jsonb_build_array(jsonb_build_object('id','cache','name','Cache','filesystem','btrfs','state','healthy','usableBytes',1000000,'usedBytes',250000)),
|
|
'observedAt',to_jsonb(now()),'receivedAt',to_jsonb(now())
|
|
)),
|
|
('pulse-smoke-agent','shares',now(),now(),jsonb_build_object(
|
|
'source',jsonb_build_object('id','pulse-smoke-agent','type','fixture'),
|
|
'shares',jsonb_build_array(jsonb_build_object('id','share-media','name','Media','usedBytes',250000,'sizeObservedAt',to_jsonb(now()),'sizeState','cached')),
|
|
'observedAt',to_jsonb(now()),'receivedAt',to_jsonb(now())
|
|
))
|
|
ON CONFLICT (agent_id,capability) DO UPDATE
|
|
SET observed_at=EXCLUDED.observed_at,received_at=EXCLUDED.received_at,payload=EXCLUDED.payload;
|
|
'@ | docker compose @compose exec -T pulse-postgres psql -v ON_ERROR_STOP=1 -U pulse -d pulse | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'failed to seed bounded container agent snapshot' }
|
|
docker compose @compose up -d --wait --wait-timeout 240
|
|
$inventoryReady = $false
|
|
for ($attempt = 0; $attempt -lt 30; $attempt++) {
|
|
Start-Sleep -Seconds 2
|
|
$inventoryRows = docker compose @compose exec -T pulse-postgres psql -U pulse -d pulse -Atc "SELECT count(*) FROM entities WHERE canonical_name='smoke/api';"
|
|
if ($LASTEXITCODE -eq 0 -and [int]($inventoryRows | Select-Object -Last 1) -eq 1) { $inventoryReady = $true; break }
|
|
}
|
|
if (-not $inventoryReady) { throw 'worker did not reconcile the fresh agent container snapshot' }
|
|
$identityRows = docker compose @compose exec -T pulse-postgres psql -U pulse -d pulse -Atc "SELECT (SELECT count(*) FROM entity_aliases WHERE source_id='b1011111-1111-4111-8111-111111111111' AND external_type='application-project' AND external_id='smoke')::text || ',' || (SELECT count(*) FROM entity_aliases WHERE source_id='b1011111-1111-4111-8111-111111111111' AND external_type='container-service' AND external_id='smoke/api')::text || ',' || (SELECT count(*) FROM container_aliases WHERE source_id='b1011111-1111-4111-8111-111111111111')::text;"
|
|
if (($identityRows | Select-Object -Last 1) -ne '1,1,1') { throw "discovery identity is not idempotent: $identityRows" }
|
|
|
|
$session = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
Invoke-RestMethod -Uri "$baseURL/auth/test-login" -WebSession $session | Out-Null
|
|
$rule = @{
|
|
schemaVersion = 1
|
|
id = 'f1011111-1111-4111-8111-111111111111'
|
|
name = 'Integratiesmoke CPU'
|
|
enabled = $true
|
|
severity = 'critical'
|
|
scope = @{ serverId = 'smoke-host' }
|
|
condition = @{ inputType = 'metric'; metric = 'host.cpu.utilization'; operator = '>='; threshold = 1; aggregation = 'avg'; windowSeconds = 60 }
|
|
evaluationIntervalSeconds = 5
|
|
pendingSeconds = 0
|
|
resolveSeconds = 0
|
|
cooldownSeconds = 60
|
|
unknownBehavior = 'become-unknown'
|
|
groupBy = @()
|
|
suppressWhen = @()
|
|
message = @{ titleKey = 'smoke.cpu.title'; bodyKey = 'smoke.cpu.body' }
|
|
} | ConvertTo-Json -Depth 8 -Compress
|
|
Invoke-RestMethod -Method Post -Uri "$baseURL/api/v1/alert-rules" -WebSession $session -ContentType 'application/json' -Body $rule | Out-Null
|
|
|
|
Write-Host '== source -> alert -> webhook ==' -ForegroundColor Cyan
|
|
$fixtureStatus = $null
|
|
for ($attempt = 0; $attempt -lt 45; $attempt++) {
|
|
Start-Sleep -Seconds 2
|
|
$fixtureJSON = docker compose @compose exec -T pulse-smoke-fixture /pulse-integration-fixture get http://127.0.0.1:9090/smoke/status
|
|
if ($LASTEXITCODE -ne 0) { continue }
|
|
$fixtureStatus = $fixtureJSON | ConvertFrom-Json
|
|
if ($fixtureStatus.prometheusRequests -gt 0 -and $fixtureStatus.events.firing -gt 0) { break }
|
|
}
|
|
# One custom smoke rule plus the two implementation-owned aggregate metric
|
|
# defaults must all reach the approved Prometheus fixture. This guards the
|
|
# alert-scope/catalog contract, not merely a single happy-path query.
|
|
if ($null -eq $fixtureStatus -or $fixtureStatus.prometheusRequests -lt 3 -or $fixtureStatus.events.firing -lt 1) {
|
|
throw "full alert chain did not complete: $($fixtureStatus | ConvertTo-Json -Compress)"
|
|
}
|
|
$workerLogs = docker compose @compose logs --no-color pulse-worker 2>$null
|
|
if ($workerLogs -match 'alert metric query failed') { throw 'implementation-owned alert rule failed its bounded metric query' }
|
|
if ($fixtureStatus.uniqueDeliveryKeys -gt $fixtureStatus.webhookRequests) { throw 'receiver observed inconsistent idempotency keys' }
|
|
|
|
$snapshotRows = docker compose @compose exec -T pulse-postgres psql -U pulse -d pulse -Atc "SELECT count(*) FROM agent_snapshots WHERE agent_id='pulse-smoke-agent' AND capability IN ('host','processes');"
|
|
if ([int]($snapshotRows | Select-Object -Last 1) -lt 2) { throw "agent snapshots missing: $snapshotRows" }
|
|
$deliveredRows = docker compose @compose exec -T pulse-postgres psql -U pulse -d pulse -Atc "SELECT count(*) FROM notification_outbox WHERE event_type='firing' AND status='delivered';"
|
|
if ([int]($deliveredRows | Select-Object -Last 1) -lt 1) { throw "delivered firing audit missing: $deliveredRows" }
|
|
|
|
Write-Host '== real browser -> web proxy -> API -> PostgreSQL ==' -ForegroundColor Cyan
|
|
$env:PULSE_E2E_REAL_BASE_URL = $baseURL
|
|
pnpm --filter @itworx/pulse-web exec playwright test tests/e2e/real-stack.spec.ts --project=desktop-chromium
|
|
if ($LASTEXITCODE -ne 0) { throw 'real-stack Playwright gate failed' }
|
|
Write-Host ("INTEGRATION SMOKE: PASS sourceQueries={0} webhookAttempts={1} uniqueDeliveries={2}" -f $fixtureStatus.prometheusRequests, $fixtureStatus.webhookRequests, $fixtureStatus.uniqueDeliveryKeys) -ForegroundColor Green
|
|
$succeeded = $true
|
|
} finally {
|
|
if (-not $succeeded) {
|
|
docker compose @compose ps 2>$null
|
|
docker compose @compose logs --no-color --tail 120 2>$null
|
|
}
|
|
# The project name is unique to this process. `-v` removes only the empty or
|
|
# smoke-populated volume created above; no pre-existing Docker object can
|
|
# match or be referenced by this compose project.
|
|
docker compose @compose down --volumes --remove-orphans 2>$null
|
|
Remove-Item Env:PULSE_POSTGRES_PASSWORD,Env:PULSE_DATABASE_URL,Env:PULSE_ENV,Env:PULSE_AUTH_MODE,Env:PULSE_SESSION_IDLE_TTL,Env:PULSE_SESSION_ABSOLUTE_TTL,Env:PULSE_DEV_WEB_PORT,Env:PULSE_DEV_API_PORT,Env:PULSE_DEV_DB_PORT,Env:PULSE_SMOKE_WEBHOOK_TOKEN,Env:PULSE_E2E_REAL_BASE_URL -ErrorAction SilentlyContinue
|
|
}
|