183 lines
18 KiB
PowerShell
183 lines
18 KiB
PowerShell
#Requires -Version 7.0
|
|
param([Parameter(Mandatory=$true)][ValidateSet('list','focused','all','clippy','build')][string]$Stage)
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference='Stop'
|
|
$PSNativeCommandUseErrorActionPreference=$false
|
|
$taskWorkspace='C:\Users\User\Documents\Git\ARCADIA-Studio\.local\scenario-spacing-ytr83r-20260913'
|
|
$taskTarget='C:\Users\User\Documents\Git\ARCADIA-Studio\target'
|
|
$taskPin='b10b28cefd1d0794073ba241a0c8de9a83e85f68'
|
|
if($taskPin -notmatch '^[0-9a-f]{40}$'){throw 'Final reviewed source pin and captures are not prepared; do not run this template.'}
|
|
$toolchainEnv='C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\env.ps1'
|
|
function Hash-Input([string]$Path){(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()}
|
|
function Same-Names($Left,$Right){$a=@($Left|Sort-Object);$b=@($Right|Sort-Object);return $a.Count -eq $b.Count -and @(Compare-Object $a $b).Count -eq 0}
|
|
$taskLog=Join-Path $PSScriptRoot ($Stage+'.log')
|
|
$taskReceipt=Join-Path $PSScriptRoot ($Stage+'-receipt.json')
|
|
if((Test-Path -LiteralPath $taskLog)-or(Test-Path -LiteralPath $taskReceipt)){throw 'Do not replace evidence; prepare a new gate directory for retries.'}
|
|
$cache=Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'cache-policy.json')|ConvertFrom-Json
|
|
if($cache.base -cne $taskPin -or -not $cache.retainExistingTarget -or $cache.cleanRequested){throw 'Wrong reviewed cache policy.'}
|
|
. (Join-Path $PSScriptRoot 'binary-inputs.ps1')
|
|
. (Join-Path $PSScriptRoot 'batch-helpers.ps1')
|
|
$batchPlan=Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'batches.json')|ConvertFrom-Json
|
|
$selection=Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'expected-tests.json')|ConvertFrom-Json
|
|
$expected=@($selection.tests|Sort-Object -Unique)
|
|
if($selection.source_commit -cne $taskPin -or $selection.expected_desktop_count -ne 1095 -or @($selection.desktop_tests).Count -ne 1095 -or @($selection.desktop_tests|Sort-Object -Unique).Count -ne 1095 -or $selection.expected_count -ne 301 -or $expected.Count -ne 301 -or @($selection.tests).Count -ne 301){throw 'Wrong exact focused membership.'}
|
|
$expectedCount=301
|
|
$commonFiles=@('run.ps1','capture.ps1','binary-inputs.ps1','batch-helpers.ps1','batches.json','expected-tests.json','expected-inputs.json','enumeration.json','external-inputs.json','inputs-complete-before.json','cache-policy.json','source-provenance.json','README.md','verify-preparation.ps1','preparation-verification.json','strict-preflight.ps1','strict-preflight-receipt.json','strict-preflight.log','inputs-complete-strict-preflight-before.json','inputs-complete-strict-preflight-after.json','toolchain-versions.json')
|
|
$boundScripts=@{}
|
|
foreach($file in $commonFiles){$boundScripts[$file]=Hash-Input (Join-Path $PSScriptRoot $file)}
|
|
$previous=@{focused='list';all='focused';clippy='all';build='clippy'}
|
|
$preflightPath=Join-Path $PSScriptRoot 'strict-preflight-receipt.json'
|
|
$preflight=Get-Content -Raw -LiteralPath $preflightPath|ConvertFrom-Json
|
|
if($preflight.stage -cne 'strict-preflight' -or $preflight.base -cne $taskPin -or $preflight.workspace -ine $taskWorkspace -or $preflight.target -ine $taskTarget -or $preflight.cargoExitCode -ne 0 -or -not $preflight.passed -or -not $preflight.inputsUnchanged -or $null -ne $preflight.postflightFailure){throw 'Strict preflight has not passed for this exact source'}
|
|
foreach($property in $preflight.boundScriptAndInputHashes.PSObject.Properties){if((Hash-Input (Join-Path $PSScriptRoot $property.Name)) -cne $property.Value){throw ('Preflight transitive binding changed: '+$property.Name)}}
|
|
if((Hash-Input (Join-Path $PSScriptRoot 'strict-preflight.log')) -cne $preflight.rawLogSha256 -or (Hash-Input (Join-Path $PSScriptRoot 'inputs-complete-strict-preflight-before.json')) -cne $preflight.beforeCaptureSha256 -or (Hash-Input (Join-Path $PSScriptRoot 'inputs-complete-strict-preflight-after.json')) -cne $preflight.afterCaptureSha256 -or (Hash-Input (Join-Path $PSScriptRoot 'toolchain-versions.json')) -cne $preflight.toolchainVersionsSha256){throw 'Preflight output/capture binding changed'}
|
|
$priorBinding=if($Stage -eq 'list'){[ordered]@{stage='strict-preflight';receiptSha256=(Hash-Input $preflightPath);rawLogSha256=$preflight.rawLogSha256}}else{$null}
|
|
$prior=$preflight
|
|
if($previous.ContainsKey($Stage)){
|
|
$priorName=$previous[$Stage]
|
|
$priorPath=Join-Path $PSScriptRoot ($priorName+'-receipt.json')
|
|
$prior=Get-Content -Raw -LiteralPath $priorPath|ConvertFrom-Json
|
|
if($prior.stage -cne $priorName -or $prior.base -cne $taskPin -or $prior.workspace -ine $taskWorkspace -or $prior.target -ine $taskTarget -or $prior.cargoExitCode -ne 0 -or -not $prior.selectionValid -or -not $prior.inputsUnchanged -or -not $prior.testBinaryBindingValid -or $null -ne $prior.postflightFailure){throw 'Exact prior sequential stage did not pass.'}
|
|
foreach($property in $prior.boundScriptAndInputHashes.PSObject.Properties){if((Hash-Input (Join-Path $PSScriptRoot $property.Name)) -cne $property.Value){throw ('Prior transitive binding changed: '+$property.Name)};$boundScripts[$property.Name]=$property.Value}
|
|
if((Hash-Input (Join-Path $PSScriptRoot ($priorName+'.log'))) -cne $prior.rawLogSha256){throw 'Prior raw output changed.'}
|
|
foreach($side in @('before','after')){
|
|
$captureName='inputs-complete-'+$priorName+'-'+$side+'.json'
|
|
$captureHash=Hash-Input (Join-Path $PSScriptRoot $captureName)
|
|
$expectedHash=if($side -eq 'before'){$prior.beforeCaptureSha256}else{$prior.afterCaptureSha256}
|
|
if($captureHash -cne $expectedHash){throw 'Prior source capture changed'}
|
|
$boundScripts[$captureName]=$captureHash
|
|
}
|
|
$boundScripts[$priorName+'-receipt.json']=Hash-Input $priorPath
|
|
$boundScripts[$priorName+'.log']=$prior.rawLogSha256
|
|
$priorBinding=[ordered]@{stage=$priorName;receiptSha256=(Hash-Input $priorPath);rawLogSha256=$prior.rawLogSha256}
|
|
}
|
|
Push-Location -LiteralPath $taskWorkspace
|
|
try {
|
|
& (Join-Path $PSScriptRoot 'capture.ps1') -Stage ($Stage+'-before')
|
|
. $toolchainEnv
|
|
$env:CARGO_TARGET_DIR=$taskTarget
|
|
$env:CARGO_TERM_COLOR='never'
|
|
$PSNativeCommandUseErrorActionPreference=$false
|
|
$versionPath=Join-Path $PSScriptRoot 'toolchain-versions.json'
|
|
$versionsHash=Hash-Input $versionPath
|
|
if($priorBinding -and $prior.toolchainVersionsSha256 -cne $versionsHash){throw 'Toolchain version receipt changed.'}
|
|
Assert-BatchPlan $batchPlan $expected (Get-Command cargo).Source
|
|
$batchReceipts=@();$batchesValid=$true
|
|
$taskArgs=switch($Stage){
|
|
'list' {@('test','-p','oa-desktop','--all-targets','--locked','--offline','--','--list')}
|
|
'focused' {@('test','-p','oa-desktop','--bin','arcadia-studio','--locked','--offline','--','--exact')+$expected+@('--test-threads=1')}
|
|
'all' {@('test','-p','oa-desktop','--all-targets','--locked','--offline','--','--test-threads=1')}
|
|
'clippy' {@('clippy','-p','oa-desktop','-p','oa-server','--all-targets','--locked','--offline','--','-D','warnings')}
|
|
'build' {@('build','-p','oa-desktop','--bin','arcadia-studio','--locked','--offline')}
|
|
}
|
|
$taskStart=[DateTime]::UtcNow.ToString('o')
|
|
$testExecutablesBefore=@()
|
|
if($Stage -ne 'list'){
|
|
$testExecutablesBefore=@($prior.testExecutables|ForEach-Object {Get-GateBinary $_.path $taskTarget})
|
|
if(-not (Same-GateBinaries @($prior.testExecutables) $testExecutablesBefore)){throw 'Previously bound test executable changed before this stage.'}
|
|
}
|
|
if($Stage -eq 'focused'){
|
|
$taskExit=0;$lastBatch=$null
|
|
foreach($batch in $batchPlan.batches){
|
|
$batchId=$batch.id;$batchLog=Join-Path $PSScriptRoot ($batchId+'.log');$batchReceipt=Join-Path $PSScriptRoot ($batchId+'-receipt.json')
|
|
if((Test-Path -LiteralPath $batchLog)-or(Test-Path -LiteralPath $batchReceipt)){throw 'Preserve existing batch evidence'}
|
|
& (Join-Path $PSScriptRoot 'capture.ps1') -Stage ($batchId+'-before')
|
|
$batchBefore=@($testExecutablesBefore|ForEach-Object {Get-GateBinary $_.path $taskTarget})
|
|
if(-not (Same-GateBinaries $testExecutablesBefore $batchBefore)){throw 'Listed executable changed before batch'}
|
|
$batchArgs=@($batchPlan.base_arguments)+@($batch.tests)+@($batchPlan.tail_arguments)
|
|
$batchStart=[DateTime]::UtcNow.ToString('o');$batchFailure=$null;$batchExit=-1
|
|
try{& cargo @batchArgs 2>&1|Tee-Object -FilePath $batchLog;$batchExit=$LASTEXITCODE}catch{$batchFailure=$_.Exception.Message;if(-not(Test-Path -LiteralPath $batchLog)){[IO.File]::WriteAllText($batchLog,$batchFailure)}}
|
|
$batchEnd=[DateTime]::UtcNow.ToString('o');$batchText=Get-Content -Raw -LiteralPath $batchLog
|
|
$batchResult=Read-BatchResults $batchText;$batchValid=$false;$batchInputs=$false;$batchAfter=@();$batchBinaryValid=$false
|
|
try{
|
|
& (Join-Path $PSScriptRoot 'capture.ps1') -Stage ($batchId+'-after')
|
|
foreach($file in $boundScripts.Keys){if((Hash-Input (Join-Path $PSScriptRoot $file)) -cne $boundScripts[$file]){throw 'Recipe/input changed during batch'}}
|
|
if((Hash-Input $versionPath) -cne $versionsHash){throw 'Toolchain record changed during batch'}
|
|
$batchInputs=$true
|
|
$batchAfter=@($batchBefore|ForEach-Object {Get-GateBinary $_.path $taskTarget})
|
|
$observed=@(Get-GateTestBinaries $batchText $taskTarget)
|
|
if(-not(Same-GateBinaries $batchBefore $batchAfter)-or-not(Same-GateBinaries $batchAfter $observed)){throw 'Batch executed another or changed test binary'}
|
|
$batchBinaryValid=$true
|
|
Assert-ExactBatch $batch $batchResult 1095
|
|
if($batchExit -ne 0 -or $null -ne $batchFailure){throw 'Batch process failed to finish successfully'}
|
|
$batchValid=$true
|
|
}catch{$batchFailure=$_.Exception.Message}
|
|
$record=[ordered]@{schema='scenario_details_exact_batch_v1';stage=$batchId;base=$taskPin;workspace=$taskWorkspace;target=$taskTarget;command=('cargo '+($batchArgs -join ' '));arguments=$batchArgs;commandCharacters=$batch.command_characters;startUtc=$batchStart;endUtc=$batchEnd;cargoExitCode=$batchExit;selectionValid=$batchValid;inputsUnchanged=$batchInputs;postflightFailure=$batchFailure;boundScriptAndInputHashes=$boundScripts;listReceiptSha256=(Hash-Input (Join-Path $PSScriptRoot 'list-receipt.json'));batchPlanSha256=(Hash-Input (Join-Path $PSScriptRoot 'batches.json'));priorBatch=$lastBatch;rawLogSha256=(Hash-Input $batchLog);beforeCaptureSha256=(Hash-Input (Join-Path $PSScriptRoot ('inputs-complete-'+$batchId+'-before.json')));afterCaptureSha256=if(Test-Path (Join-Path $PSScriptRoot ('inputs-complete-'+$batchId+'-after.json'))){Hash-Input (Join-Path $PSScriptRoot ('inputs-complete-'+$batchId+'-after.json'))}else{$null};executedTests=$batchResult.executedTests;testResultSummaries=$batchResult.testResultSummaries;testExecutablesBefore=$batchBefore;testExecutables=$batchAfter;testBinaryBindingValid=$batchBinaryValid;toolchainVersionsSha256=$versionsHash}
|
|
$record|ConvertTo-Json -Depth 14|Set-Content -LiteralPath $batchReceipt -Encoding utf8
|
|
$lastBatch=[ordered]@{stage=$batchId;receiptSha256=(Hash-Input $batchReceipt);rawLogSha256=(Hash-Input $batchLog)}
|
|
$batchReceipts+=$lastBatch
|
|
foreach($artifact in @(($batchId+'-receipt.json'),($batchId+'.log'),('inputs-complete-'+$batchId+'-before.json'),('inputs-complete-'+$batchId+'-after.json'))){if(Test-Path -LiteralPath (Join-Path $PSScriptRoot $artifact)){$boundScripts[$artifact]=Hash-Input (Join-Path $PSScriptRoot $artifact)}}
|
|
# Aggregate raw output is the byte-exact concatenation, never a fabricated
|
|
# one-process summary. Individual logs/receipts remain the execution authority.
|
|
$stream=[IO.File]::Open($taskLog,[IO.FileMode]::Append,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
|
try{$bytes=[IO.File]::ReadAllBytes($batchLog);$stream.Write($bytes,0,$bytes.Length)}finally{$stream.Dispose()}
|
|
if(-not $batchValid){$batchesValid=$false;$taskExit=if($batchExit -ne 0){$batchExit}else{1};break}
|
|
}
|
|
}else{
|
|
& cargo @taskArgs 2>&1|Tee-Object -FilePath $taskLog
|
|
$taskExit=$LASTEXITCODE
|
|
}
|
|
|
|
$taskEnd=[DateTime]::UtcNow.ToString('o')
|
|
$taskText=Get-Content -Raw -LiteralPath $taskLog
|
|
$taskValid=$true;$taskFailure=$null
|
|
$listed=@();$executed=@();$matched=@();$ignored=@();$summaries=@();$listSummaries=@()
|
|
if($Stage -eq 'list'){
|
|
$listed=@([regex]::Matches($taskText,'(?m)^([^\r\n]+): test\r?$')|ForEach-Object {$_.Groups[1].Value})
|
|
$matched=@($listed|Where-Object {$_ -cin $expected}|Sort-Object)
|
|
$listSummaries=@([regex]::Matches($taskText,'(?m)^(\d+) tests, (\d+) benchmarks\r?$')|ForEach-Object {[ordered]@{tests=[int]$_.Groups[1].Value;benchmarks=[int]$_.Groups[2].Value}})
|
|
$taskValid=$listed.Count -ge $expectedCount -and @($listed|Sort-Object -Unique).Count -eq $listed.Count -and (Same-Names $expected $matched) -and (Same-Names @($selection.desktop_tests) $listed) -and $listSummaries.Count -gt 0 -and ($listSummaries|ForEach-Object {$_.tests}|Measure-Object -Sum).Sum -eq $listed.Count -and ($listSummaries|ForEach-Object {$_.benchmarks}|Measure-Object -Sum).Sum -eq 0
|
|
}
|
|
if($Stage -in @('focused','all')){
|
|
$executed=@([regex]::Matches($taskText,'(?m)^test ([^\r\n]+?) \.\.\. (ok|FAILED|ignored)[^\r\n]*\r?$')|ForEach-Object {[ordered]@{name=$_.Groups[1].Value;result=$_.Groups[2].Value}})
|
|
$summaries=@([regex]::Matches($taskText,'test result: (ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;')|ForEach-Object {[ordered]@{status=$_.Groups[1].Value;passed=[int]$_.Groups[2].Value;failed=[int]$_.Groups[3].Value;ignored=[int]$_.Groups[4].Value;measured=[int]$_.Groups[5].Value;filtered=[int]$_.Groups[6].Value}})
|
|
$ignored=@($executed|Where-Object {$_.result -eq 'ignored'}|ForEach-Object {$_.name})
|
|
$passedCount=@($executed|Where-Object {$_.result -eq 'ok'}).Count
|
|
$failedCount=@($executed|Where-Object {$_.result -eq 'FAILED'}).Count
|
|
$taskValid=$summaries.Count -gt 0 -and $failedCount -eq 0 -and @($summaries|Where-Object {$_.status -ne 'ok' -or $_.failed -ne 0 -or $_.measured -ne 0}).Count -eq 0 -and ($summaries|ForEach-Object {$_. passed}|Measure-Object -Sum).Sum -eq $passedCount -and ($summaries|ForEach-Object {$_. ignored}|Measure-Object -Sum).Sum -eq $ignored.Count
|
|
if($Stage -in @('focused','all')){
|
|
$matched=@($executed|Where-Object {$_.name -cin $expected -and $_.result -eq 'ok'}|ForEach-Object {$_.name}|Sort-Object)
|
|
$taskValid=$taskValid -and (Same-Names $expected $matched)
|
|
}
|
|
if($Stage -eq 'focused'){$taskValid=$taskValid -and $batchesValid -and $batchReceipts.Count -eq @($batchPlan.batches).Count -and $executed.Count -eq $expectedCount -and @($executed.name|Sort-Object -Unique).Count -eq $expectedCount -and $passedCount -eq $expectedCount -and $ignored.Count -eq 0 -and $summaries.Count -eq $batchReceipts.Count}
|
|
if($Stage -eq 'all'){
|
|
$listPath=Join-Path $PSScriptRoot 'list-receipt.json'
|
|
$listHash=Hash-Input $listPath
|
|
$listReceipt=Get-Content -Raw -LiteralPath $listPath|ConvertFrom-Json
|
|
if($prior.priorStage.stage -cne 'list' -or $prior.priorStage.receiptSha256 -cne $listHash){throw 'Desktop listing changed after focused execution.'}
|
|
$boundScripts['list-receipt.json']=$listHash
|
|
$taskValid=$taskValid -and (Same-Names @($listReceipt.listedTests) @($executed|ForEach-Object {$_.name})) -and ($summaries|ForEach-Object {$_. filtered}|Measure-Object -Sum).Sum -eq 0 -and (Same-Names @($selection.ignored_tests) $ignored)
|
|
}
|
|
|
|
}
|
|
$inputsUnchanged=$false
|
|
try {
|
|
& (Join-Path $PSScriptRoot 'capture.ps1') -Stage ($Stage+'-after')
|
|
foreach($file in $boundScripts.Keys){if((Hash-Input (Join-Path $PSScriptRoot $file)) -cne $boundScripts[$file]){throw ('Gate recipe/input changed: '+$file)}}
|
|
if((Hash-Input $versionPath) -cne $versionsHash){throw 'Toolchain version receipt changed.'}
|
|
if($priorBinding -and (Hash-Input (Join-Path $PSScriptRoot ($priorBinding.stage+'-receipt.json'))) -cne $priorBinding.receiptSha256){throw 'Prior receipt changed during gate.'}
|
|
$inputsUnchanged=$true
|
|
}catch{$taskFailure=$_.Exception.Message}
|
|
$testExecutables=@();$testBinaryBindingValid=$false
|
|
try {
|
|
if($Stage -eq 'list'){
|
|
$testExecutables=@(Get-GateTestBinaries $taskText $taskTarget)
|
|
if($testExecutables.Count -ne 1){throw 'Expected exactly one actual desktop test executable from Cargo listing.'}
|
|
} else {
|
|
$testExecutables=@($testExecutablesBefore|ForEach-Object {Get-GateBinary $_.path $taskTarget})
|
|
if(-not (Same-GateBinaries $testExecutablesBefore $testExecutables)){throw 'Bound test executable changed during this stage.'}
|
|
if($Stage -in @('focused','all')){
|
|
$observed=@(Get-GateTestBinaries $taskText $taskTarget)
|
|
if(-not (Same-GateBinaries $testExecutables $observed)){throw 'Executed test path differs from the exact listed executable.'}
|
|
}
|
|
}
|
|
$testBinaryBindingValid=$true
|
|
} catch {$taskFailure=$_.Exception.Message}
|
|
$taskValid=$taskValid -and $testBinaryBindingValid
|
|
$binary=$null
|
|
if($Stage -eq 'build' -and $taskExit -eq 0){$path=Join-Path $taskTarget 'debug/arcadia-studio.exe';$item=Get-Item -LiteralPath $path;$binary=[ordered]@{path=$item.FullName;bytes=$item.Length;sha256=(Hash-Input $path);mtimeUtc=$item.LastWriteTimeUtc.ToString('o')}}
|
|
[ordered]@{stage=$Stage;executionMode=if($Stage -eq 'focused'){'bounded-exact-batches-v1'}else{'cargo-single-stage'};batchReceipts=$batchReceipts;batchPlanSha256=(Hash-Input (Join-Path $PSScriptRoot 'batches.json'));command=if($Stage -eq 'focused'){'cargo test in bounded exact-name batches; see batchReceipts'}else{'cargo '+($taskArgs -join ' ')};arguments=if($Stage -eq 'focused'){@()}else{$taskArgs};base=$taskPin;workspace=$taskWorkspace;target=$taskTarget;startUtc=$taskStart;endUtc=$taskEnd;cargoExitCode=$taskExit;selectionValid=$taskValid;inputsUnchanged=$inputsUnchanged;postflightFailure=$taskFailure;boundScriptAndInputHashes=$boundScripts;priorStage=$priorBinding;externalEnvironmentPath=$toolchainEnv;externalEnvironmentSha256=(Hash-Input $toolchainEnv);toolchainVersionsSha256=$versionsHash;rawLogSha256=(Hash-Input $taskLog);beforeCaptureSha256=(Hash-Input (Join-Path $PSScriptRoot ('inputs-complete-'+$Stage+'-before.json')));afterCaptureSha256=if(Test-Path -LiteralPath (Join-Path $PSScriptRoot ('inputs-complete-'+$Stage+'-after.json'))){Hash-Input (Join-Path $PSScriptRoot ('inputs-complete-'+$Stage+'-after.json'))}else{$null};listedTests=$listed;executedTests=$executed;testResultSummaries=$summaries;matchedRegressionTests=$matched;ignoredTests=$ignored;listSummaries=$listSummaries;testExecutablesBefore=$testExecutablesBefore;testExecutables=$testExecutables;testBinaryBindingValid=$testBinaryBindingValid;binary=$binary}|ConvertTo-Json -Depth 14|Set-Content -LiteralPath $taskReceipt -Encoding utf8
|
|
if($taskExit -ne 0){exit $taskExit}
|
|
if(-not $taskValid -or -not $inputsUnchanged){throw 'Gate membership or input binding failed; inspect receipt and raw output.'}
|
|
} finally {Pop-Location}
|