Fix native reentry proof for stage-keyed progress slots
This commit is contained in:
@@ -38,6 +38,21 @@ function Same($First,$Second){
|
||||
return $records[0]-ceq$records[1]
|
||||
}catch{return $false}
|
||||
}
|
||||
function SameProgress($First,$Second){
|
||||
# Save slots are keyed by unique stageId; preserve every other array's strict order.
|
||||
try{
|
||||
$firstCopy=Clone $First;$secondCopy=Clone $Second
|
||||
foreach($copy in @($firstCopy,$secondCopy)){
|
||||
if($copy-isnot[pscustomobject]-or$copy.PSObject.Properties.Name-cnotcontains'puzzleStates'-or$copy.puzzleStates-isnot[array]-or$copy.puzzleStates.Count-gt30){return $false}
|
||||
$ids=[Collections.Generic.HashSet[long]]::new()
|
||||
foreach($slot in $copy.puzzleStates){
|
||||
if($slot-isnot[pscustomobject]-or$slot.PSObject.Properties.Name-cnotcontains'stageId'-or($slot.stageId-isnot[long]-and$slot.stageId-isnot[int])-or$slot.stageId-lt1-or$slot.stageId-gt30-or!$ids.Add([long]$slot.stageId)){return $false}
|
||||
}
|
||||
$copy.puzzleStates=@($copy.puzzleStates|Sort-Object stageId)
|
||||
}
|
||||
return Same $firstCopy $secondCopy
|
||||
}catch{return $false}
|
||||
}
|
||||
function Slot($State,[string]$Type,[int]$Stage){if($Type-eq'free'){return $State.latestFreePlay};return @($State.puzzleStates|Where-Object stageId -eq $Stage)[0]}
|
||||
function SameBalls($First,$Second,[double]$Tolerance=.004){
|
||||
if((($First.balls.id|Sort-Object)-join',')-ne(($Second.balls.id|Sort-Object)-join',')){return $false}
|
||||
@@ -204,7 +219,7 @@ try {
|
||||
@{name=$fixture.name;entryObservationMilliseconds=$watch.Elapsed.TotalMilliseconds;editorEntryMilliseconds=$fixture.editorEntryMilliseconds;method='Native wall time includes process launch/home/readiness/UI/ADB/save pull; does not isolate GameApp entry CPU';utc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json|Set-Content -LiteralPath (Join-Path $EvidenceDir "$($fixture.name)-timing.json")
|
||||
Step "$($fixture.name)-home-again" @('shell','input','tap','312','108')
|
||||
if($fixture.slot-eq'free'){Step "$($fixture.name)-reenter" @('shell','input','tap','1200','810')}else{SelectStage "$($fixture.name)-reenter" $fixture.stage}
|
||||
Check "$($fixture.name)-reentry-exact" (Same $after (Save "$($fixture.name)-reentered")) 'Repair/reset is stable and cannot repeatedly shift or change other slots'
|
||||
Check "$($fixture.name)-reentry-exact" (SameProgress $after (Save "$($fixture.name)-reentered")) 'Repair/reset is stable; only unique stage-keyed puzzle slot order may differ, all nested values and arrays remain exact'
|
||||
}
|
||||
foreach($fixture in $fixtures.drags){
|
||||
$state=FixtureState $fixture.source 'free' 1;$before=LoadFixture $fixture.name $state 'free' 1;$board=$before.latestFreePlay
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
param([Parameter(Mandatory)][string]$EvidenceDir,[string]$FinalSource=(Join-Path $PSScriptRoot 'exercise-final.ps1'),[string]$PlaygroundSource=(Join-Path $PSScriptRoot 'exercise-playground.ps1'),[string]$PreservationFixtureDir,[string]$BoardReadyPng,[string]$HomePng)
|
||||
param([Parameter(Mandatory)][string]$EvidenceDir,[string]$FinalSource=(Join-Path $PSScriptRoot 'exercise-final.ps1'),[string]$PlaygroundSource=(Join-Path $PSScriptRoot 'exercise-playground.ps1'),[string]$PreservationFixtureDir,[string]$BoardReadyPng,[string]$HomePng,[string]$ReentryFixtureDir)
|
||||
$ErrorActionPreference='Stop'
|
||||
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
|
||||
$checks=[Collections.Generic.List[object]]::new()
|
||||
@@ -95,6 +95,94 @@ exit ([int]$env:PLAYGROUND_CONTROL_TOUCH_EXIT)
|
||||
Probe "actual-preservation-float32-$kind" ($accepted-eq($kind-eq'same-float32')) @{accepted=$accepted;expectedPass=$kind-eq'same-float32'}
|
||||
}
|
||||
Probe 'actual-manual-camera-float32-exact' (Same $floatSource.latestFreePlay.camera $floatProduction.latestFreePlay.camera) 'Same float32 camera components and distance, different JSON decimal representation'
|
||||
$reentry=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq'SameProgress'},$true)
|
||||
if($reentry){. ([scriptblock]::Create($reentry.Extent.Text))}
|
||||
Probe 'actual-reentry-comparator-present' ($null-ne$reentry) 'Only outer puzzleStates order may be canonicalized; Same remains strict'
|
||||
$progress=Clone $floatProduction
|
||||
$progress.completedStageIds=@(1..30);$progress.latestFreePlay.sourceStageId=6
|
||||
$progress.puzzleStates[0].collectedBallIds=@('float-a','float-b');$progress.puzzleStates[0].countedBallIds=@('float-a','float-b')
|
||||
$third=Clone $progress.puzzleStates[0];$third.stageId=3
|
||||
$ball=Clone $third.balls[0];$ball.id='float-c';$third.balls=@($third.balls)+@($ball)
|
||||
$third.groups=@($third.balls|ForEach-Object {[pscustomobject]@{id=('group-'+$_.id);ballIds=@($_.id)}})
|
||||
$third.collectedBallIds=@();$third.countedBallIds=@()
|
||||
$sixth=Clone $progress.puzzleStates[0];$sixth.stageId=6;$sixth.collectedBallIds=@();$sixth.countedBallIds=@()
|
||||
$progress.puzzleStates=@($progress.puzzleStates[0],$third,$sixth)
|
||||
$progressBefore=$progress|ConvertTo-Json -Depth 20 -Compress
|
||||
foreach($order in @(@(0,1,2),@(0,2,1),@(1,0,2),@(1,2,0),@(2,0,1),@(2,1,0))){
|
||||
$actual=Clone $progress;$actual.puzzleStates=@($order|ForEach-Object {$actual.puzzleStates[$_]})
|
||||
$accepted=if($reentry){SameProgress $progress $actual}else{Same $progress $actual}
|
||||
Probe ('actual-reentry-slot-permutation-'+($order-join'')) $accepted @{stageIds=@($actual.puzzleStates.stageId);scope='Actual comparator, synthetic valid stage-keyed slots'}
|
||||
Probe ('actual-same-retains-slot-order-'+($order-join'')) ((Same $progress $actual)-eq(($order-join'')-eq'012')) 'Generic Same still rejects reordered arrays'
|
||||
}
|
||||
$empty=Clone $progress;$empty.puzzleStates=@()
|
||||
Probe 'actual-reentry-empty-slots-valid' $(if($reentry){SameProgress $empty $empty}else{Same $empty $empty}) 'Empty puzzle slot array is valid'
|
||||
foreach($kind in @('position-one-ulp','other-camera-one-ulp','free-camera-one-ulp','id-case','added-ball','lost-ball','membership','collect','count','source-stage','tutorial','completion','schema','balls-order','groups-order','members-order','collect-order','count-order','tutorial-order','completion-order','slot-added','slot-lost')){
|
||||
$actual=Clone $progress
|
||||
switch($kind){
|
||||
'position-one-ulp'{$actual.puzzleStates[0].balls[0].position.y=[double][MathF]::BitIncrement([single]$actual.puzzleStates[0].balls[0].position.y)}
|
||||
'other-camera-one-ulp'{$actual.puzzleStates[2].camera.distance=[double][MathF]::BitIncrement([single]$actual.puzzleStates[2].camera.distance)}
|
||||
'free-camera-one-ulp'{$actual.latestFreePlay.camera.rotation.w=[double][MathF]::BitIncrement([single]$actual.latestFreePlay.camera.rotation.w)}
|
||||
'id-case'{$actual.puzzleStates[0].balls[0].id='FLOAT-A'}
|
||||
'added-ball'{$actual.puzzleStates[0].balls=@($actual.puzzleStates[0].balls)+@(Clone $ball)}
|
||||
'lost-ball'{$actual.puzzleStates[0].balls=@($actual.puzzleStates[0].balls[0])}
|
||||
'membership'{$actual.puzzleStates[0].groups[0].ballIds=@('float-a')}
|
||||
'collect'{$actual.puzzleStates[0].collectedBallIds=@('float-a')}
|
||||
'count'{$actual.puzzleStates[0].countedBallIds=@('float-a')}
|
||||
'source-stage'{$actual.latestFreePlay.sourceStageId=7}
|
||||
'tutorial'{$actual.seenTutorialIds[0]='MERGE'}
|
||||
'completion'{$actual.completedStageIds=@(1..29)}
|
||||
'schema'{$actual.schemaVersion=2}
|
||||
'balls-order'{$actual.puzzleStates[0].balls=@($actual.puzzleStates[0].balls[1],$actual.puzzleStates[0].balls[0])}
|
||||
'groups-order'{$actual.puzzleStates[1].groups=@($actual.puzzleStates[1].groups[2],$actual.puzzleStates[1].groups[1],$actual.puzzleStates[1].groups[0])}
|
||||
'members-order'{$actual.puzzleStates[0].groups[0].ballIds=@('float-b','float-a')}
|
||||
'collect-order'{$actual.puzzleStates[0].collectedBallIds=@('float-b','float-a')}
|
||||
'count-order'{$actual.puzzleStates[0].countedBallIds=@('float-b','float-a')}
|
||||
'tutorial-order'{$actual.seenTutorialIds=@('split','merge')}
|
||||
'completion-order'{$actual.completedStageIds=@(30..1)}
|
||||
'slot-added'{$slot=Clone $actual.puzzleStates[0];$slot.stageId=4;$actual.puzzleStates=@($actual.puzzleStates)+@($slot)}
|
||||
'slot-lost'{$actual.puzzleStates=@($actual.puzzleStates[0],$actual.puzzleStates[1])}
|
||||
}
|
||||
$accepted=if($reentry){SameProgress $progress $actual}else{Same $progress $actual}
|
||||
Probe "actual-reentry-rejects-$kind" (!$accepted) @{accepted=$accepted}
|
||||
}
|
||||
foreach($kind in @('duplicate','missing-list','null-list','scalar-list','object-list','null-slot','scalar-slot','missing-stage','case-stage','null-stage','zero-stage','high-stage','string-stage','decimal-stage','fraction-stage','boolean-stage','too-many-slots')){
|
||||
$invalid=Clone $progress
|
||||
switch($kind){
|
||||
'duplicate'{$invalid.puzzleStates[1].stageId=2}
|
||||
'missing-list'{$invalid.PSObject.Properties.Remove('puzzleStates')}
|
||||
'null-list'{$invalid.puzzleStates=$null}
|
||||
'scalar-list'{$invalid.puzzleStates=2}
|
||||
'object-list'{$invalid.puzzleStates=$invalid.puzzleStates[0]}
|
||||
'null-slot'{$invalid.puzzleStates[0]=$null}
|
||||
'scalar-slot'{$invalid.puzzleStates[0]=2}
|
||||
'missing-stage'{$invalid.puzzleStates[0].PSObject.Properties.Remove('stageId')}
|
||||
'case-stage'{$slot=$invalid.puzzleStates[0];$slot.PSObject.Properties.Remove('stageId');$slot|Add-Member -NotePropertyName StageId -NotePropertyValue 2}
|
||||
'null-stage'{$invalid.puzzleStates[0].stageId=$null}
|
||||
'zero-stage'{$invalid.puzzleStates[0].stageId=0}
|
||||
'high-stage'{$invalid.puzzleStates[0].stageId=31}
|
||||
'string-stage'{$invalid.puzzleStates[0].stageId='2'}
|
||||
'decimal-stage'{$invalid.puzzleStates[0].stageId=[double]2.0}
|
||||
'fraction-stage'{$invalid.puzzleStates[0].stageId=2.5}
|
||||
'boolean-stage'{$invalid.puzzleStates[0].stageId=$true}
|
||||
'too-many-slots'{$invalid.puzzleStates=@(1..31|ForEach-Object {$slot=Clone $progress.puzzleStates[0];$slot.stageId=$_;$slot})}
|
||||
}
|
||||
$accepted=if($reentry){SameProgress $invalid $invalid}else{Same $invalid $invalid}
|
||||
Probe "actual-reentry-invalid-slots-$kind" (!$accepted) @{accepted=$accepted;scope='Even identical invalid slot lists must reject'}
|
||||
}
|
||||
Probe 'actual-reentry-comparison-does-not-mutate-record' (($progress|ConvertTo-Json -Depth 20 -Compress)-ceq$progressBefore) 'Canonicalization touches detached copies only'
|
||||
if($ReentryFixtureDir){
|
||||
$enteredPath=Join-Path $ReentryFixtureDir 'counting-position-only-entered.json';$reenteredPath=Join-Path $ReentryFixtureDir 'counting-position-only-reentered.json'
|
||||
$entered=Get-Content -LiteralPath $enteredPath -Raw|ConvertFrom-Json;$reentered=Get-Content -LiteralPath $reenteredPath -Raw|ConvertFrom-Json
|
||||
$accepted=if($reentry){SameProgress $entered $reentered}else{Same $entered $reentered}
|
||||
Probe 'actual-native-reentry-raw-records' $accepted @{entered=$enteredPath;enteredSha256=(Get-FileHash -LiteralPath $enteredPath).Hash;reentered=$reenteredPath;reenteredSha256=(Get-FileHash -LiteralPath $reenteredPath).Hash;beforeOrder=@($entered.puzzleStates.stageId);afterOrder=@($reentered.puzzleStates.stageId);scope='Actual native JSON replay only; no live native rerun'}
|
||||
Probe 'actual-native-reentry-generic-same-remains-strict' (!(Same $entered $reentered)) 'Original Same rejects sole outer puzzle slot order change'
|
||||
$reentryCheck=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.CommandAst]-and$node.GetCommandName()-eq'Check'-and$node.Extent.Text-like'*reentry-exact*'},$true)
|
||||
if(!$reentryCheck){throw 'Missing actual native reentry assertion'}
|
||||
$fixture=@{name='raw-replay'};$after=$entered;$controlReentryState=$reentered
|
||||
function Save([string]$Name){return Clone $controlReentryState}
|
||||
$accepted=$false;try{& ([scriptblock]::Create($reentryCheck.Extent.Text));$accepted=$true}catch{}
|
||||
Probe 'actual-native-reentry-assertion-replay' $accepted @{statement=$reentryCheck.Extent.Text;scope='Execute exact suite Check with raw native saves, transport replay only'}
|
||||
}
|
||||
if($PreservationFixtureDir){
|
||||
$injectedPath=Join-Path $PreservationFixtureDir 'safe-free-injected.json';$enteredPath=Join-Path $PreservationFixtureDir 'safe-free-entered.json'
|
||||
$injected=Get-Content -LiteralPath $injectedPath -Raw|ConvertFrom-Json;$entered=Get-Content -LiteralPath $enteredPath -Raw|ConvertFrom-Json
|
||||
|
||||
Reference in New Issue
Block a user