$ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $root = Split-Path -Parent $PSScriptRoot Set-Location -LiteralPath $root $api = 'http://localhost:58080' $receiver = 'http://localhost:59210' $tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token' $tenant = '10000000-0000-0000-0000-000000000010' $key = 'alert-delivery-test-smoke-' + [Guid]::NewGuid().ToString('N') $routeKey = 'alert-delivery-route-smoke-' + [Guid]::NewGuid().ToString('N') $requestID = 'alert-delivery-test-smoke-' + [Guid]::NewGuid().ToString('N') $temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('alert-receiver-conformance-' + [Guid]::NewGuid().ToString('N')) $routesPath = Join-Path $temporaryRoot 'routes.json' $testsPath = Join-Path $temporaryRoot 'tests.json' $incidentPath = Join-Path $temporaryRoot 'incident.json' $capturePath = Join-Path $temporaryRoot 'receiver-capture.json' $reportPath = Join-Path $temporaryRoot 'receiver-report.json' $test = $null $route = $null $createdRoute = $false try { New-Item -ItemType Directory -Path $temporaryRoot | Out-Null $token = (Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{ grant_type = 'password'; client_id = 'voice-platform-web'; username = 'owner'; password = 'LocalOnly123!'; scope = 'openid' }).access_token $headers = @{Authorization = "Bearer $token"; 'X-Tenant-Id' = $tenant} $routes = (Invoke-RestMethod -Method Get -Uri "$api/api/v1/ops/alert-routing-policies" -Headers $headers).data.items $route = @($routes | Where-Object status -eq 'ACTIVE' | Select-Object -First 1)[0] if ($null -eq $route) { $routeBody = @{ category = 'TECHNICAL'; severity = 'HIGH'; receiver_group = 'local-platform-oncall'; response_minutes = 15 escalation_after_minutes = 30; escalation_receiver_group = 'local-incident-command' runbook_ref = 'runbook://local/alert-delivery-test'; change_summary = '本地告警接收器联通烟测临时路由' } | ConvertTo-Json -Compress $routeResponse = Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/alert-routing-policies" -Headers ($headers + @{'Idempotency-Key' = $routeKey; 'X-Request-Id' = $requestID}) -ContentType 'application/json' -Body $routeBody $route = $routeResponse.data $createdRoute = $true } $createHeaders = $headers.Clone() $createHeaders['Idempotency-Key'] = $key $createHeaders['X-Request-Id'] = $requestID $body = @{reason = '本地真实Worker、HMAC签名与接收器联通验证'} | ConvertTo-Json -Compress $created = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alert-routing-policies/$($route.id)/delivery-tests" -Headers $createHeaders -ContentType 'application/json' -Body $body $test = ($created.Content | ConvertFrom-Json).data if ($created.StatusCode -ne 201 -or $test.status -ne 'PENDING' -or -not $test.id -or -not $test.test_ref) { throw '联通测试创建结果不正确' } $deadline = (Get-Date).AddSeconds(15) do { Start-Sleep -Milliseconds 400 $listed = (Invoke-RestMethod -Method Get -Uri "$api/api/v1/ops/alert-delivery-tests" -Headers $headers).data.items $current = @($listed | Where-Object id -eq $test.id | Select-Object -First 1)[0] } while (($null -eq $current -or @('PENDING','PROCESSING') -contains $current.status) -and (Get-Date) -lt $deadline) if ($null -eq $current -or $current.status -ne 'DELIVERED' -or $current.attempt_count -lt 1) { throw "Worker未完成联通测试投递,当前状态=$($current.status)" } $received = (Invoke-RestMethod -Method Get -Uri "$receiver/events?test_ref=$($test.test_ref)").items if (@($received).Count -ne 1 -or $received[0].event_type -ne 'alert.webhook_test_requested' -or $received[0].data.action -ne 'TEST') { throw '验签接收器未记录唯一联通测试事件' } $incidentCount = (docker compose exec -T postgres psql -U voice -d voice_platform -tAc "SELECT count(*) FROM alert_incident WHERE title='告警接收器联通测试' OR summary LIKE '这是一条由运营工作台发起的受控测试通知%';").Trim() if ($incidentCount -ne '0') { throw '联通测试错误地创建了正式告警事件' } $replayed = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alert-routing-policies/$($route.id)/delivery-tests" -Headers $createHeaders -ContentType 'application/json' -Body $body if ($replayed.StatusCode -ne 200 -or $replayed.Headers.'Idempotency-Replayed' -ne 'true' -or ($replayed.Content | ConvertFrom-Json).data.id -ne $test.id) { throw '联通测试幂等重放不正确' } $now = [DateTime]::UtcNow $mockIncidentId = [Guid]::NewGuid().ToString() $mockReceivedAt = $now.AddMinutes(-2) $mockAcknowledgedAt = $now.AddMinutes(-1) $buildDigest = (docker image inspect data-terminal-cloud-api --format '{{.Id}}').Trim() $jsonOptions = @{Depth = 12; Compress = $false} [System.IO.File]::WriteAllText($routesPath, (@{items = @($route)} | ConvertTo-Json @jsonOptions), [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($testsPath, (@{items = @($current)} | ConvertTo-Json @jsonOptions), [System.Text.UTF8Encoding]::new($false)) $incident = @{ id = $mockIncidentId; status = 'RESOLVED'; receiver_group = $route.receiver_group first_detected_at = $now.AddMinutes(-3).ToString('o'); resolved_at = $now.ToString('o') actions = @(@{action_type = 'ACKNOWLEDGED'; created_at = $mockAcknowledgedAt.ToString('o')}) } [System.IO.File]::WriteAllText($incidentPath, ($incident | ConvertTo-Json @jsonOptions), [System.Text.UTF8Encoding]::new($false)) $capture = @{ schema_version = 'alert-receiver-capture-v1'; evidence_source = 'MOCK'; candidate_version = '0.63.0-rc.local'; build_digest = $buildDigest receiver_system_ref = 'mock-alert-receiver:local'; collected_at = $now.ToString('o') group_mappings = @( @{receiver_group = $route.receiver_group; mapped_target_refs = @('local-sink:primary'); verified_at = $now.AddMinutes(-1).ToString('o')}, @{receiver_group = $route.escalation_receiver_group; mapped_target_refs = @('local-sink:escalation'); verified_at = $now.AddMinutes(-1).ToString('o')} ) test_receipts = @(@{test_ref = $test.test_ref; event_id = $received[0].event_id; receiver_group = $received[0].data.receiver_group; delivery_count = 1; signature_verified = $true; received_at = ([DateTime]$received[0].received_at).ToUniversalTime().ToString('o')}) formal_drill = @{incident_id = $mockIncidentId; receiver_group = $route.receiver_group; delivery_count = 1; signature_verified = $true; controlled_drill = $true; received_at = $mockReceivedAt.ToString('o'); acknowledged_at = $mockAcknowledgedAt.ToString('o'); acknowledgement_ref = 'mock-ticket:local-only'} } [System.IO.File]::WriteAllText($capturePath, ($capture | ConvertTo-Json @jsonOptions), [System.Text.UTF8Encoding]::new($false)) $nativePreference = $PSNativeCommandUseErrorActionPreference $PSNativeCommandUseErrorActionPreference = $false go run ./cmd/alert-receiver-conformance -routes $routesPath -tests $testsPath -incident $incidentPath -receiver $capturePath -candidate-version '0.63.0-rc.local' -build-digest $buildDigest -output $reportPath $conformanceExit = $LASTEXITCODE $PSNativeCommandUseErrorActionPreference = $nativePreference $report = Get-Content -Raw -LiteralPath $reportPath | ConvertFrom-Json if ($conformanceExit -ne 0) { throw "接收器一致性校验失败:$((@($report.checks | Where-Object status -eq 'FAIL').code) -join ',');coverage=$($report.route_coverage | ConvertTo-Json -Compress);cloud=$($current.id)/$($current.test_ref);receipt=$($received[0].event_id)/$($received[0].test_ref)" } if ($report.result -ne 'PASS' -or $report.evidence_scope -ne 'MOCK_RECEIVER' -or -not $report.external_validation_required -or $report.active_route_count -ne 1 -or -not $report.formal_drill_verified) { throw '本地接收器一致性报告未保持模拟证据边界' } Write-Host "告警接收器联通烟测通过:路由=$($route.id),任务=$($test.id),真实Worker投递、HMAC验签、唯一回执和映射一致;报告范围=MOCK_RECEIVER,真实网关门禁仍待验。" } finally { if ($null -ne $test) { docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "BEGIN; DELETE FROM audit_event WHERE request_id='$requestID'; DELETE FROM idempotency_record WHERE idempotency_key IN ('$key','$routeKey'); DELETE FROM outbox_event WHERE public_id='$($test.id)'::uuid; COMMIT;" | Out-Null } if ($createdRoute -and $null -ne $route) { docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "DELETE FROM alert_routing_policy WHERE public_id='$($route.id)'::uuid;" | Out-Null } if (Test-Path -LiteralPath $temporaryRoot) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force } if ($null -ne $test -or $createdRoute) { Write-Host '已清理联通烟测的临时路由、审计、幂等和Outbox记录;本地接收器只保留进程内验签样本。' } }