build: add server-rs smoke scripts
This commit is contained in:
@@ -121,12 +121,14 @@
|
|||||||
交付物:[../server-rs/scripts/test.ps1](../server-rs/scripts/test.ps1)、[../server-rs/scripts/test.sh](../server-rs/scripts/test.sh)
|
交付物:[../server-rs/scripts/test.ps1](../server-rs/scripts/test.ps1)、[../server-rs/scripts/test.sh](../server-rs/scripts/test.sh)
|
||||||
- [x] 新增 lint / fmt / clippy / check 脚本
|
- [x] 新增 lint / fmt / clippy / check 脚本
|
||||||
交付物:[../server-rs/scripts/check.ps1](../server-rs/scripts/check.ps1)、[../server-rs/scripts/check.sh](../server-rs/scripts/check.sh)
|
交付物:[../server-rs/scripts/check.ps1](../server-rs/scripts/check.ps1)、[../server-rs/scripts/check.sh](../server-rs/scripts/check.sh)
|
||||||
- [ ] 新增 smoke 脚本
|
- [x] 新增 smoke 脚本
|
||||||
|
交付物:[../server-rs/scripts/smoke.ps1](../server-rs/scripts/smoke.ps1)、[../server-rs/scripts/smoke.sh](../server-rs/scripts/smoke.sh)
|
||||||
- [ ] 新增 SpacetimeDB 本地开发脚本
|
- [ ] 新增 SpacetimeDB 本地开发脚本
|
||||||
|
|
||||||
### 阶段验收
|
### 阶段验收
|
||||||
|
|
||||||
- [ ] Axum 服务可独立启动
|
- [x] Axum 服务可独立启动
|
||||||
|
证据:`./server-rs/scripts/smoke.ps1` 已通过,覆盖临时启动 `api-server`、等待 `/healthz` 就绪并验证 raw / envelope 协议。
|
||||||
- [x] `/healthz` 返回与当前工程兼容
|
- [x] `/healthz` 返回与当前工程兼容
|
||||||
- [x] 基础 response envelope 与 request id 行为稳定
|
- [x] 基础 response envelope 与 request id 行为稳定
|
||||||
证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 envelope 协商与 `/healthz` 头部回写。
|
证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 envelope 协商与 `/healthz` 头部回写。
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ server-rs/
|
|||||||
├─ dev.sh / dev.ps1
|
├─ dev.sh / dev.ps1
|
||||||
├─ check.sh / check.ps1
|
├─ check.sh / check.ps1
|
||||||
├─ spacetime-publish.sh
|
├─ spacetime-publish.sh
|
||||||
└─ smoke.sh
|
└─ smoke.sh / smoke.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
目录职责约束:
|
目录职责约束:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
## 2. 当前阶段说明
|
## 2. 当前阶段说明
|
||||||
|
|
||||||
当前目录已经完成以下三十项初始化:
|
当前目录已经完成以下三十二项初始化:
|
||||||
|
|
||||||
1. 为新后端预留正式目录并把路径固定到仓库结构中。
|
1. 为新后端预留正式目录并把路径固定到仓库结构中。
|
||||||
2. 创建虚拟 workspace `Cargo.toml`,后续 package 会逐项挂入。
|
2. 创建虚拟 workspace `Cargo.toml`,后续 package 会逐项挂入。
|
||||||
@@ -46,11 +46,12 @@
|
|||||||
28. 创建 `scripts/test.sh`,固定 Unix-like 本地测试入口。
|
28. 创建 `scripts/test.sh`,固定 Unix-like 本地测试入口。
|
||||||
29. 创建 `scripts/check.ps1`,固定 Windows 本地统一检查入口。
|
29. 创建 `scripts/check.ps1`,固定 Windows 本地统一检查入口。
|
||||||
30. 创建 `scripts/check.sh`,固定 Unix-like 本地统一检查入口。
|
30. 创建 `scripts/check.sh`,固定 Unix-like 本地统一检查入口。
|
||||||
|
31. 创建 `scripts/smoke.ps1`,固定 Windows 本地冒烟验证入口。
|
||||||
|
32. 创建 `scripts/smoke.sh`,固定 Unix-like 本地冒烟验证入口。
|
||||||
|
|
||||||
后续任务会继续在本目录内按顺序补齐:
|
后续任务会继续在本目录内按顺序补齐:
|
||||||
|
|
||||||
1. smoke 脚本
|
1. SpacetimeDB 本地开发脚本
|
||||||
2. SpacetimeDB 本地开发脚本
|
|
||||||
|
|
||||||
## 3. 已冻结边界
|
## 3. 已冻结边界
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
6. 由 `../../scripts/dev.ps1` 与 `../../scripts/dev.sh` 驱动的本地开发启动链路
|
6. 由 `../../scripts/dev.ps1` 与 `../../scripts/dev.sh` 驱动的本地开发启动链路
|
||||||
7. 由 `../../scripts/test.ps1` 与 `../../scripts/test.sh` 驱动的本地测试链路
|
7. 由 `../../scripts/test.ps1` 与 `../../scripts/test.sh` 驱动的本地测试链路
|
||||||
8. 由 `../../scripts/check.ps1` 与 `../../scripts/check.sh` 驱动的本地统一检查链路
|
8. 由 `../../scripts/check.ps1` 与 `../../scripts/check.sh` 驱动的本地统一检查链路
|
||||||
|
9. 由 `../../scripts/smoke.ps1` 与 `../../scripts/smoke.sh` 驱动的本地启动与协议冒烟链路
|
||||||
|
|
||||||
## 2. 当前阶段说明
|
## 2. 当前阶段说明
|
||||||
|
|
||||||
@@ -81,6 +82,12 @@
|
|||||||
3. 当只需聚焦单个 package 时,可通过 `-Package` 或 `SERVER_RS_CHECK_PACKAGE` 收窄 `clippy / check / test` 目标。
|
3. 当只需聚焦单个 package 时,可通过 `-Package` 或 `SERVER_RS_CHECK_PACKAGE` 收窄 `clippy / check / test` 目标。
|
||||||
4. `cargo fmt --all --check` 仍固定覆盖整个 workspace,避免多 package 下格式基线漂移。
|
4. `cargo fmt --all --check` 仍固定覆盖整个 workspace,避免多 package 下格式基线漂移。
|
||||||
|
|
||||||
|
当前本地 smoke 链路约定:
|
||||||
|
|
||||||
|
1. `../../scripts/smoke.ps1` 与 `../../scripts/smoke.sh` 会先构建 `api-server`,再拉起临时本地进程完成冒烟验证。
|
||||||
|
2. smoke 当前固定校验 `/healthz` 的 raw 响应、envelope 响应以及 `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 头。
|
||||||
|
3. smoke 通过后,可作为“Axum 服务可独立启动且基础 contract 可联通”的本地自动化证据。
|
||||||
|
|
||||||
## 3. 边界约束
|
## 3. 边界约束
|
||||||
|
|
||||||
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
||||||
|
|||||||
239
server-rs/scripts/smoke.ps1
Normal file
239
server-rs/scripts/smoke.ps1
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Alias("h")]
|
||||||
|
[switch]$Help,
|
||||||
|
[string]$ApiHost = "127.0.0.1",
|
||||||
|
[int]$Port = 3101,
|
||||||
|
[string]$Log = "warn,tower_http=warn",
|
||||||
|
[int]$StartupTimeoutSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Write-Usage {
|
||||||
|
@(
|
||||||
|
'Usage:'
|
||||||
|
' ./server-rs/scripts/smoke.ps1'
|
||||||
|
' ./server-rs/scripts/smoke.ps1 -ApiHost 127.0.0.1 -Port 3201 -Log "info,tower_http=info"'
|
||||||
|
''
|
||||||
|
'Notes:'
|
||||||
|
' 1. Build api-server and start an ephemeral local process'
|
||||||
|
' 2. Verify /healthz raw payload, response headers, and envelope contract'
|
||||||
|
' 3. Stop the temporary process automatically after the smoke checks finish'
|
||||||
|
) -join [Environment]::NewLine
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Condition {
|
||||||
|
param(
|
||||||
|
[bool]$Condition,
|
||||||
|
[string]$Message
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Condition) {
|
||||||
|
throw $Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-InheritedEnvVar {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Value,
|
||||||
|
[hashtable]$Snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Snapshot.ContainsKey($Name)) {
|
||||||
|
$Snapshot[$Name] = [Environment]::GetEnvironmentVariable($Name, "Process")
|
||||||
|
}
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable($Name, $Value, "Process")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-InheritedEnvVars {
|
||||||
|
param([hashtable]$Snapshot)
|
||||||
|
|
||||||
|
foreach ($entry in $Snapshot.GetEnumerator()) {
|
||||||
|
[Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HeaderValue {
|
||||||
|
param(
|
||||||
|
$Headers,
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Headers) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Headers[$Name]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-ForHealthz {
|
||||||
|
param(
|
||||||
|
[string]$Uri,
|
||||||
|
[int]$TimeoutSeconds,
|
||||||
|
$Process
|
||||||
|
)
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||||
|
$lastError = $null
|
||||||
|
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
if ($Process.HasExited) {
|
||||||
|
throw "api-server exited before /healthz became ready."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 2
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastError = "Unexpected status code $($response.StatusCode)"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$lastError = $_.Exception.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 300
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Timed out waiting for /healthz readiness. Last error: $lastError"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-ProcessLogs {
|
||||||
|
param(
|
||||||
|
[string]$StdoutLog,
|
||||||
|
[string]$StderrLog
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Test-Path $StdoutLog) {
|
||||||
|
Write-Host "[server-rs:smoke] stdout:"
|
||||||
|
Get-Content -Path $StdoutLog
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $StderrLog) {
|
||||||
|
Write-Host "[server-rs:smoke] stderr:"
|
||||||
|
Get-Content -Path $StderrLog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Help) {
|
||||||
|
Write-Usage
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$serverRsDir = Split-Path -Parent $scriptDir
|
||||||
|
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
|
||||||
|
$binaryPath = Join-Path $serverRsDir "target\debug\api-server.exe"
|
||||||
|
$baseUrl = "http://$ApiHost`:$Port"
|
||||||
|
$healthzUrl = "$baseUrl/healthz"
|
||||||
|
$runId = [Guid]::NewGuid().ToString("N")
|
||||||
|
$stdoutLog = Join-Path $env:TEMP "genarrative-server-rs-smoke-$runId.stdout.log"
|
||||||
|
$stderrLog = Join-Path $env:TEMP "genarrative-server-rs-smoke-$runId.stderr.log"
|
||||||
|
$envSnapshot = @{}
|
||||||
|
$serverProcess = $null
|
||||||
|
$shouldKeepLogs = $false
|
||||||
|
|
||||||
|
if (-not (Test-Path $manifestPath)) {
|
||||||
|
throw "Missing server-rs/Cargo.toml, cannot start smoke script."
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $serverRsDir
|
||||||
|
try {
|
||||||
|
Write-Host "[server-rs:smoke] step: cargo build -p api-server"
|
||||||
|
cargo build -p api-server --manifest-path $manifestPath
|
||||||
|
|
||||||
|
Assert-Condition (Test-Path $binaryPath) "Missing api-server binary at $binaryPath after cargo build."
|
||||||
|
|
||||||
|
Set-InheritedEnvVar -Name "GENARRATIVE_API_HOST" -Value $ApiHost -Snapshot $envSnapshot
|
||||||
|
Set-InheritedEnvVar -Name "GENARRATIVE_API_PORT" -Value "$Port" -Snapshot $envSnapshot
|
||||||
|
Set-InheritedEnvVar -Name "GENARRATIVE_API_LOG" -Value $Log -Snapshot $envSnapshot
|
||||||
|
|
||||||
|
Write-Host "[server-rs:smoke] step: start api-server binary"
|
||||||
|
$serverProcess = Start-Process `
|
||||||
|
-FilePath $binaryPath `
|
||||||
|
-WorkingDirectory $serverRsDir `
|
||||||
|
-PassThru `
|
||||||
|
-RedirectStandardOutput $stdoutLog `
|
||||||
|
-RedirectStandardError $stderrLog
|
||||||
|
|
||||||
|
Restore-InheritedEnvVars -Snapshot $envSnapshot
|
||||||
|
|
||||||
|
Write-Host "[server-rs:smoke] step: wait for /healthz readiness"
|
||||||
|
Wait-ForHealthz -Uri $healthzUrl -TimeoutSeconds $StartupTimeoutSeconds -Process $serverProcess
|
||||||
|
|
||||||
|
$rawRequestId = "smoke-healthz-raw"
|
||||||
|
Write-Host "[server-rs:smoke] step: verify raw /healthz contract"
|
||||||
|
$rawResponse = Invoke-WebRequest `
|
||||||
|
-Uri $healthzUrl `
|
||||||
|
-Headers @{ "x-request-id" = $rawRequestId } `
|
||||||
|
-UseBasicParsing `
|
||||||
|
-TimeoutSec 5
|
||||||
|
$rawBody = $rawResponse.Content | ConvertFrom-Json
|
||||||
|
|
||||||
|
Assert-Condition ($rawResponse.StatusCode -eq 200) "Raw /healthz did not return HTTP 200."
|
||||||
|
Assert-Condition ($rawBody.ok -eq $true) "Raw /healthz body is missing ok=true."
|
||||||
|
Assert-Condition ($rawBody.service -eq "genarrative-node-server") "Raw /healthz body returned an unexpected service name."
|
||||||
|
|
||||||
|
$apiVersionHeader = Get-HeaderValue -Headers $rawResponse.Headers -Name "x-api-version"
|
||||||
|
$routeVersionHeader = Get-HeaderValue -Headers $rawResponse.Headers -Name "x-route-version"
|
||||||
|
$requestIdHeader = Get-HeaderValue -Headers $rawResponse.Headers -Name "x-request-id"
|
||||||
|
$responseTimeHeader = Get-HeaderValue -Headers $rawResponse.Headers -Name "x-response-time-ms"
|
||||||
|
$responseTimeValue = 0
|
||||||
|
|
||||||
|
Assert-Condition ($requestIdHeader -eq $rawRequestId) "Raw /healthz did not echo x-request-id."
|
||||||
|
Assert-Condition (-not [string]::IsNullOrWhiteSpace($apiVersionHeader)) "Raw /healthz is missing x-api-version."
|
||||||
|
Assert-Condition ($routeVersionHeader -eq $apiVersionHeader) "Raw /healthz x-route-version is not aligned with x-api-version."
|
||||||
|
Assert-Condition (
|
||||||
|
[int]::TryParse(($responseTimeHeader | ForEach-Object { $_ }), [ref]$responseTimeValue)
|
||||||
|
) "Raw /healthz x-response-time-ms is not a valid integer."
|
||||||
|
Assert-Condition ($responseTimeValue -ge 0) "Raw /healthz x-response-time-ms must be >= 0."
|
||||||
|
|
||||||
|
$envelopeRequestId = "smoke-healthz-envelope"
|
||||||
|
Write-Host "[server-rs:smoke] step: verify envelope /healthz contract"
|
||||||
|
$envelopeResponse = Invoke-WebRequest `
|
||||||
|
-Uri $healthzUrl `
|
||||||
|
-Headers @{
|
||||||
|
"x-request-id" = $envelopeRequestId
|
||||||
|
"x-genarrative-response-envelope" = "1"
|
||||||
|
} `
|
||||||
|
-UseBasicParsing `
|
||||||
|
-TimeoutSec 5
|
||||||
|
$envelopeBody = $envelopeResponse.Content | ConvertFrom-Json
|
||||||
|
|
||||||
|
Assert-Condition ($envelopeResponse.StatusCode -eq 200) "Envelope /healthz did not return HTTP 200."
|
||||||
|
Assert-Condition ($envelopeBody.ok -eq $true) "Envelope /healthz body is missing ok=true."
|
||||||
|
Assert-Condition ($null -eq $envelopeBody.error) "Envelope /healthz should return error=null."
|
||||||
|
Assert-Condition ($envelopeBody.data.ok -eq $true) "Envelope /healthz data.ok should be true."
|
||||||
|
Assert-Condition ($envelopeBody.data.service -eq "genarrative-node-server") "Envelope /healthz returned an unexpected service name."
|
||||||
|
Assert-Condition ($envelopeBody.meta.apiVersion -eq $apiVersionHeader) "Envelope /healthz meta.apiVersion does not match x-api-version."
|
||||||
|
Assert-Condition ($envelopeBody.meta.requestId -eq $envelopeRequestId) "Envelope /healthz meta.requestId did not echo x-request-id."
|
||||||
|
Assert-Condition ($envelopeBody.meta.routeVersion -eq $routeVersionHeader) "Envelope /healthz meta.routeVersion does not match x-route-version."
|
||||||
|
Assert-Condition ($envelopeBody.meta.operation -eq "GET /healthz") "Envelope /healthz meta.operation is unexpected."
|
||||||
|
Assert-Condition ($envelopeBody.meta.latencyMs -ge 0) "Envelope /healthz meta.latencyMs must be >= 0."
|
||||||
|
|
||||||
|
Write-Host "[server-rs:smoke] all checks passed"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$shouldKeepLogs = $true
|
||||||
|
Write-ProcessLogs -StdoutLog $stdoutLog -StderrLog $stderrLog
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Restore-InheritedEnvVars -Snapshot $envSnapshot
|
||||||
|
|
||||||
|
if ($null -ne $serverProcess -and -not $serverProcess.HasExited) {
|
||||||
|
Stop-Process -Id $serverProcess.Id -Force
|
||||||
|
$serverProcess.WaitForExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
Pop-Location
|
||||||
|
|
||||||
|
if (-not $shouldKeepLogs) {
|
||||||
|
Remove-Item -LiteralPath $stdoutLog -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -LiteralPath $stderrLog -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
188
server-rs/scripts/smoke.sh
Normal file
188
server-rs/scripts/smoke.sh
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 先编译并拉起临时 api-server 进程,再校验 /healthz 的裸响应、envelope 和关键响应头契约。
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法:
|
||||||
|
./server-rs/scripts/smoke.sh
|
||||||
|
GENARRATIVE_SMOKE_PORT=3201 ./server-rs/scripts/smoke.sh
|
||||||
|
|
||||||
|
说明:
|
||||||
|
1. 先执行 `cargo build -p api-server`
|
||||||
|
2. 启动一个临时 `api-server` 进程并等待 `/healthz` 就绪
|
||||||
|
3. 校验 raw `/healthz`、envelope `/healthz` 以及 `x-request-id / x-api-version / x-route-version / x-response-time-ms`
|
||||||
|
4. 冒烟结束后自动回收临时进程
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SERVER_RS_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
MANIFEST_PATH="${SERVER_RS_DIR}/Cargo.toml"
|
||||||
|
SERVER_BIN="${SERVER_RS_DIR}/target/debug/api-server"
|
||||||
|
API_HOST="${GENARRATIVE_SMOKE_HOST:-127.0.0.1}"
|
||||||
|
API_PORT="${GENARRATIVE_SMOKE_PORT:-3101}"
|
||||||
|
API_LOG="${GENARRATIVE_SMOKE_LOG:-warn,tower_http=warn}"
|
||||||
|
STARTUP_TIMEOUT_SECONDS="${GENARRATIVE_SMOKE_TIMEOUT_SECONDS:-30}"
|
||||||
|
BASE_URL="http://${API_HOST}:${API_PORT}"
|
||||||
|
HEALTHZ_URL="${BASE_URL}/healthz"
|
||||||
|
RUN_ID="$(date +%s)-$$"
|
||||||
|
STDOUT_LOG="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.stdout.log"
|
||||||
|
STDERR_LOG="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.stderr.log"
|
||||||
|
KEEP_LOGS=0
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
if [[ "${haystack}" != *"${needle}"* ]]; then
|
||||||
|
echo "[server-rs:smoke] ${message}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_logs() {
|
||||||
|
if [[ -f "${STDOUT_LOG}" ]]; then
|
||||||
|
echo "[server-rs:smoke] stdout:"
|
||||||
|
cat "${STDOUT_LOG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${STDERR_LOG}" ]]; then
|
||||||
|
echo "[server-rs:smoke] stderr:"
|
||||||
|
cat "${STDERR_LOG}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${SERVER_PID}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then
|
||||||
|
kill "${SERVER_PID}" 2>/dev/null || true
|
||||||
|
wait "${SERVER_PID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${KEEP_LOGS}" -eq 0 ]]; then
|
||||||
|
rm -f "${STDOUT_LOG}" "${STDERR_LOG}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||||||
|
echo "[server-rs:smoke] 未找到 ${MANIFEST_PATH},无法启动冒烟脚本。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${SERVER_RS_DIR}"
|
||||||
|
|
||||||
|
echo "[server-rs:smoke] 步骤: cargo build -p api-server"
|
||||||
|
cargo build -p api-server --manifest-path "${MANIFEST_PATH}"
|
||||||
|
|
||||||
|
if [[ ! -x "${SERVER_BIN}" ]]; then
|
||||||
|
echo "[server-rs:smoke] 未找到 ${SERVER_BIN},构建结果异常。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[server-rs:smoke] 步骤: start api-server binary"
|
||||||
|
GENARRATIVE_API_HOST="${API_HOST}" \
|
||||||
|
GENARRATIVE_API_PORT="${API_PORT}" \
|
||||||
|
GENARRATIVE_API_LOG="${API_LOG}" \
|
||||||
|
"${SERVER_BIN}" >"${STDOUT_LOG}" 2>"${STDERR_LOG}" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
echo "[server-rs:smoke] 步骤: wait for /healthz readiness"
|
||||||
|
READY=0
|
||||||
|
for ((i = 0; i < STARTUP_TIMEOUT_SECONDS * 10; i++)); do
|
||||||
|
if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
|
||||||
|
KEEP_LOGS=1
|
||||||
|
print_logs
|
||||||
|
echo "[server-rs:smoke] api-server exited before /healthz became ready." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -fsS "${HEALTHZ_URL}" >/dev/null 2>&1; then
|
||||||
|
READY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${READY}" -ne 1 ]]; then
|
||||||
|
KEEP_LOGS=1
|
||||||
|
print_logs
|
||||||
|
echo "[server-rs:smoke] /healthz readiness timed out." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RAW_BODY_FILE="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.raw.body.json"
|
||||||
|
RAW_HEADER_FILE="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.raw.headers.txt"
|
||||||
|
ENVELOPE_BODY_FILE="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.envelope.body.json"
|
||||||
|
ENVELOPE_HEADER_FILE="${TMPDIR:-/tmp}/genarrative-server-rs-smoke-${RUN_ID}.envelope.headers.txt"
|
||||||
|
|
||||||
|
trap 'rm -f "${RAW_BODY_FILE}" "${RAW_HEADER_FILE}" "${ENVELOPE_BODY_FILE}" "${ENVELOPE_HEADER_FILE}"; cleanup' EXIT
|
||||||
|
|
||||||
|
RAW_REQUEST_ID="smoke-healthz-raw"
|
||||||
|
echo "[server-rs:smoke] 步骤: verify raw /healthz contract"
|
||||||
|
curl -fsS \
|
||||||
|
-H "x-request-id: ${RAW_REQUEST_ID}" \
|
||||||
|
-D "${RAW_HEADER_FILE}" \
|
||||||
|
-o "${RAW_BODY_FILE}" \
|
||||||
|
"${HEALTHZ_URL}"
|
||||||
|
|
||||||
|
RAW_BODY="$(tr -d '\n' <"${RAW_BODY_FILE}")"
|
||||||
|
RAW_REQUEST_ID_HEADER="$(grep -i '^x-request-id:' "${RAW_HEADER_FILE}" | tail -n 1 | cut -d':' -f2- | tr -d '\r' | xargs)"
|
||||||
|
API_VERSION_HEADER="$(grep -i '^x-api-version:' "${RAW_HEADER_FILE}" | tail -n 1 | cut -d':' -f2- | tr -d '\r' | xargs)"
|
||||||
|
ROUTE_VERSION_HEADER="$(grep -i '^x-route-version:' "${RAW_HEADER_FILE}" | tail -n 1 | cut -d':' -f2- | tr -d '\r' | xargs)"
|
||||||
|
RESPONSE_TIME_HEADER="$(grep -i '^x-response-time-ms:' "${RAW_HEADER_FILE}" | tail -n 1 | cut -d':' -f2- | tr -d '\r' | xargs)"
|
||||||
|
|
||||||
|
assert_contains "${RAW_BODY}" '"ok":true' "raw /healthz body 缺少 ok=true。"
|
||||||
|
assert_contains "${RAW_BODY}" '"service":"genarrative-node-server"' "raw /healthz body 返回了意外的 service。"
|
||||||
|
|
||||||
|
if [[ "${RAW_REQUEST_ID_HEADER}" != "${RAW_REQUEST_ID}" ]]; then
|
||||||
|
echo "[server-rs:smoke] raw /healthz 没有正确回写 x-request-id。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${API_VERSION_HEADER}" ]]; then
|
||||||
|
echo "[server-rs:smoke] raw /healthz 缺少 x-api-version。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${ROUTE_VERSION_HEADER}" != "${API_VERSION_HEADER}" ]]; then
|
||||||
|
echo "[server-rs:smoke] raw /healthz 的 x-route-version 与 x-api-version 不一致。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "${RESPONSE_TIME_HEADER}" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "[server-rs:smoke] raw /healthz 的 x-response-time-ms 不是合法整数。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENVELOPE_REQUEST_ID="smoke-healthz-envelope"
|
||||||
|
echo "[server-rs:smoke] 步骤: verify envelope /healthz contract"
|
||||||
|
curl -fsS \
|
||||||
|
-H "x-request-id: ${ENVELOPE_REQUEST_ID}" \
|
||||||
|
-H "x-genarrative-response-envelope: 1" \
|
||||||
|
-D "${ENVELOPE_HEADER_FILE}" \
|
||||||
|
-o "${ENVELOPE_BODY_FILE}" \
|
||||||
|
"${HEALTHZ_URL}"
|
||||||
|
|
||||||
|
ENVELOPE_BODY="$(tr -d '\n' <"${ENVELOPE_BODY_FILE}")"
|
||||||
|
|
||||||
|
assert_contains "${ENVELOPE_BODY}" '"ok":true' "envelope /healthz body 缺少 ok=true。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" '"error":null' "envelope /healthz body 缺少 error=null。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" '"data":{"ok":true,"service":"genarrative-node-server"}' "envelope /healthz data 返回异常。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" "\"requestId\":\"${ENVELOPE_REQUEST_ID}\"" "envelope /healthz meta.requestId 没有回写请求 ID。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" "\"apiVersion\":\"${API_VERSION_HEADER}\"" "envelope /healthz meta.apiVersion 与响应头不一致。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" "\"routeVersion\":\"${ROUTE_VERSION_HEADER}\"" "envelope /healthz meta.routeVersion 与响应头不一致。"
|
||||||
|
assert_contains "${ENVELOPE_BODY}" '"operation":"GET /healthz"' "envelope /healthz meta.operation 异常。"
|
||||||
|
|
||||||
|
echo "[server-rs:smoke] all checks passed"
|
||||||
Reference in New Issue
Block a user