Files

176 lines
12 KiB
PowerShell

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()
}
foreach ($standaloneProduct in @('brain', 'bell')) {
& pwsh.exe -NoProfile -File $startScript -Manifest $manifestPath -Product $standaloneProduct
Assert-True ($LASTEXITCODE -eq 0) "$standaloneProduct standalone start failed."
& pwsh.exe -NoProfile -File $statusScript -Manifest $manifestPath -Product $standaloneProduct -Json
Assert-True ($LASTEXITCODE -eq 0) "$standaloneProduct standalone status is not healthy."
& pwsh.exe -NoProfile -File $stopScript -Manifest $manifestPath -Product $standaloneProduct
Assert-True ($LASTEXITCODE -eq 0) "$standaloneProduct standalone stop failed."
}
& 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 }
}