param( [Parameter(Mandatory=$true)][string]$ServerExe, [Parameter(Mandatory=$true)][string]$PristineFile, [Parameter(Mandatory=$true)][string]$SavedEntry ) $ErrorActionPreference='Stop' $taskRoot=(Resolve-Path (Join-Path $PSScriptRoot '../..')).Path $exe=(Resolve-Path -LiteralPath $ServerExe).Path $pristine=(Resolve-Path -LiteralPath $PristineFile).Path $saved=(Resolve-Path -LiteralPath $SavedEntry).Path $include=(Resolve-Path -LiteralPath (Join-Path (Split-Path -Parent $saved) 'sa/model.arcadia')).Path $runRoot=Join-Path $taskRoot ('.local/timed-bundle-oracle-'+(Get-Date -Format 'yyyyMMdd-HHmmss')) $storePath=Join-Path $runRoot 'store' New-Item -ItemType Directory -Path $runRoot,$storePath | Out-Null $listener=[Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback,0) $listener.Start(); $probePort=$listener.LocalEndpoint.Port; $listener.Stop() $base="http://127.0.0.1:$probePort" $testToken='timed-bundle-oracle-20260906-synthetic-only' $tokenPath=Join-Path $runRoot 'synthetic-token.txt' [IO.File]::WriteAllText($tokenPath,$testToken) $script:ownedServer=$null $script:launch=0 function Start-Oracle { $script:launch++ $script:ownedServer=Start-Process -FilePath $exe -ArgumentList @('--bind',"127.0.0.1:$probePort",'--data-dir',('"'+$storePath+'"'),'--token-file',('"'+$tokenPath+'"')) -WindowStyle Hidden -PassThru -RedirectStandardOutput (Join-Path $runRoot "server-$script:launch.stdout.log") -RedirectStandardError (Join-Path $runRoot "server-$script:launch.stderr.log") for($n=0;$n -lt 40;$n++) { if($script:ownedServer.HasExited){throw 'Oracle server exited'} try{$r=Invoke-WebRequest "$base/health/ready" -TimeoutSec 1;if($r.StatusCode -eq 200){return}}catch{} Start-Sleep -Milliseconds 250 } throw 'Oracle readiness expired' } function Stop-Oracle { if($null -ne $script:ownedServer -and -not $script:ownedServer.HasExited){Stop-Process -InputObject $script:ownedServer;$script:ownedServer.WaitForExit()} $script:ownedServer=$null } function Request-Oracle([string]$label,[string]$method,[string]$path,$body=$null,[int]$expected=200) { $args=@{Uri="$base$path";Method=$method;Headers=@{Authorization="Bearer $testToken"};SkipHttpErrorCheck=$true;TimeoutSec=40} if($null -ne $body){$json=ConvertTo-Json -InputObject $body -Depth 100 -Compress;[IO.File]::WriteAllText((Join-Path $runRoot "$label.request.json"),$json);$args.ContentType='application/json';$args.Body=[Text.Encoding]::UTF8.GetBytes($json)} $r=Invoke-WebRequest @args [IO.File]::WriteAllText((Join-Path $runRoot "$label.response.json"),[string]$r.Content) $tag=[string]($r.Headers.ETag | Select-Object -First 1) @{status=[int]$r.StatusCode;etag=$tag;path=$path} | ConvertTo-Json | Set-Content (Join-Path $runRoot "$label.meta.json") if($r.StatusCode -ne $expected){throw "$label expected $expected, got $($r.StatusCode): $($r.Content)"} return @{data=(ConvertFrom-Json -InputObject $r.Content -AsHashtable);etag=$tag;location=[string]($r.Headers.Location | Select-Object -First 1)} } function Ordered-Json($value) { if($null -eq $value){return $null} if($value -is [Collections.IDictionary]){$map=[ordered]@{};foreach($key in @($value.Keys | Sort-Object)){$map[$key]=Ordered-Json $value[$key]};return $map} if($value -is [Collections.IList]){$items=@(foreach($item in $value){Ordered-Json $item});return ,$items} return $value } function Equal-Graph($left,$right,[string]$label) { $a=ConvertTo-Json -InputObject (Ordered-Json $left) -Depth 100 -Compress $b=ConvertTo-Json -InputObject (Ordered-Json $right) -Depth 100 -Compress if($a -cne $b){[IO.File]::WriteAllText((Join-Path $runRoot "$label.expected.json"),$a);[IO.File]::WriteAllText((Join-Path $runRoot "$label.actual.json"),$b);throw "$label differs"} } try { $hashes=@(Get-FileHash -LiteralPath $exe,$pristine,$saved,$include -Algorithm SHA256 | Select-Object Path,Hash) $hashes | ConvertTo-Json | Set-Content (Join-Path $runRoot 'input-hashes.json') $pristineBundle=@{entry=[IO.Path]::GetFileName($pristine);files=@{}} $pristineBundle.files[$pristineBundle.entry]=[IO.File]::ReadAllText($pristine) $savedBundle=@{entry=[IO.Path]::GetFileName($saved);files=@{}} $savedBundle.files[$savedBundle.entry]=[IO.File]::ReadAllText($saved) $savedBundle.files['sa/model.arcadia']=[IO.File]::ReadAllText($include) Start-Oracle $oldImport=Request-Oracle '01-pristine-import' 'POST' '/api/v1/projects/import-source-project' $pristineBundle 201 $newImport=Request-Oracle '02-saved-import' 'POST' '/api/v1/projects/import-source-project' $savedBundle 201 $old=Request-Oracle '03-pristine-model' 'GET' "$($oldImport.location)/model" $new=Request-Oracle '04-saved-model' 'GET' "$($newImport.location)/model" $bundle=Request-Oracle '05-saved-source-project' 'GET' "$($newImport.location)/source-project" Equal-Graph $savedBundle $bundle.data 'saved-source-bytes' $eventId='49044cb7-5f91-408a-86fc-1cc012eb2972' $oldEventId='f0060906-0000-4000-8000-000000000013' $transitionId='f0060906-0000-4000-8000-000000000033' if($old.data.elements.Contains($eventId)){throw 'Event already exists in pristine model'} $expectedModel=ConvertFrom-Json -InputObject (ConvertTo-Json -InputObject $old.data -Depth 100) -AsHashtable $event=ConvertFrom-Json -InputObject (ConvertTo-Json -InputObject $expectedModel.elements[$oldEventId] -Depth 100) -AsHashtable $event.id=$eventId;$event.alias='after_30_s';$event.name='After 30 s' $event.body.Sa.Shared.Event.body.Time.expression='30 s' $expectedModel.elements[$eventId]=$event $expectedModel.relations[$transitionId].name='Unfinished timed refresh' $expectedModel.relations[$transitionId].body.StateTransition.triggers=@($expectedModel.relations[$transitionId].body.StateTransition.triggers)+@($eventId) Equal-Graph $expectedModel $new.data 'exact-three-deltas' $validation=Request-Oracle '06-saved-validation' 'GET' "$($newImport.location)/validation" if(-not $validation.data.valid -or @($validation.data.issues | Where-Object severity -eq 'error').Count){throw 'Saved graph invalid'} Stop-Oracle Start-Oracle $reopened=Request-Oracle '07-reopened-model' 'GET' "$($newImport.location)/model" $reopenedBundle=Request-Oracle '08-reopened-source-project' 'GET' "$($newImport.location)/source-project" Equal-Graph $new.data $reopened.data 'reopened-model' Equal-Graph $savedBundle $reopenedBundle.data 'reopened-source-bytes' if($new.etag -cne $reopened.etag){throw 'Restart changed ETag'} $afterHashes=@(Get-FileHash -LiteralPath $exe,$pristine,$saved,$include -Algorithm SHA256 | Select-Object Path,Hash) Equal-Graph $hashes $afterHashes 'input-files-unchanged' @{result='passed';evidence=$runRoot;pristine_elements=$old.data.elements.Count;saved_elements=$new.data.elements.Count;relations=$new.data.relations.Count;diagrams=$new.data.diagrams.Count;placements=@($new.data.diagrams.Values | ForEach-Object {$_.nodes.Count});new_event=$new.data.elements[$eventId];transition_before=$old.data.relations[$transitionId];transition_after=$new.data.relations[$transitionId];issues=$validation.data.issues} | ConvertTo-Json -Depth 40 | 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-Oracle}