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 } }