151 lines
10 KiB
PowerShell
151 lines
10 KiB
PowerShell
#requires -Version 7.0
|
|
param([Parameter(Mandatory=$true)][string]$ServerExe)
|
|
$ErrorActionPreference = 'Stop'
|
|
$taskRoot = $PSScriptRoot
|
|
$exe = (Resolve-Path -LiteralPath $ServerExe).Path
|
|
$runRoot = Join-Path $taskRoot 'rest'
|
|
$storePath = Join-Path $runRoot 'store'
|
|
New-Item -ItemType Directory -Path $runRoot,$storePath | Out-Null
|
|
$payload = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'coverage-probe.json') -Raw | ConvertFrom-Json -AsHashtable
|
|
$testToken = 'release-coverage-probe-20260906-local-only'
|
|
$tokenPath = Join-Path $runRoot 'synthetic-token.txt'
|
|
[IO.File]::WriteAllText($tokenPath,$testToken)
|
|
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback,0)
|
|
$listener.Start()
|
|
$probePort = $listener.LocalEndpoint.Port
|
|
$listener.Stop()
|
|
$base = "http://127.0.0.1:$probePort"
|
|
$script:probeProcess = $null
|
|
$script:sequence = 0
|
|
$script:launch = 0
|
|
$codes = @($payload.expected | ForEach-Object { $_.code })
|
|
function Assert-Probe($condition,[string]$message) { if (-not $condition) { throw $message } }
|
|
function Start-ProbeServer {
|
|
$script:launch++
|
|
$startOptions = @{
|
|
WorkingDirectory = Join-Path $PSScriptRoot 'unrelated-cwd'
|
|
FilePath = $exe
|
|
ArgumentList = @('--bind',"127.0.0.1:$probePort",'--data-dir',('"'+$storePath+'"'),'--token-file',('"'+$tokenPath+'"'))
|
|
PassThru = $true
|
|
RedirectStandardOutput = Join-Path $runRoot "server-$script:launch.stdout.log"
|
|
RedirectStandardError = Join-Path $runRoot "server-$script:launch.stderr.log"
|
|
}
|
|
# WindowStyle is Windows-only; Unix keeps the child attached to this probe.
|
|
if ($IsWindows) { $startOptions.WindowStyle = 'Hidden' }
|
|
$script:probeProcess = Start-Process @startOptions
|
|
@{pid=$script:probeProcess.Id;exe=$exe;arguments=$startOptions.ArgumentList;cwd=$startOptions.WorkingDirectory;PATH=$env:PATH;started=(Get-Date -Format o)} | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $runRoot "server-$script:launch.launch.json")
|
|
for ($attempt = 0; $attempt -lt 40; $attempt++) {
|
|
if ($script:probeProcess.HasExited) { throw 'Probe server exited before readiness' }
|
|
try { $ready=Invoke-WebRequest "$base/health/ready" -TimeoutSec 1; if ($ready.StatusCode -eq 200) { return } } catch {}
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
throw 'Probe server did not become ready'
|
|
}
|
|
function Stop-ProbeServer {
|
|
if ($null -ne $script:probeProcess -and -not $script:probeProcess.HasExited) {
|
|
Stop-Process -InputObject $script:probeProcess
|
|
$script:probeProcess.WaitForExit()
|
|
}
|
|
if ($null -ne $script:probeProcess) { @{pid=$script:probeProcess.Id;has_exited=$script:probeProcess.HasExited;exit_code=$script:probeProcess.ExitCode;stopped=(Get-Date -Format o);method='owned probe Stop-Process after assertions'} | ConvertTo-Json | Set-Content (Join-Path $runRoot "server-$script:launch.stop.json") }
|
|
$script:probeProcess = $null
|
|
}
|
|
function Call-Probe([string]$name,[string]$method,[string]$path,$body=$null,[string]$etag='',[int]$expectedStatus=200) {
|
|
$script:sequence++
|
|
$stem = Join-Path $runRoot ('{0:D2}-{1}' -f $script:sequence,$name)
|
|
$headers = @{Authorization="Bearer $testToken"}
|
|
if ($etag) { $headers['If-Match']=$etag }
|
|
$request = @{Uri="$base$path";Method=$method;Headers=$headers;SkipHttpErrorCheck=$true;TimeoutSec=40}
|
|
if ($null -ne $body) {
|
|
$json=ConvertTo-Json -InputObject $body -Depth 100 -Compress
|
|
[IO.File]::WriteAllText("$stem.request.json",$json)
|
|
$request.ContentType='application/json'
|
|
$request.Body=[Text.Encoding]::UTF8.GetBytes($json)
|
|
}
|
|
$response=Invoke-WebRequest @request
|
|
$content=[string]$response.Content
|
|
[IO.File]::WriteAllText("$stem.response.txt",$content)
|
|
$tag=[string]($response.Headers.ETag | Select-Object -First 1)
|
|
@{method=$method;path=$path;status=[int]$response.StatusCode;etag=$tag} | ConvertTo-Json | Set-Content -LiteralPath "$stem.meta.json"
|
|
Assert-Probe ($response.StatusCode -eq $expectedStatus) "$name expected $expectedStatus; got $($response.StatusCode): $content"
|
|
$data=$null
|
|
try { $data=ConvertFrom-Json -InputObject $content -AsHashtable } catch {}
|
|
return @{content=$content;data=$data;etag=$tag;headers=$response.Headers}
|
|
}
|
|
function Tuples($issues) {
|
|
return (@($issues | Where-Object { $_.code -in $codes } | ForEach-Object { "$($_.code)|$($_.object_id)|$($_.field)" } | Sort-Object) -join "`n")
|
|
}
|
|
function Assert-Coverage($response,[bool]$located=$false) {
|
|
$found=@($response.data.issues | Where-Object { $_.code -in $codes })
|
|
Assert-Probe ((Tuples $found) -ceq (Tuples $payload.expected)) 'Five exact coverage tuples differ'
|
|
Assert-Probe (@($response.data.issues | Where-Object severity -eq 'error').Count -eq 0) 'Unexpected structural error'
|
|
foreach ($issue in $found) {
|
|
Assert-Probe ($issue.severity -eq 'warning') 'Coverage is not warning severity'
|
|
if ($located) {
|
|
Assert-Probe ($issue.path -eq 'work.arcadia' -and $issue.line -ge 1 -and $issue.column -ge 1 -and $null -ne $issue.span) 'Missing submitted-source location'
|
|
}
|
|
}
|
|
}
|
|
function Assert-Bundle($actual,$expected) {
|
|
Assert-Probe ($actual.entry -ceq $expected.entry -and $actual.files.Count -eq $expected.files.Count) 'Bundle entry/file count changed'
|
|
foreach ($key in $expected.files.Keys) { Assert-Probe ($actual.files[$key] -ceq $expected.files[$key]) "Source bytes changed: $key" }
|
|
}
|
|
try {
|
|
@{exe=$exe;sha256=(Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash;port=$probePort;started=(Get-Date -Format o)} | ConvertTo-Json | Set-Content (Join-Path $runRoot 'run.json')
|
|
Start-ProbeServer
|
|
$created=Call-Probe 'create' 'POST' '/api/v1/projects' @{name='Coverage probe host'} '' 201
|
|
$path=[string]($created.headers.Location | Select-Object -First 1)
|
|
Assert-Probe ($path -like '/api/v1/projects/*') 'Create omitted resource Location'
|
|
$before=Call-Probe 'before-model' 'GET' "$path/model"
|
|
$beforeBundle=Call-Probe 'before-bundle' 'GET' "$path/source-project"
|
|
$candidate=Call-Probe 'incomplete-candidate' 'POST' "$path/validation" @{format='source-project';source_project=$payload.incomplete}
|
|
Assert-Probe $candidate.data.valid 'Incomplete model should be valid'
|
|
Assert-Coverage $candidate $true
|
|
$after=Call-Probe 'candidate-no-commit-model' 'GET' "$path/model"
|
|
$afterBundle=Call-Probe 'candidate-no-commit-bundle' 'GET' "$path/source-project"
|
|
Assert-Probe ($after.content -ceq $before.content -and $after.etag -ceq $before.etag) 'Candidate changed accepted model/revision'
|
|
Assert-Bundle $afterBundle.data $beforeBundle.data
|
|
$committed=Call-Probe 'commit-incomplete' 'PUT' "$path/source-project" $payload.incomplete $before.etag
|
|
Assert-Coverage $committed $true
|
|
Assert-Probe ($committed.data.changed -and $committed.etag -cne $before.etag) 'Warning-only commit did not advance revision'
|
|
$model=Call-Probe 'incomplete-model' 'GET' "$path/model"
|
|
$bundle=Call-Probe 'incomplete-bundle' 'GET' "$path/source-project"
|
|
Assert-Bundle $bundle.data $payload.incomplete
|
|
$source=Call-Probe 'incomplete-canonical' 'GET' "$path/source"
|
|
$validation=Call-Probe 'incomplete-validation' 'GET' "$path/validation"
|
|
Assert-Coverage $validation
|
|
$jsonCheck=Call-Probe 'json-parity' 'POST' "$path/validation" @{format='json';model=$model.data}
|
|
$textCheck=Call-Probe 'text-parity' 'POST' "$path/validation" @{format='arcadia';source=$source.content}
|
|
Assert-Coverage $jsonCheck
|
|
Assert-Coverage $textCheck
|
|
Assert-Probe ($jsonCheck.data.valid -and $textCheck.data.valid) 'JSON/text parity candidate invalid'
|
|
$invalid=Call-Probe 'invalid-candidate' 'POST' "$path/validation" @{format='source-project';source_project=$payload.invalid}
|
|
Assert-Probe (-not $invalid.data.valid -and @($invalid.data.issues | Where-Object { $_.code -eq 'invalid_endpoints' -and $_.severity -eq 'error' }).Count -ge 1) 'Invalid endpoint not diagnosed'
|
|
Assert-Probe ((Tuples $invalid.data.issues) -eq '') 'Structural error did not gate coverage'
|
|
$null=Call-Probe 'invalid-write-rejected' 'PUT' "$path/source-project" $payload.invalid $model.etag 422
|
|
$unchanged=Call-Probe 'rejection-model' 'GET' "$path/model"
|
|
$unchangedBundle=Call-Probe 'rejection-bundle' 'GET' "$path/source-project"
|
|
Assert-Probe ($unchanged.content -ceq $model.content -and $unchanged.etag -ceq $model.etag) 'Rejected write changed model/revision'
|
|
Assert-Bundle $unchangedBundle.data $bundle.data
|
|
$fixed=Call-Probe 'repair-write' 'PUT' "$path/source-project" $payload.repaired $model.etag
|
|
Assert-Probe ((Tuples $fixed.data.issues) -eq '') 'Repair left coverage warnings'
|
|
$fixedModel=Call-Probe 'repaired-model' 'GET' "$path/model"
|
|
$fixedBundle=Call-Probe 'repaired-bundle' 'GET' "$path/source-project"
|
|
Assert-Bundle $fixedBundle.data $payload.repaired
|
|
$fixedValidation=Call-Probe 'repaired-validation' 'GET' "$path/validation"
|
|
Stop-ProbeServer
|
|
Start-ProbeServer
|
|
$reopened=Call-Probe 'restart-model' 'GET' "$path/model"
|
|
$reopenedBundle=Call-Probe 'restart-bundle' 'GET' "$path/source-project"
|
|
$reopenedValidation=Call-Probe 'restart-validation' 'GET' "$path/validation"
|
|
Assert-Probe ($reopened.content -ceq $fixedModel.content -and $reopened.etag -ceq $fixedModel.etag) 'Restart changed model/revision'
|
|
Assert-Bundle $reopenedBundle.data $fixedBundle.data
|
|
Assert-Probe ($reopenedValidation.content -ceq $fixedValidation.content) 'Restart changed validation'
|
|
$again=Call-Probe 'restore-incomplete-by-replacement' 'PUT' "$path/source-project" $payload.incomplete $reopened.etag
|
|
Assert-Coverage $again $true
|
|
@{result='passed';requests=$script:sequence;resource=$path;initial_revision=$committed.data.revision;repaired_revision=$fixed.data.revision;final_revision=$again.data.revision;repair_issues=$fixed.data.issues;evidence=$runRoot} | ConvertTo-Json -Depth 20 | Set-Content (Join-Path $runRoot 'result.json')
|
|
Get-Content (Join-Path $runRoot 'result.json')
|
|
} catch {
|
|
$_ | Out-String | Set-Content (Join-Path $runRoot 'failure.txt')
|
|
throw
|
|
} finally { Stop-ProbeServer }
|