This commit is contained in:
68
server-rs/scripts/check.ps1
Normal file
68
server-rs/scripts/check.ps1
Normal file
@@ -0,0 +1,68 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[Alias("Package")]
|
||||
[string]$Crate = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:'
|
||||
' ./server-rs/scripts/check.ps1'
|
||||
' ./server-rs/scripts/check.ps1 -Crate api-server'
|
||||
''
|
||||
'Notes:'
|
||||
' 1. Run cargo fmt --all --check for the whole server-rs workspace'
|
||||
' 2. Run clippy/check/test for the whole workspace by default'
|
||||
' 3. Use -Crate to target one workspace crate for clippy/check/test'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
|
||||
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
throw "Missing server-rs/Cargo.toml, cannot start check script."
|
||||
}
|
||||
|
||||
Write-Host "[server-rs:check] working dir: $serverRsDir"
|
||||
Write-Host "[server-rs:check] step: cargo fmt --all --check"
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
cargo fmt --all --check --manifest-path $manifestPath
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Crate)) {
|
||||
Write-Host "[server-rs:check] step: cargo clippy --workspace --all-targets --all-features -D warnings"
|
||||
cargo clippy --workspace --manifest-path $manifestPath --all-targets --all-features -- -D warnings
|
||||
|
||||
Write-Host "[server-rs:check] step: cargo check --workspace"
|
||||
cargo check --workspace --manifest-path $manifestPath
|
||||
|
||||
Write-Host "[server-rs:check] step: cargo test --workspace"
|
||||
cargo test --workspace --manifest-path $manifestPath
|
||||
}
|
||||
else {
|
||||
Write-Host "[server-rs:check] target crate: $Crate"
|
||||
Write-Host "[server-rs:check] step: cargo clippy -p $Crate --all-targets --all-features -D warnings"
|
||||
cargo clippy -p $Crate --manifest-path $manifestPath --all-targets --all-features -- -D warnings
|
||||
|
||||
Write-Host "[server-rs:check] step: cargo check -p $Crate"
|
||||
cargo check -p $Crate --manifest-path $manifestPath
|
||||
|
||||
Write-Host "[server-rs:check] step: cargo test -p $Crate"
|
||||
cargo test -p $Crate --manifest-path $manifestPath
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
62
server-rs/scripts/check.sh
Normal file
62
server-rs/scripts/check.sh
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 统一串联 Rust workspace 的格式、lint、编译与测试校验,保证本地和 CI 使用同一条检查链路。
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./server-rs/scripts/check.sh
|
||||
SERVER_RS_CHECK_CRATE=api-server ./server-rs/scripts/check.sh
|
||||
|
||||
说明:
|
||||
1. 先执行整个 `server-rs` workspace 的 `cargo fmt --all --check`
|
||||
2. 默认继续执行整个 workspace 的 `cargo clippy`、`cargo check`、`cargo test`
|
||||
3. 可通过 `SERVER_RS_CHECK_CRATE` 将 clippy/check/test 收窄到单个 crate
|
||||
4. `cargo fmt --all --check` 始终覆盖整个 workspace,避免多 crate 下格式口径漂移
|
||||
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"
|
||||
|
||||
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||||
echo "[server-rs:check] 未找到 ${MANIFEST_PATH},无法启动检查脚本。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[server-rs:check] 工作目录: ${SERVER_RS_DIR}"
|
||||
echo "[server-rs:check] 步骤: cargo fmt --all --check"
|
||||
|
||||
cd "${SERVER_RS_DIR}"
|
||||
cargo fmt --all --check --manifest-path "${MANIFEST_PATH}"
|
||||
|
||||
TARGET_CRATE="${SERVER_RS_CHECK_CRATE:-${SERVER_RS_CHECK_PACKAGE:-}}"
|
||||
|
||||
if [[ -n "${TARGET_CRATE}" ]]; then
|
||||
echo "[server-rs:check] 目标 crate: ${TARGET_CRATE}"
|
||||
echo "[server-rs:check] 步骤: cargo clippy -p ${TARGET_CRATE} --all-targets --all-features -D warnings"
|
||||
cargo clippy -p "${TARGET_CRATE}" --manifest-path "${MANIFEST_PATH}" --all-targets --all-features -- -D warnings
|
||||
|
||||
echo "[server-rs:check] 步骤: cargo check -p ${TARGET_CRATE}"
|
||||
cargo check -p "${TARGET_CRATE}" --manifest-path "${MANIFEST_PATH}"
|
||||
|
||||
echo "[server-rs:check] 步骤: cargo test -p ${TARGET_CRATE}"
|
||||
cargo test -p "${TARGET_CRATE}" --manifest-path "${MANIFEST_PATH}"
|
||||
else
|
||||
echo "[server-rs:check] 步骤: cargo clippy --workspace --all-targets --all-features -D warnings"
|
||||
cargo clippy --workspace --manifest-path "${MANIFEST_PATH}" --all-targets --all-features -- -D warnings
|
||||
|
||||
echo "[server-rs:check] 步骤: cargo check --workspace"
|
||||
cargo check --workspace --manifest-path "${MANIFEST_PATH}"
|
||||
|
||||
echo "[server-rs:check] 步骤: cargo test --workspace"
|
||||
cargo test --workspace --manifest-path "${MANIFEST_PATH}"
|
||||
fi
|
||||
52
server-rs/scripts/dev.ps1
Normal file
52
server-rs/scripts/dev.ps1
Normal file
@@ -0,0 +1,52 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[string]$ApiHost = "127.0.0.1",
|
||||
[int]$Port = 3100,
|
||||
[string]$Log = "info,tower_http=info"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:'
|
||||
' ./server-rs/scripts/dev.ps1'
|
||||
' ./server-rs/scripts/dev.ps1 -ApiHost 0.0.0.0 -Port 3100 -Log "debug,tower_http=debug"'
|
||||
''
|
||||
'Notes:'
|
||||
' 1. Set default local env vars for the Rust api-server'
|
||||
' 2. Start Axum with cargo run -p api-server'
|
||||
' 3. This script only covers the api-server local dev loop'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
|
||||
if (-not (Test-Path (Join-Path $serverRsDir "Cargo.toml"))) {
|
||||
throw "Missing server-rs/Cargo.toml, cannot start local dev script."
|
||||
}
|
||||
|
||||
$env:GENARRATIVE_API_HOST = $ApiHost
|
||||
$env:GENARRATIVE_API_PORT = "$Port"
|
||||
$env:GENARRATIVE_API_LOG = $Log
|
||||
|
||||
Write-Host "[server-rs:dev] working dir: $serverRsDir"
|
||||
Write-Host "[server-rs:dev] GENARRATIVE_API_HOST=$($env:GENARRATIVE_API_HOST)"
|
||||
Write-Host "[server-rs:dev] GENARRATIVE_API_PORT=$($env:GENARRATIVE_API_PORT)"
|
||||
Write-Host "[server-rs:dev] GENARRATIVE_API_LOG=$($env:GENARRATIVE_API_LOG)"
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
cargo run -p api-server --manifest-path (Join-Path $serverRsDir "Cargo.toml")
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
41
server-rs/scripts/dev.sh
Normal file
41
server-rs/scripts/dev.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./server-rs/scripts/dev.sh
|
||||
GENARRATIVE_API_HOST=0.0.0.0 GENARRATIVE_API_PORT=3100 ./server-rs/scripts/dev.sh
|
||||
|
||||
说明:
|
||||
1. 为 Rust `api-server` 注入本地开发默认环境变量
|
||||
2. 使用 `cargo run -p api-server` 启动 Axum 服务
|
||||
3. 当前只覆盖 `api-server` 本地开发链路,不包含 SpacetimeDB
|
||||
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)"
|
||||
|
||||
if [[ ! -f "${SERVER_RS_DIR}/Cargo.toml" ]]; then
|
||||
echo "[server-rs:dev] 未找到 ${SERVER_RS_DIR}/Cargo.toml,无法启动本地开发脚本。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export GENARRATIVE_API_HOST="${GENARRATIVE_API_HOST:-127.0.0.1}"
|
||||
export GENARRATIVE_API_PORT="${GENARRATIVE_API_PORT:-3100}"
|
||||
export GENARRATIVE_API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}"
|
||||
|
||||
echo "[server-rs:dev] 工作目录: ${SERVER_RS_DIR}"
|
||||
echo "[server-rs:dev] GENARRATIVE_API_HOST=${GENARRATIVE_API_HOST}"
|
||||
echo "[server-rs:dev] GENARRATIVE_API_PORT=${GENARRATIVE_API_PORT}"
|
||||
echo "[server-rs:dev] GENARRATIVE_API_LOG=${GENARRATIVE_API_LOG}"
|
||||
|
||||
cd "${SERVER_RS_DIR}"
|
||||
cargo run -p api-server --manifest-path "${SERVER_RS_DIR}/Cargo.toml"
|
||||
519
server-rs/scripts/oss-smoke.ps1
Normal file
519
server-rs/scripts/oss-smoke.ps1
Normal file
@@ -0,0 +1,519 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[string]$ApiHost = "127.0.0.1",
|
||||
[int]$Port = 3310,
|
||||
[string]$Log = "warn,tower_http=warn",
|
||||
[int]$StartupTimeoutSeconds = 30,
|
||||
[string]$LegacyPrefix = "/generated-character-drafts/*",
|
||||
[string[]]$PathSegments = @("oss-smoke"),
|
||||
[string]$FileName = "tmp_oss_upload_test.txt",
|
||||
[string]$FileContent = "Genarrative OSS smoke test",
|
||||
[switch]$KeepObject,
|
||||
[switch]$JsonOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:'
|
||||
' ./server-rs/scripts/oss-smoke.ps1'
|
||||
' ./server-rs/scripts/oss-smoke.ps1 -LegacyPrefix "/generated-characters/*" -PathSegments hero_001,visual,asset_01'
|
||||
' ./server-rs/scripts/oss-smoke.ps1 -KeepObject'
|
||||
''
|
||||
'Notes:'
|
||||
' 1. Load OSS config from repository root .env and .env.local'
|
||||
' 2. Start a temporary local api-server process'
|
||||
' 3. Request /api/assets/direct-upload-tickets and perform a real PostObject upload'
|
||||
' 4. Verify bucket access, uploaded object visibility, and delete the test object by default'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
function Assert-Condition {
|
||||
param(
|
||||
[bool]$Condition,
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
if (-not $Condition) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Read-EnvFile {
|
||||
param(
|
||||
[string]$Path,
|
||||
[hashtable]$Target
|
||||
)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -Encoding UTF8 $Path) {
|
||||
$trimmed = $line.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith('#')) {
|
||||
continue
|
||||
}
|
||||
|
||||
$separatorIndex = $trimmed.IndexOf('=')
|
||||
if ($separatorIndex -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
$key = $trimmed.Substring(0, $separatorIndex).Trim()
|
||||
$value = $trimmed.Substring($separatorIndex + 1).Trim()
|
||||
|
||||
if ($value.Length -ge 2 -and $value.StartsWith('"') -and $value.EndsWith('"')) {
|
||||
$value = $value.Substring(1, $value.Length - 2)
|
||||
}
|
||||
|
||||
$Target[$key] = $value
|
||||
}
|
||||
}
|
||||
|
||||
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 ConvertTo-Rfc1123Date {
|
||||
return [DateTime]::UtcNow.ToString("r", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
}
|
||||
|
||||
function New-HmacSha1Signature {
|
||||
param(
|
||||
[string]$Secret,
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$hmac = New-Object System.Security.Cryptography.HMACSHA1
|
||||
try {
|
||||
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret)
|
||||
$hashBytes = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Content))
|
||||
return [Convert]::ToBase64String($hashBytes)
|
||||
}
|
||||
finally {
|
||||
$hmac.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SignedOssRequest {
|
||||
param(
|
||||
[string]$Method,
|
||||
[string]$Bucket,
|
||||
[string]$Endpoint,
|
||||
[string]$AccessKeyId,
|
||||
[string]$AccessKeySecret,
|
||||
[string]$ObjectKey = ""
|
||||
)
|
||||
|
||||
$dateValue = ConvertTo-Rfc1123Date
|
||||
$canonicalResource = if ([string]::IsNullOrWhiteSpace($ObjectKey)) {
|
||||
"/$Bucket/"
|
||||
}
|
||||
else {
|
||||
"/$Bucket/$ObjectKey"
|
||||
}
|
||||
$stringToSign = "$Method`n`n`n$dateValue`n$canonicalResource"
|
||||
$signature = New-HmacSha1Signature -Secret $AccessKeySecret -Content $stringToSign
|
||||
$uri = if ([string]::IsNullOrWhiteSpace($ObjectKey)) {
|
||||
"https://$Bucket.$Endpoint/"
|
||||
}
|
||||
else {
|
||||
"https://$Bucket.$Endpoint/$ObjectKey"
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest `
|
||||
-Uri $uri `
|
||||
-Method $Method `
|
||||
-Headers @{
|
||||
"Date" = $dateValue
|
||||
"Authorization" = "OSS $AccessKeyId`:$signature"
|
||||
} `
|
||||
-UseBasicParsing `
|
||||
-TimeoutSec 30
|
||||
|
||||
return @{
|
||||
ok = $true
|
||||
status = [int]$response.StatusCode
|
||||
url = $uri
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
$body = $_.Exception.Message
|
||||
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
ok = $false
|
||||
status = $statusCode
|
||||
url = $uri
|
||||
body = $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 Invoke-JsonPost {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[string]$Body
|
||||
)
|
||||
|
||||
$response = Invoke-WebRequest `
|
||||
-Uri $Uri `
|
||||
-Method Post `
|
||||
-ContentType "application/json" `
|
||||
-Headers @{ "x-request-id" = "oss-smoke-$([guid]::NewGuid().ToString('N').Substring(0, 8))" } `
|
||||
-Body $Body `
|
||||
-UseBasicParsing `
|
||||
-TimeoutSec 30
|
||||
|
||||
return $response.Content | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function New-MultipartFormData {
|
||||
param(
|
||||
[hashtable]$Fields,
|
||||
[string]$FilePath,
|
||||
[string]$FileFieldName = "file"
|
||||
)
|
||||
|
||||
$boundary = "----CodexBoundary$([Guid]::NewGuid().ToString('N'))"
|
||||
$encoding = [System.Text.Encoding]::UTF8
|
||||
$memory = New-Object System.IO.MemoryStream
|
||||
|
||||
try {
|
||||
foreach ($entry in $Fields.GetEnumerator()) {
|
||||
$prefix = "--$boundary`r`nContent-Disposition: form-data; name=""$($entry.Key)""`r`n`r`n$($entry.Value)`r`n"
|
||||
$bytes = $encoding.GetBytes($prefix)
|
||||
$memory.Write($bytes, 0, $bytes.Length)
|
||||
}
|
||||
|
||||
$fileInfo = Get-Item -LiteralPath $FilePath
|
||||
$mimeType = "application/octet-stream"
|
||||
if ($fileInfo.Extension -ieq ".txt") {
|
||||
$mimeType = "text/plain"
|
||||
}
|
||||
|
||||
$fileHeader = "--$boundary`r`nContent-Disposition: form-data; name=""$FileFieldName""; filename=""$($fileInfo.Name)""`r`nContent-Type: $mimeType`r`n`r`n"
|
||||
$fileHeaderBytes = $encoding.GetBytes($fileHeader)
|
||||
$memory.Write($fileHeaderBytes, 0, $fileHeaderBytes.Length)
|
||||
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes($FilePath)
|
||||
$memory.Write($fileBytes, 0, $fileBytes.Length)
|
||||
|
||||
$fileFooterBytes = $encoding.GetBytes("`r`n--$boundary--`r`n")
|
||||
$memory.Write($fileFooterBytes, 0, $fileFooterBytes.Length)
|
||||
|
||||
return @{
|
||||
Boundary = $boundary
|
||||
Body = $memory.ToArray()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$memory.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-IfExists {
|
||||
param([string]$Path)
|
||||
|
||||
if (Test-Path $Path) {
|
||||
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
$repoRoot = Split-Path -Parent $serverRsDir
|
||||
$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-oss-smoke-$runId.stdout.log"
|
||||
$stderrLog = Join-Path $env:TEMP "genarrative-server-rs-oss-smoke-$runId.stderr.log"
|
||||
$uploadFilePath = Join-Path $env:TEMP "$runId-$FileName"
|
||||
$envSnapshot = @{}
|
||||
$mergedEnv = @{}
|
||||
$serverProcess = $null
|
||||
$deleteResult = $null
|
||||
$signedObjectHead = $null
|
||||
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
throw "Missing server-rs/Cargo.toml, cannot start OSS smoke script."
|
||||
}
|
||||
|
||||
Read-EnvFile -Path (Join-Path $repoRoot ".env") -Target $mergedEnv
|
||||
Read-EnvFile -Path (Join-Path $repoRoot ".env.local") -Target $mergedEnv
|
||||
|
||||
$bucket = [string]($mergedEnv["ALIYUN_OSS_BUCKET"])
|
||||
$endpoint = [string]($mergedEnv["ALIYUN_OSS_ENDPOINT"])
|
||||
$accessKeyId = [string]($mergedEnv["ALIYUN_OSS_ACCESS_KEY_ID"])
|
||||
$accessKeySecret = [string]($mergedEnv["ALIYUN_OSS_ACCESS_KEY_SECRET"])
|
||||
|
||||
Assert-Condition (-not [string]::IsNullOrWhiteSpace($bucket)) "Missing ALIYUN_OSS_BUCKET in .env/.env.local."
|
||||
Assert-Condition (-not [string]::IsNullOrWhiteSpace($endpoint)) "Missing ALIYUN_OSS_ENDPOINT in .env/.env.local."
|
||||
Assert-Condition (-not [string]::IsNullOrWhiteSpace($accessKeyId)) "Missing ALIYUN_OSS_ACCESS_KEY_ID in .env/.env.local."
|
||||
Assert-Condition (-not [string]::IsNullOrWhiteSpace($accessKeySecret)) "Missing ALIYUN_OSS_ACCESS_KEY_SECRET in .env/.env.local."
|
||||
|
||||
$bucketHead = Invoke-SignedOssRequest `
|
||||
-Method "HEAD" `
|
||||
-Bucket $bucket `
|
||||
-Endpoint $endpoint `
|
||||
-AccessKeyId $accessKeyId `
|
||||
-AccessKeySecret $accessKeySecret
|
||||
|
||||
[System.IO.File]::WriteAllText($uploadFilePath, "$FileContent`n", [System.Text.Encoding]::UTF8)
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
Write-Host "[server-rs:oss-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."
|
||||
|
||||
foreach ($entry in $mergedEnv.GetEnumerator()) {
|
||||
Set-InheritedEnvVar -Name $entry.Key -Value ([string]$entry.Value) -Snapshot $envSnapshot
|
||||
}
|
||||
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:oss-smoke] step: start api-server binary"
|
||||
$serverProcess = Start-Process `
|
||||
-FilePath $binaryPath `
|
||||
-WorkingDirectory $repoRoot `
|
||||
-PassThru `
|
||||
-RedirectStandardOutput $stdoutLog `
|
||||
-RedirectStandardError $stderrLog
|
||||
|
||||
Restore-InheritedEnvVars -Snapshot $envSnapshot
|
||||
|
||||
Write-Host "[server-rs:oss-smoke] step: wait for /healthz readiness"
|
||||
Wait-ForHealthz -Uri $healthzUrl -TimeoutSeconds $StartupTimeoutSeconds -Process $serverProcess
|
||||
|
||||
$timestampSegment = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$resolvedPathSegments = @($PathSegments + $timestampSegment)
|
||||
$ticketRequestBody = @{
|
||||
legacyPrefix = $LegacyPrefix
|
||||
pathSegments = $resolvedPathSegments
|
||||
fileName = $FileName
|
||||
contentType = "text/plain"
|
||||
metadata = @{
|
||||
origin = "server-rs-oss-smoke"
|
||||
"asset-kind" = "manual-test"
|
||||
}
|
||||
} | ConvertTo-Json -Depth 5
|
||||
|
||||
Write-Host "[server-rs:oss-smoke] step: request direct upload ticket"
|
||||
$ticketEnvelope = Invoke-JsonPost -Uri "$baseUrl/api/assets/direct-upload-tickets" -Body $ticketRequestBody
|
||||
$upload = $ticketEnvelope.upload
|
||||
if ($null -eq $upload) {
|
||||
$upload = $ticketEnvelope.data.upload
|
||||
}
|
||||
Assert-Condition ($null -ne $upload) "OSS direct upload ticket response is missing upload payload."
|
||||
|
||||
$formFields = @{}
|
||||
$upload.formFields.psobject.Properties | ForEach-Object {
|
||||
$formFields[$_.Name] = [string]$_.Value
|
||||
}
|
||||
|
||||
$multipart = New-MultipartFormData -Fields $formFields -FilePath $uploadFilePath
|
||||
|
||||
Write-Host "[server-rs:oss-smoke] step: upload test object to OSS"
|
||||
$uploadResponse = $null
|
||||
try {
|
||||
$uploadResponse = Invoke-WebRequest `
|
||||
-Uri $upload.host `
|
||||
-Method Post `
|
||||
-ContentType "multipart/form-data; boundary=$($multipart.Boundary)" `
|
||||
-Body $multipart.Body `
|
||||
-UseBasicParsing `
|
||||
-TimeoutSec 60
|
||||
|
||||
$uploadResult = @{
|
||||
ok = $true
|
||||
status = [int]$uploadResponse.StatusCode
|
||||
body = $uploadResponse.Content
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
}
|
||||
}
|
||||
|
||||
$uploadResult = @{
|
||||
ok = $false
|
||||
status = $statusCode
|
||||
body = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
$publicHead = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$upload.publicUrl)) {
|
||||
try {
|
||||
$publicResponse = Invoke-WebRequest `
|
||||
-Uri ([string]$upload.publicUrl) `
|
||||
-Method Head `
|
||||
-UseBasicParsing `
|
||||
-TimeoutSec 30
|
||||
|
||||
$publicHead = @{
|
||||
ok = $true
|
||||
status = [int]$publicResponse.StatusCode
|
||||
url = [string]$upload.publicUrl
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
}
|
||||
}
|
||||
|
||||
$publicHead = @{
|
||||
ok = $false
|
||||
status = $statusCode
|
||||
url = [string]$upload.publicUrl
|
||||
body = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$signedObjectHead = Invoke-SignedOssRequest `
|
||||
-Method "HEAD" `
|
||||
-Bucket $bucket `
|
||||
-Endpoint $endpoint `
|
||||
-AccessKeyId $accessKeyId `
|
||||
-AccessKeySecret $accessKeySecret `
|
||||
-ObjectKey ([string]$upload.objectKey)
|
||||
|
||||
if (-not $KeepObject) {
|
||||
$deleteResult = Invoke-SignedOssRequest `
|
||||
-Method "DELETE" `
|
||||
-Bucket $bucket `
|
||||
-Endpoint $endpoint `
|
||||
-AccessKeyId $accessKeyId `
|
||||
-AccessKeySecret $accessKeySecret `
|
||||
-ObjectKey ([string]$upload.objectKey)
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
bucketHead = $bucketHead
|
||||
ticket = [ordered]@{
|
||||
host = [string]$upload.host
|
||||
objectKey = [string]$upload.objectKey
|
||||
legacyPublicPath = [string]$upload.legacyPublicPath
|
||||
publicUrl = if ([string]::IsNullOrWhiteSpace([string]$upload.publicUrl)) {
|
||||
$null
|
||||
}
|
||||
else {
|
||||
[string]$upload.publicUrl
|
||||
}
|
||||
}
|
||||
upload = $uploadResult
|
||||
publicHead = $publicHead
|
||||
signedObjectHead = $signedObjectHead
|
||||
delete = $deleteResult
|
||||
}
|
||||
|
||||
if ($JsonOnly) {
|
||||
$result | ConvertTo-Json -Depth 8
|
||||
}
|
||||
else {
|
||||
Write-Host "[server-rs:oss-smoke] result:"
|
||||
$result | ConvertTo-Json -Depth 8
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Restore-InheritedEnvVars -Snapshot $envSnapshot
|
||||
|
||||
if ($null -ne $serverProcess -and -not $serverProcess.HasExited) {
|
||||
Stop-Process -Id $serverProcess.Id -Force
|
||||
$serverProcess.WaitForExit()
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
|
||||
Remove-IfExists -Path $uploadFilePath
|
||||
Remove-IfExists -Path $stdoutLog
|
||||
Remove-IfExists -Path $stderrLog
|
||||
}
|
||||
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"
|
||||
68
server-rs/scripts/spacetime-dev.ps1
Normal file
68
server-rs/scripts/spacetime-dev.ps1
Normal file
@@ -0,0 +1,68 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[string]$ListenHost = "127.0.0.1",
|
||||
[int]$Port = 3000,
|
||||
[string]$RootDir = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:'
|
||||
' ./server-rs/scripts/spacetime-dev.ps1'
|
||||
' ./server-rs/scripts/spacetime-dev.ps1 -ListenHost 127.0.0.1 -Port 3101'
|
||||
''
|
||||
'Notes:'
|
||||
' 1. Start local standalone SpacetimeDB for the Genarrative Rust backend'
|
||||
' 2. Store local SpacetimeDB state in server-rs/.spacetimedb/local by default'
|
||||
' 3. Default port is 3000 to align with the spacetime CLI local server alias'
|
||||
' 4. Current stage already has crates/spacetime-module scaffold, but still does not auto-publish the module'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RootDir)) {
|
||||
$RootDir = Join-Path $serverRsDir ".spacetimedb\local"
|
||||
}
|
||||
|
||||
if (-not (Test-Path (Join-Path $serverRsDir "crates\spacetime-module\README.md"))) {
|
||||
throw "Missing server-rs/crates/spacetime-module/README.md, cannot start SpacetimeDB local dev script."
|
||||
}
|
||||
|
||||
$spacetimeCommand = Get-Command spacetime -ErrorAction SilentlyContinue
|
||||
|
||||
if ($null -eq $spacetimeCommand) {
|
||||
throw @(
|
||||
"Missing 'spacetime' CLI.",
|
||||
"Install guide: https://spacetimedb.com/install",
|
||||
"Windows PowerShell: iwr https://windows.spacetimedb.com -useb | iex"
|
||||
) -join " "
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RootDir | Out-Null
|
||||
|
||||
$listenAddress = "$ListenHost`:$Port"
|
||||
|
||||
Write-Host "[server-rs:spacetime-dev] working dir: $serverRsDir"
|
||||
Write-Host "[server-rs:spacetime-dev] root dir: $RootDir"
|
||||
Write-Host "[server-rs:spacetime-dev] listen addr: $listenAddress"
|
||||
Write-Host "[server-rs:spacetime-dev] mode: standalone"
|
||||
Write-Host "[server-rs:spacetime-dev] note: module scaffold already exists; publish remains manual in this stage"
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
& $spacetimeCommand.Source --root-dir $RootDir start --edition standalone --listen-addr $listenAddress
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
53
server-rs/scripts/spacetime-dev.sh
Normal file
53
server-rs/scripts/spacetime-dev.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 当前阶段只负责启动本地 standalone SpacetimeDB,先把服务器进程和数据目录口径固定下来,不提前耦合 publish 流程。
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./server-rs/scripts/spacetime-dev.sh
|
||||
GENARRATIVE_SPACETIME_PORT=3101 ./server-rs/scripts/spacetime-dev.sh
|
||||
|
||||
说明:
|
||||
1. 启动 Genarrative Rust 后端使用的本地 standalone SpacetimeDB
|
||||
2. 默认把本地数据目录放到 `server-rs/.spacetimedb/local`
|
||||
3. 默认端口使用 `3000`,与 `spacetime` CLI 的 local server 昵称保持一致
|
||||
4. 当前阶段已具备 `crates/spacetime-module` scaffold,但暂不自动 publish
|
||||
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)"
|
||||
LISTEN_HOST="${GENARRATIVE_SPACETIME_HOST:-127.0.0.1}"
|
||||
PORT="${GENARRATIVE_SPACETIME_PORT:-3000}"
|
||||
ROOT_DIR="${GENARRATIVE_SPACETIME_ROOT_DIR:-${SERVER_RS_DIR}/.spacetimedb/local}"
|
||||
|
||||
if [[ ! -f "${SERVER_RS_DIR}/crates/spacetime-module/README.md" ]]; then
|
||||
echo "[server-rs:spacetime-dev] 未找到 crates/spacetime-module/README.md,无法启动本地 SpacetimeDB 脚本。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v spacetime >/dev/null 2>&1; then
|
||||
echo "[server-rs:spacetime-dev] 缺少 spacetime CLI。" >&2
|
||||
echo "[server-rs:spacetime-dev] 安装文档: https://spacetimedb.com/install" >&2
|
||||
echo "[server-rs:spacetime-dev] Linux/macOS 安装命令: curl -sSf https://install.spacetimedb.com | sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${ROOT_DIR}"
|
||||
|
||||
echo "[server-rs:spacetime-dev] 工作目录: ${SERVER_RS_DIR}"
|
||||
echo "[server-rs:spacetime-dev] 数据目录: ${ROOT_DIR}"
|
||||
echo "[server-rs:spacetime-dev] 监听地址: ${LISTEN_HOST}:${PORT}"
|
||||
echo "[server-rs:spacetime-dev] 模式: standalone"
|
||||
echo "[server-rs:spacetime-dev] 说明: 当前阶段已落 crate scaffold,但仍不自动 publish crates/spacetime-module"
|
||||
|
||||
cd "${SERVER_RS_DIR}"
|
||||
spacetime --root-dir "${ROOT_DIR}" start --edition standalone --listen-addr "${LISTEN_HOST}:${PORT}"
|
||||
50
server-rs/scripts/test.ps1
Normal file
50
server-rs/scripts/test.ps1
Normal file
@@ -0,0 +1,50 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[Alias("Package")]
|
||||
[string]$Crate = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:'
|
||||
' ./server-rs/scripts/test.ps1'
|
||||
' ./server-rs/scripts/test.ps1 -Crate api-server'
|
||||
''
|
||||
'Notes:'
|
||||
' 1. Run cargo test for the server-rs workspace by default'
|
||||
' 2. Use -Crate to target one workspace crate only'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
|
||||
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
throw "Missing server-rs/Cargo.toml, cannot start test script."
|
||||
}
|
||||
|
||||
Write-Host "[server-rs:test] working dir: $serverRsDir"
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
if ([string]::IsNullOrWhiteSpace($Crate)) {
|
||||
cargo test --manifest-path $manifestPath
|
||||
}
|
||||
else {
|
||||
Write-Host "[server-rs:test] target crate: $Crate"
|
||||
cargo test -p $Crate --manifest-path $manifestPath
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
42
server-rs/scripts/test.sh
Normal file
42
server-rs/scripts/test.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./server-rs/scripts/test.sh
|
||||
SERVER_RS_TEST_CRATE=api-server ./server-rs/scripts/test.sh
|
||||
|
||||
Notes:
|
||||
1. Run cargo test for the server-rs workspace by default
|
||||
2. Use SERVER_RS_TEST_CRATE to target one workspace crate only
|
||||
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"
|
||||
|
||||
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||||
echo "[server-rs:test] Missing ${MANIFEST_PATH}, cannot start test script." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[server-rs:test] working dir: ${SERVER_RS_DIR}"
|
||||
|
||||
cd "${SERVER_RS_DIR}"
|
||||
|
||||
TARGET_CRATE="${SERVER_RS_TEST_CRATE:-${SERVER_RS_TEST_PACKAGE:-}}"
|
||||
|
||||
if [[ -n "${TARGET_CRATE}" ]]; then
|
||||
echo "[server-rs:test] target crate: ${TARGET_CRATE}"
|
||||
cargo test -p "${TARGET_CRATE}" --manifest-path "${MANIFEST_PATH}"
|
||||
else
|
||||
cargo test --manifest-path "${MANIFEST_PATH}"
|
||||
fi
|
||||
Reference in New Issue
Block a user