merge: 完成三项目协调隔离 E2E (#155)
用户已于 2026-08-31 明确验收通过 #155。
This commit was merged in pull request #170.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
pwsh.exe -NoProfile -File "%~dp0run-coordination-e2e.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,191 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$PostgresBin = 'D:\pgsql17\bin',
|
||||
[string]$Python = '',
|
||||
[switch]$SkipIndependentProductE2E,
|
||||
[switch]$KeepTemporary
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$temporaryRoot = [IO.Path]::GetFullPath((Join-Path ([IO.Path]::GetTempPath()) ('yovision-coordination-e2e-' + [guid]::NewGuid().ToString('N'))))
|
||||
$postgresData = Join-Path $temporaryRoot 'postgres'
|
||||
$postgresLog = Join-Path $temporaryRoot 'postgres.log'
|
||||
$postgresStarted = $false
|
||||
$savedEnvironment = @{}
|
||||
$sensitiveValues = [Collections.Generic.List[string]]::new()
|
||||
|
||||
function Get-FreeTcpPort {
|
||||
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
|
||||
try { $listener.Start(); return ([Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() }
|
||||
}
|
||||
|
||||
function Wait-Tcp([int]$Port, [bool]$Open, [int]$Attempts = 120) {
|
||||
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
|
||||
$client = [Net.Sockets.TcpClient]::new()
|
||||
try { $connected = $client.ConnectAsync('127.0.0.1', $Port).Wait(250) -and $client.Connected } catch { $connected = $false } finally { $client.Dispose() }
|
||||
if ($connected -eq $Open) { return }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
throw "TCP port $Port did not reach open=$Open"
|
||||
}
|
||||
|
||||
function New-RandomName([string]$Prefix) {
|
||||
return $Prefix + '_' + [guid]::NewGuid().ToString('N').Substring(0, 12)
|
||||
}
|
||||
|
||||
function New-RandomSecret {
|
||||
$buffer = New-Object byte[] 48
|
||||
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
|
||||
try { $generator.GetBytes($buffer) } finally { $generator.Dispose() }
|
||||
return [Convert]::ToBase64String($buffer).Replace('+', 'A').Replace('/', 'B')
|
||||
}
|
||||
|
||||
function Set-TestEnvironment([string]$Name, [string]$Value, [bool]$Sensitive = $false) {
|
||||
if (-not $script:savedEnvironment.ContainsKey($Name)) {
|
||||
$script:savedEnvironment[$Name] = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable($Name, $Value, 'Process')
|
||||
if ($Sensitive) { $script:sensitiveValues.Add($Value) }
|
||||
}
|
||||
|
||||
function Invoke-Checked {
|
||||
param([string]$Name, [string]$WorkingDirectory, [scriptblock]$Command)
|
||||
Write-Host "[coordination-e2e] $Name"
|
||||
Push-Location $WorkingDirectory
|
||||
try {
|
||||
& $Command
|
||||
if ($LASTEXITCODE -ne 0) { throw "$Name failed with exit code $LASTEXITCODE" }
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
function Assert-NoSecretInLogs {
|
||||
$logs = @(Get-ChildItem -LiteralPath $temporaryRoot -File -Recurse -ErrorAction SilentlyContinue)
|
||||
foreach ($log in $logs) {
|
||||
$stream = [IO.File]::Open($log.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete)
|
||||
try {
|
||||
$reader = [IO.StreamReader]::new($stream, [Text.Encoding]::UTF8, $true)
|
||||
try { $content = $reader.ReadToEnd() } finally { $reader.Dispose() }
|
||||
} finally { $stream.Dispose() }
|
||||
foreach ($secret in $sensitiveValues) {
|
||||
if ($secret.Length -ge 8 -and $content.Contains($secret)) {
|
||||
throw "Temporary log exposed a generated E2E secret: $($log.Name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
|
||||
|
||||
try {
|
||||
foreach ($tool in @('initdb.exe', 'pg_ctl.exe', 'createdb.exe', 'psql.exe')) {
|
||||
$path = Join-Path $PostgresBin $tool
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Required PostgreSQL tool not found: $path" }
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Python)) {
|
||||
$candidate = Join-Path $repositoryRoot 'Brain\.venv\Scripts\python.exe'
|
||||
$Python = if (Test-Path -LiteralPath $candidate -PathType Leaf) { $candidate } else { 'python.exe' }
|
||||
}
|
||||
$contractEnvironment = Join-Path $temporaryRoot 'contract-venv'
|
||||
& $Python -m venv $contractEnvironment
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not create the isolated contract-test environment.' }
|
||||
$contractPython = Join-Path $contractEnvironment 'Scripts\python.exe'
|
||||
$env:PIP_DISABLE_PIP_VERSION_CHECK = '1'
|
||||
& $contractPython -m pip install --quiet -r (Join-Path $repositoryRoot 'contracts\tests\source-config-v1\requirements.txt') 'cryptography==50.0.1'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not install the pinned contract-test dependencies.' }
|
||||
|
||||
$postgresPort = Get-FreeTcpPort
|
||||
if ($postgresPort -eq 5432) { throw 'Coordination E2E refuses the default PostgreSQL port.' }
|
||||
$clusterUser = New-RandomName 'yvcoord'
|
||||
$senseRole = New-RandomName 'sense_owner'
|
||||
$bellRole = New-RandomName 'bell_owner'
|
||||
$senseDatabase = New-RandomName 'sense_e2e'
|
||||
$bellDatabase = New-RandomName 'bell_e2e'
|
||||
|
||||
& (Join-Path $PostgresBin 'initdb.exe') -D $postgresData -U $clusterUser -A trust --encoding=UTF8 --no-locale | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Isolated PostgreSQL initdb failed.' }
|
||||
$startArguments = "-D `"$postgresData`" -l `"$postgresLog`" -o `"-p $postgresPort -h 127.0.0.1`" start"
|
||||
Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList $startArguments -RedirectStandardOutput (Join-Path $temporaryRoot 'pg-ctl.out.log') -RedirectStandardError (Join-Path $temporaryRoot 'pg-ctl.err.log') -WindowStyle Hidden | Out-Null
|
||||
Wait-Tcp -Port $postgresPort -Open $true
|
||||
$postgresStarted = $true
|
||||
|
||||
$psql = Join-Path $PostgresBin 'psql.exe'
|
||||
foreach ($role in @($senseRole, $bellRole)) {
|
||||
& $psql -X -h 127.0.0.1 -p $postgresPort -U $clusterUser -d postgres -v ON_ERROR_STOP=1 -c "CREATE ROLE $role LOGIN;" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not create isolated role $role" }
|
||||
}
|
||||
& (Join-Path $PostgresBin 'createdb.exe') -h 127.0.0.1 -p $postgresPort -U $clusterUser -O $senseRole $senseDatabase
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not create isolated Sense database.' }
|
||||
& (Join-Path $PostgresBin 'createdb.exe') -h 127.0.0.1 -p $postgresPort -U $clusterUser -O $bellRole $bellDatabase
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not create isolated Bell database.' }
|
||||
|
||||
$senseDsn = "host=127.0.0.1 port=$postgresPort user=$senseRole dbname=$senseDatabase sslmode=disable"
|
||||
$bellDsn = "host=127.0.0.1 port=$postgresPort user=$bellRole dbname=$bellDatabase sslmode=disable"
|
||||
if ($senseDsn -eq $bellDsn -or $senseRole -eq $bellRole -or $senseDatabase -eq $bellDatabase) { throw 'Sense and Bell isolation invariant failed.' }
|
||||
|
||||
Set-TestEnvironment 'GOTOOLCHAIN' 'go1.26.5'
|
||||
Set-TestEnvironment 'PYTHONDONTWRITEBYTECODE' '1'
|
||||
Set-TestEnvironment 'SENSE_OUTBOX_TEST_DATABASE_URL' $senseDsn
|
||||
Set-TestEnvironment 'BELL_DATABASE_URL' $bellDsn
|
||||
Set-TestEnvironment 'BELL_EVENT_INGRESS_TEST_DATABASE_URL' $bellDsn
|
||||
Set-TestEnvironment 'BELL_RULE_ALERT_TEST_DATABASE_URL' $bellDsn
|
||||
Set-TestEnvironment 'BELL_ALERT_LIFECYCLE_TEST_DATABASE_URL' $bellDsn
|
||||
Set-TestEnvironment 'BELL_JWT_SECRET' (New-RandomSecret) $true
|
||||
Set-TestEnvironment 'BELL_BOOTSTRAP_USERNAME' (New-RandomName 'coord_admin')
|
||||
Set-TestEnvironment 'BELL_BOOTSTRAP_PASSWORD' (New-RandomSecret) $true
|
||||
Set-TestEnvironment 'BELL_RULE_ALERT_OPERATOR_PASSWORD' (New-RandomSecret) $true
|
||||
Set-TestEnvironment 'BELL_HOST' '127.0.0.1'
|
||||
Set-TestEnvironment 'BELL_PORT' (Get-FreeTcpPort).ToString()
|
||||
|
||||
Invoke-Checked 'Bell formal migrations' (Join-Path $repositoryRoot 'Bell\server') { go run . migrate -c config/settings.demo.yml *> (Join-Path $temporaryRoot 'bell-migrate.log') }
|
||||
|
||||
Invoke-Checked 'source-config v1 contract' $repositoryRoot { & $contractPython -m unittest discover -s contracts/tests/source-config-v1 -p 'test_*.py' -v }
|
||||
Invoke-Checked 'runtime-status v1 contract' $repositoryRoot { & $contractPython contracts/tests/runtime-status-v1/test_contract.py }
|
||||
Invoke-Checked 'machine-identity v1 cross-language contract' $repositoryRoot { & $contractPython contracts/tests/machine-identity-v1/test_contract.py }
|
||||
Invoke-Checked 'events v1 contract' $repositoryRoot { & $contractPython contracts/tests/events-v1/test_contract.py }
|
||||
Invoke-Checked 'evidence v1 contract' $repositoryRoot { & $contractPython contracts/tests/evidence-v1/test_contract.py }
|
||||
|
||||
Invoke-Checked 'Sense source/status integration' (Join-Path $repositoryRoot 'Sense\tests\integration\brain_control') { go test . -count=1 -v }
|
||||
Invoke-Checked 'Sense Brain-event/evidence/Outbox integration' (Join-Path $repositoryRoot 'Sense\tests\integration\bell_connector') { go test . -count=1 -v }
|
||||
Invoke-Checked 'Sense PostgreSQL Outbox recovery' (Join-Path $repositoryRoot 'Sense\server') { go test ./app/sense/outbox -count=1 -v }
|
||||
Invoke-Checked 'Brain source/status connector and anonymous event export' $repositoryRoot {
|
||||
& $Python -m pytest Brain/tests/integration/sense_control Brain/tests/integration/event_export -q
|
||||
}
|
||||
Invoke-Checked 'Bell ingress and evidence degradation' (Join-Path $repositoryRoot 'Bell\server') { go test ./tests/integration/event_ingress -count=1 -v }
|
||||
Invoke-Checked 'Bell rule and alert projection' (Join-Path $repositoryRoot 'Bell\server') { go test ./tests/bell_rule_alert -count=1 -v }
|
||||
Invoke-Checked 'Bell alert lifecycle' (Join-Path $repositoryRoot 'Bell\server') { go test ./tests/bell_alert_lifecycle -count=1 -v }
|
||||
|
||||
$identityFacts = (& $psql -X -h 127.0.0.1 -p $postgresPort -U $clusterUser -d postgres -tAc "select datname||':'||pg_get_userbyid(datdba) from pg_database where datname in ('$senseDatabase','$bellDatabase') order by datname;")
|
||||
if (@($identityFacts).Count -ne 2 -or ($identityFacts -join '|') -notmatch [regex]::Escape($senseRole) -or ($identityFacts -join '|') -notmatch [regex]::Escape($bellRole)) {
|
||||
throw 'PostgreSQL ownership isolation evidence is incomplete.'
|
||||
}
|
||||
|
||||
if (-not $SkipIndependentProductE2E) {
|
||||
Invoke-Checked 'Sense independent isolated E2E regression' $repositoryRoot { & (Join-Path $repositoryRoot 'Sense\tests\e2e\run-isolated-e2e.ps1') -PostgresBin $PostgresBin }
|
||||
Invoke-Checked 'Brain independent test regression' $repositoryRoot { & $Python -m pytest Brain/tests -q }
|
||||
Invoke-Checked 'Bell independent isolated E2E regression' $repositoryRoot { & (Join-Path $repositoryRoot 'Bell\tests\e2e\run-isolated-e2e.ps1') -PostgresBin $PostgresBin }
|
||||
}
|
||||
|
||||
Assert-NoSecretInLogs
|
||||
Write-Host "COORDINATION_E2E passed: versioned contracts, source/status, anonymous event, durable Outbox recovery, Bell Receipt/Event/Alert lifecycle, identity/replay/conflict/evidence faults, separate Sense/Bell databases. postgres_port=$postgresPort"
|
||||
} finally {
|
||||
if ($postgresStarted) {
|
||||
Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList "-D `"$postgresData`" -m fast stop" -RedirectStandardOutput (Join-Path $temporaryRoot 'pg-stop.out.log') -RedirectStandardError (Join-Path $temporaryRoot 'pg-stop.err.log') -WindowStyle Hidden -Wait | Out-Null
|
||||
try { Wait-Tcp -Port $postgresPort -Open $false -Attempts 40 } catch {}
|
||||
}
|
||||
foreach ($entry in $savedEnvironment.GetEnumerator()) {
|
||||
[Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process')
|
||||
}
|
||||
if ($KeepTemporary) {
|
||||
Write-Host "Kept coordination E2E directory: $temporaryRoot"
|
||||
} elseif (Test-Path -LiteralPath $temporaryRoot) {
|
||||
$resolved = [IO.Path]::GetFullPath($temporaryRoot)
|
||||
$tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||
if (-not $resolved.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or -not ([IO.Path]::GetFileName($resolved)).StartsWith('yovision-coordination-e2e-')) {
|
||||
throw "Refusing unsafe temporary cleanup: $resolved"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolved -Recurse -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Coordination E2E
|
||||
|
||||
Run the complete isolated coordination acceptance from the repository root:
|
||||
|
||||
```powershell
|
||||
pwsh scripts/e2e/coordination/run-coordination-e2e.ps1
|
||||
```
|
||||
|
||||
The runner creates a temporary PostgreSQL cluster on a dynamic loopback port,
|
||||
uses distinct random owners and databases for Sense and Bell, exercises the
|
||||
frozen contracts and the three connector chains, runs each product's existing
|
||||
independent regression, checks generated secrets are absent from temporary
|
||||
logs, and removes only the processes and directory it created.
|
||||
|
||||
`-SkipIndependentProductE2E` is intended only for local harness debugging and
|
||||
does not satisfy issue #155 acceptance. `-KeepTemporary` preserves disposable
|
||||
diagnostics after a failed run; the directory contains test-only generated
|
||||
credentials and must not be committed or shared.
|
||||
Reference in New Issue
Block a user