feat: 建立三项目可选部署编排 (#154)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# YoVision 可选协调部署
|
||||
|
||||
本目录只组合已经验收的 Sense、Brain、Bell 交付入口,不复制产品实现,也不成为业务事实源。三个产品仍使用独立包、进程、端口、数据目录、日志、数据库角色、账户空间、浏览器 Cookie 和机器身份。
|
||||
|
||||
## 准备
|
||||
|
||||
1. 将 `coordination.example.json` 复制到仓库外受控配置目录,填写三个已审核交付包的精确版本与绝对路径。
|
||||
2. 分别准备仓库外 `sense.env`、`brain.env`、`bell.env`。Sense 至少提供 `SENSE_DATABASE_URL`、`SENSE_JWT_SECRET`、`SENSE_PORT`,Bell 至少提供 `BELL_DATABASE_URL`、`BELL_JWT_SECRET`、`BELL_PORT`、`BELL_WEB_PORT`;其余配置(包括 connector 开关)仍遵循各产品自己的环境文件说明。不得在清单或仓库中填写密码、JWT、token 或私钥内容。
|
||||
3. 为每个调用实例创建独立 Ed25519 私钥和消费者公钥注册表;清单只引用私钥路径。
|
||||
4. 为 Sense、Bell 创建不同 PostgreSQL 数据库和角色;先使用各自迁移入口完成备份与迁移。
|
||||
5. 确认清单声明的所有端口、包目录、数据目录和日志目录互不重叠。
|
||||
|
||||
编排器会创建并隔离清单中的数据、日志目录;各产品环境文件或产品配置还必须把自身持久化与业务日志指向对应目录。编排器不会猜测或改写产品内部配置。
|
||||
|
||||
示例清单中的地址、版本和标识都是不可直接投产的占位值。默认 16 路只是当前交付配额,不是编排器容量上限。
|
||||
|
||||
## 命令
|
||||
|
||||
从仓库根目录运行;不需要修改 PowerShell 执行策略:
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File scripts/runtime/coordination/start-yovision.ps1 -Manifest C:\YoVision\config\coordination.json -ValidateOnly
|
||||
pwsh -NoProfile -File scripts/runtime/coordination/start-yovision.ps1 -Manifest C:\YoVision\config\coordination.json -Product bell,sense,brain
|
||||
pwsh -NoProfile -File scripts/runtime/coordination/status-yovision.ps1 -Manifest C:\YoVision\config\coordination.json -Product all
|
||||
pwsh -NoProfile -File scripts/runtime/coordination/stop-yovision.ps1 -Manifest C:\YoVision\config\coordination.json -Product brain
|
||||
```
|
||||
|
||||
`.bat` 包装器接受相同参数。启动时,`all` 只包含清单中 `enabled=true` 的产品;停止和查看状态时,`all` 会覆盖三个产品,避免产品被停用后遗留进程。启动顺序固定为 Bell → Sense → Brain,停止顺序固定为 Brain → Sense → Bell。单端失败只返回非零并清理该端新进程,不自动停止已经运行的其他端。
|
||||
|
||||
## 状态与日志
|
||||
|
||||
编排状态写入清单的 `runtime_root\state`,每端只保存 PID、启动时间、版本、命令路径和清单摘要,不保存环境变量值。协调启动日志分别写入各产品独立 `log_directory`。产品自己的日志仍由产品入口管理。
|
||||
|
||||
`status` 返回 `running`、`unhealthy`、`stopped` 或 `stale`;仅当所选产品全部健康运行时退出码为 0。PID 与命令归属不匹配时不停止进程,必须人工核对。
|
||||
|
||||
## 升级与回退
|
||||
|
||||
升级顺序为:备份 Sense/Bell → 迁移 Bell → 启动/检查 Bell → 迁移 Sense → 启动/检查 Sense → 更新 Brain → 启用 connector。每次只替换一个独立包并更新清单版本。
|
||||
|
||||
回退时先停用 Brain event export、Sense ingress/relay 与 Bell ingress/evidence connector,再按产品独立入口回退包或恢复各自数据库。不得删除 Sense Outbox、运行投影、Bell Receipt/Event、replay 或审计事实。根级编排故障时直接恢复三个产品原有独立入口。
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"schema_version": "yovision.coordination/v1",
|
||||
"deployment_id": "school-a-yovision",
|
||||
"runtime_root": "C:\\YoVision\\runtime\\coordination",
|
||||
"products": {
|
||||
"sense": {
|
||||
"enabled": true,
|
||||
"version": "replace-with-reviewed-sense-artifact-version",
|
||||
"package_root": "C:\\YoVision\\packages\\sense",
|
||||
"environment_file": "C:\\YoVision\\secrets\\sense.env",
|
||||
"data_directory": "C:\\YoVision\\data\\sense",
|
||||
"log_directory": "C:\\YoVision\\logs\\sense",
|
||||
"ports": [18080, 9997, 8889],
|
||||
"browser_origin": "http://127.0.0.1:18080",
|
||||
"cookie_name": "Sense-Admin-Token",
|
||||
"account_namespace": "sense-users",
|
||||
"database_id": "sense",
|
||||
"database_role": "sense_app",
|
||||
"start": { "executable": "scripts\\runtime\\start-sense.ps1", "arguments": ["-Mode", "production"] },
|
||||
"stop": { "executable": "scripts\\runtime\\stop-sense.ps1", "arguments": ["-Mode", "production"] },
|
||||
"health": { "kind": "http", "url": "http://127.0.0.1:18080/healthz", "timeout_seconds": 60 },
|
||||
"identities": [
|
||||
{ "principal": "yv:sense:school-a", "key_id": "sense-2026-01", "private_key_path": "C:\\YoVision\\secrets\\sense-to-bell.ed25519" }
|
||||
]
|
||||
},
|
||||
"brain": {
|
||||
"enabled": true,
|
||||
"version": "replace-with-reviewed-brain-artifact-version",
|
||||
"package_root": "C:\\YoVision\\packages\\brain",
|
||||
"environment_file": "C:\\YoVision\\secrets\\brain.env",
|
||||
"data_directory": "C:\\YoVision\\data\\brain",
|
||||
"log_directory": "C:\\YoVision\\logs\\brain",
|
||||
"ports": [18100],
|
||||
"browser_origin": "",
|
||||
"cookie_name": "",
|
||||
"account_namespace": "brain-machine-only",
|
||||
"database_id": "",
|
||||
"database_role": "",
|
||||
"start": { "executable": ".venv\\Scripts\\python.exe", "arguments": ["-m", "yovision_brain.app", "--config", "C:\\YoVision\\config\\brain.json", "--output", "-"] },
|
||||
"health": { "kind": "process", "timeout_seconds": 15 },
|
||||
"identities": [
|
||||
{ "principal": "yv:brain:school-a", "key_id": "brain-2026-01", "private_key_path": "C:\\YoVision\\secrets\\brain-to-sense.ed25519" }
|
||||
]
|
||||
},
|
||||
"bell": {
|
||||
"enabled": true,
|
||||
"version": "replace-with-reviewed-bell-artifact-version",
|
||||
"package_root": "C:\\YoVision\\packages\\bell",
|
||||
"environment_file": "C:\\YoVision\\secrets\\bell.env",
|
||||
"data_directory": "C:\\YoVision\\data\\bell",
|
||||
"log_directory": "C:\\YoVision\\logs\\bell",
|
||||
"ports": [18090, 18091],
|
||||
"browser_origin": "http://127.0.0.1:18091",
|
||||
"cookie_name": "Bell-Admin-Token",
|
||||
"account_namespace": "bell-users",
|
||||
"database_id": "bell",
|
||||
"database_role": "bell_app",
|
||||
"start": { "executable": "scripts\\runtime\\start-bell.ps1", "arguments": [] },
|
||||
"stop": { "executable": "scripts\\runtime\\stop-bell.ps1", "arguments": [] },
|
||||
"health": { "kind": "http", "url": "http://127.0.0.1:18090/healthz", "timeout_seconds": 60 },
|
||||
"identities": [
|
||||
{ "principal": "yv:bell:school-a", "key_id": "bell-2026-01", "private_key_path": "C:\\YoVision\\secrets\\bell-to-sense.ed25519" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://yovision.local/schemas/coordination/v1",
|
||||
"title": "YoVision coordination deployment manifest v1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "deployment_id", "runtime_root", "products"],
|
||||
"properties": {
|
||||
"schema_version": { "const": "yovision.coordination/v1" },
|
||||
"deployment_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$" },
|
||||
"runtime_root": { "type": "string", "minLength": 1 },
|
||||
"products": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["sense", "brain", "bell"],
|
||||
"properties": {
|
||||
"sense": { "$ref": "#/$defs/product" },
|
||||
"brain": { "$ref": "#/$defs/product" },
|
||||
"bell": { "$ref": "#/$defs/product" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"command": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executable", "arguments"],
|
||||
"properties": {
|
||||
"executable": { "type": "string", "minLength": 1 },
|
||||
"arguments": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["principal", "key_id", "private_key_path"],
|
||||
"properties": {
|
||||
"principal": { "type": "string", "pattern": "^yv:(sense|brain|bell):[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
||||
"key_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
||||
"private_key_path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
},
|
||||
"product": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "version", "package_root", "environment_file", "data_directory", "log_directory", "ports", "browser_origin", "cookie_name", "account_namespace", "database_id", "database_role", "start", "health", "identities"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"version": { "type": "string", "minLength": 1 },
|
||||
"package_root": { "type": "string", "minLength": 1 },
|
||||
"environment_file": { "type": "string", "minLength": 1 },
|
||||
"data_directory": { "type": "string", "minLength": 1 },
|
||||
"log_directory": { "type": "string", "minLength": 1 },
|
||||
"ports": { "type": "array", "items": { "type": "integer", "minimum": 1, "maximum": 65535 }, "uniqueItems": true },
|
||||
"browser_origin": { "type": "string" },
|
||||
"cookie_name": { "type": "string" },
|
||||
"account_namespace": { "type": "string", "minLength": 1 },
|
||||
"database_id": { "type": "string" },
|
||||
"database_role": { "type": "string" },
|
||||
"start": { "$ref": "#/$defs/command" },
|
||||
"stop": { "$ref": "#/$defs/command" },
|
||||
"health": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "timeout_seconds"],
|
||||
"properties": {
|
||||
"kind": { "enum": ["process", "http"] },
|
||||
"url": { "type": "string" },
|
||||
"timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 300 }
|
||||
}
|
||||
},
|
||||
"identities": { "type": "array", "items": { "$ref": "#/$defs/identity" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-True {
|
||||
param([Parameter(Mandatory = $true)][bool]$Condition, [Parameter(Mandatory = $true)][string]$Message)
|
||||
if (-not $Condition) { throw $Message }
|
||||
}
|
||||
|
||||
function Get-FreePort {
|
||||
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
|
||||
try { $listener.Start(); return ([Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() }
|
||||
}
|
||||
|
||||
function Write-Utf8File {
|
||||
param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content)
|
||||
$directory = Split-Path -Parent $Path
|
||||
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
|
||||
[IO.File]::WriteAllText($Path, $Content, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$scriptsRoot = Join-Path $repositoryRoot 'scripts\runtime\coordination'
|
||||
$startScript = Join-Path $scriptsRoot 'start-yovision.ps1'
|
||||
$stopScript = Join-Path $scriptsRoot 'stop-yovision.ps1'
|
||||
$statusScript = Join-Path $scriptsRoot 'status-yovision.ps1'
|
||||
$tempParent = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||
$testRoot = Join-Path $tempParent ("yovision coordination-" + [guid]::NewGuid().ToString('N'))
|
||||
$manifestPath = Join-Path $testRoot 'config\coordination.json'
|
||||
$manifest = $null
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $testRoot,(Join-Path $testRoot 'markers') | Out-Null
|
||||
$ports = @{ sense = Get-FreePort; brain = Get-FreePort; bell = Get-FreePort; bellWeb = Get-FreePort }
|
||||
Assert-True (($ports.Values | Select-Object -Unique).Count -eq 4) 'Dynamic test ports are not unique.'
|
||||
$products = [ordered]@{}
|
||||
foreach ($name in @('sense', 'brain', 'bell')) {
|
||||
$packageRoot = Join-Path $testRoot "packages\$name"
|
||||
$start = Join-Path $packageRoot 'start.ps1'
|
||||
$stop = Join-Path $packageRoot 'stop.ps1'
|
||||
Write-Utf8File -Path $start -Content @'
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:FAKE_STARTED_FILE) { [IO.File]::WriteAllText($env:FAKE_STARTED_FILE, $PID.ToString(), [Text.UTF8Encoding]::new($false)) }
|
||||
while ($true) { Start-Sleep -Milliseconds 250 }
|
||||
'@
|
||||
Write-Utf8File -Path $stop -Content @'
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:FAKE_STOPPED_FILE) { [IO.File]::WriteAllText($env:FAKE_STOPPED_FILE, 'stopped', [Text.UTF8Encoding]::new($false)) }
|
||||
$ownedPID = 0
|
||||
if (-not [int]::TryParse($env:YOVISION_COORDINATION_OWNED_PID, [ref]$ownedPID)) { exit 4 }
|
||||
& taskkill.exe /PID $ownedPID /T /F 2>$null | Out-Null
|
||||
exit 0
|
||||
'@
|
||||
$environmentPath = Join-Path $testRoot "secrets\$name.env"
|
||||
$environment = "FAKE_STARTED_FILE=$(Join-Path $testRoot "markers\$name.started")`nFAKE_STOPPED_FILE=$(Join-Path $testRoot "markers\$name.stopped")`n"
|
||||
if ($name -eq 'sense') { $environment += "SENSE_DATABASE_URL=postgres://sense_role@127.0.0.1/sense_db`nSENSE_JWT_SECRET=$(('s' * 40))`nSENSE_PORT=$($ports.sense)`n" }
|
||||
if ($name -eq 'bell') { $environment += "BELL_DATABASE_URL=postgres://bell_role@127.0.0.1/bell_db`nBELL_JWT_SECRET=$(('b' * 40))`nBELL_PORT=$($ports.bell)`nBELL_WEB_PORT=$($ports.bellWeb)`n" }
|
||||
Write-Utf8File -Path $environmentPath -Content $environment
|
||||
$keyPath = Join-Path $testRoot "secrets\$name.ed25519"
|
||||
Write-Utf8File -Path $keyPath -Content ([guid]::NewGuid().ToString('N'))
|
||||
$productPorts = if ($name -eq 'sense') { @($ports.sense) } elseif ($name -eq 'bell') { @($ports.bell, $ports.bellWeb) } else { @($ports.brain) }
|
||||
$products[$name] = [ordered]@{
|
||||
enabled = $true; version = "test-$name-v1"; package_root = $packageRoot; environment_file = $environmentPath
|
||||
data_directory = (Join-Path $testRoot "data\$name"); log_directory = (Join-Path $testRoot "logs\$name"); ports = $productPorts
|
||||
browser_origin = $(if ($name -eq 'sense') { "http://127.0.0.1:$($ports.sense)" } elseif ($name -eq 'bell') { "http://127.0.0.1:$($ports.bellWeb)" } else { '' })
|
||||
cookie_name = $(if ($name -eq 'sense') { 'Sense-Admin-Token' } elseif ($name -eq 'bell') { 'Bell-Admin-Token' } else { '' })
|
||||
account_namespace = "$name-accounts"; database_id = $(if ($name -eq 'brain') { '' } else { "${name}_db" }); database_role = $(if ($name -eq 'brain') { '' } else { "${name}_role" })
|
||||
start = [ordered]@{ executable = 'start.ps1'; arguments = @() }; stop = [ordered]@{ executable = 'stop.ps1'; arguments = @() }
|
||||
health = [ordered]@{ kind = 'process'; timeout_seconds = 5 }
|
||||
identities = @([ordered]@{ principal = "yv:${name}:test"; key_id = "${name}-test-key"; private_key_path = $keyPath })
|
||||
}
|
||||
}
|
||||
$manifest = [ordered]@{ schema_version = 'yovision.coordination/v1'; deployment_id = 'test-coordination'; runtime_root = (Join-Path $testRoot 'runtime'); products = $products }
|
||||
Write-Utf8File -Path $manifestPath -Content ($manifest | ConvertTo-Json -Depth 20)
|
||||
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -ValidateOnly
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Manifest validation failed.'
|
||||
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product sense
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Sense-only start failed.'
|
||||
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product sense -Json
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Sense-only status is not healthy.'
|
||||
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product bell -Json
|
||||
Assert-True ($LASTEXITCODE -eq 3) 'Stopped Bell status did not return exit code 3.'
|
||||
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product sense
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Sense-only stop failed.'
|
||||
|
||||
$occupiedPort = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $ports.sense)
|
||||
try {
|
||||
$occupiedPort.Start()
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product sense 2>$null
|
||||
Assert-True ($LASTEXITCODE -eq 1) 'An occupied Sense port was not rejected.'
|
||||
Assert-True (-not (Test-Path -LiteralPath (Join-Path $testRoot 'runtime\state\sense.json'))) 'Occupied-port failure left Sense state behind.'
|
||||
} finally {
|
||||
$occupiedPort.Stop()
|
||||
}
|
||||
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product brain,bell
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Brain+Bell combination start failed.'
|
||||
$bellStatePath = Join-Path $testRoot 'runtime\state\bell.json'
|
||||
$bellStateText = Get-Content -LiteralPath $bellStatePath -Raw -Encoding UTF8
|
||||
$foreignState = $bellStateText | ConvertFrom-Json
|
||||
$foreignState.pid = $PID
|
||||
Write-Utf8File -Path $bellStatePath -Content ($foreignState | ConvertTo-Json -Depth 10)
|
||||
$ownershipStatus = & pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product bell -Json
|
||||
Assert-True ($LASTEXITCODE -eq 3 -and ($ownershipStatus -join "`n") -match 'ownership-mismatch') 'Foreign PID ownership was not diagnosed.'
|
||||
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product bell 2>$null
|
||||
Assert-True ($LASTEXITCODE -eq 1 -and $null -ne (Get-Process -Id $PID -ErrorAction SilentlyContinue)) 'Stop did not protect an unrelated process.'
|
||||
Write-Utf8File -Path $bellStatePath -Content $bellStateText
|
||||
|
||||
$manifestText = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8
|
||||
Write-Utf8File -Path $manifestPath -Content ($manifestText + "`n")
|
||||
$driftStatus = & pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product bell -Json
|
||||
Assert-True ($LASTEXITCODE -eq 3 -and ($driftStatus -join "`n") -match 'manifest-drift') 'Manifest drift was not diagnosed.'
|
||||
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product bell
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Owned Bell could not be stopped after manifest drift.'
|
||||
Write-Utf8File -Path $manifestPath -Content $manifestText
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product bell
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Bell restart after manifest restoration failed.'
|
||||
|
||||
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product brain
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Brain-only stop failed.'
|
||||
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product bell -Json
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Stopping Brain damaged Bell.'
|
||||
|
||||
Write-Utf8File -Path (Join-Path $testRoot 'packages\sense\start.ps1') -Content 'exit 7'
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product sense 2>$null
|
||||
Assert-True ($LASTEXITCODE -eq 1) 'A failing product start did not return exit code 1.'
|
||||
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product bell -Json
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'A Sense start failure damaged Bell.'
|
||||
|
||||
$badManifest = $manifest | ConvertTo-Json -Depth 20 | ConvertFrom-Json -Depth 20
|
||||
$badManifest.products.bell.database_id = $badManifest.products.sense.database_id
|
||||
$badPath = Join-Path $testRoot 'config\invalid-isolation.json'
|
||||
Write-Utf8File -Path $badPath -Content ($badManifest | ConvertTo-Json -Depth 20)
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $badPath -ValidateOnly 2>$null
|
||||
Assert-True ($LASTEXITCODE -eq 1) 'Shared database identity was not rejected.'
|
||||
|
||||
$badPortManifest = $manifest | ConvertTo-Json -Depth 20 | ConvertFrom-Json -Depth 20
|
||||
$badPortManifest.products.bell.ports[0] = $badPortManifest.products.sense.ports[0]
|
||||
$badBellEnvironmentPath = Join-Path $testRoot 'secrets\bell-duplicate-port.env'
|
||||
$badBellEnvironment = Get-Content -LiteralPath $badPortManifest.products.bell.environment_file -Raw -Encoding UTF8
|
||||
$badBellEnvironment = $badBellEnvironment -replace "(?m)^BELL_PORT=\d+$", "BELL_PORT=$($ports.sense)"
|
||||
Write-Utf8File -Path $badBellEnvironmentPath -Content $badBellEnvironment
|
||||
$badPortManifest.products.bell.environment_file = $badBellEnvironmentPath
|
||||
$badPortPath = Join-Path $testRoot 'config\invalid-port-isolation.json'
|
||||
Write-Utf8File -Path $badPortPath -Content ($badPortManifest | ConvertTo-Json -Depth 20)
|
||||
& pwsh.exe -NoProfile -File $startScript -Manifest $badPortPath -ValidateOnly 2>$null
|
||||
Assert-True ($LASTEXITCODE -eq 1) 'Shared product port was not rejected.'
|
||||
|
||||
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product all
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Final stop failed.'
|
||||
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product all -Json
|
||||
Assert-True ($LASTEXITCODE -eq 3) 'Stopped combination did not report non-running status.'
|
||||
foreach ($name in @('sense', 'brain', 'bell')) {
|
||||
$statePath = Join-Path $testRoot "runtime\state\$name.json"
|
||||
Assert-True (-not (Test-Path -LiteralPath $statePath)) "State was not cleaned for $name."
|
||||
}
|
||||
Write-Host 'Coordination smoke passed: validation, port isolation, independent start/stop, combination, failure isolation, ownership and cleanup.'
|
||||
exit 0
|
||||
} finally {
|
||||
if ($manifest -and (Test-Path -LiteralPath $manifestPath)) { & pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product all 2>$null | Out-Null }
|
||||
$resolved = [IO.Path]::GetFullPath($testRoot)
|
||||
if ($resolved.StartsWith($tempParent, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $resolved)) { Remove-Item -LiteralPath $resolved -Recurse -Force }
|
||||
}
|
||||
Reference in New Issue
Block a user