diff --git a/tools/android/exercise-playground.ps1 b/tools/android/exercise-playground.ps1 index b45d80c..48858d3 100644 --- a/tools/android/exercise-playground.ps1 +++ b/tools/android/exercise-playground.ps1 @@ -64,14 +64,46 @@ function FixtureState($Board,[string]$Type,[int]$Stage){ if($Type-eq'free'){$state.latestFreePlay=Clone $Board}else{$state.puzzleStates=@($state.puzzleStates|Where-Object stageId -ne $Stage)+@(Clone $Board)} return $state } +function BoardScreen([string]$Path){ + Add-Type -AssemblyName System.Drawing;$image=[Drawing.Bitmap]::new($Path) + $pixels=[Collections.Generic.List[object]]::new();$gold=0;$world=0 + try{ + if($image.Width-eq2400-and$image.Height-eq1080){ + foreach($point in @(@(80,70),@(380,70),@(80,140),@(380,140))){ + $pixel=$image.GetPixel($point[0],$point[1]);$pixels.Add(@{x=$point[0];y=$point[1];r=$pixel.R;g=$pixel.G;b=$pixel.B}) + if($pixel.R-ge230-and$pixel.G-ge185-and$pixel.G-le210-and$pixel.B-ge60-and$pixel.B-le90){$gold++} + } + foreach($x in @(600,1200,1800)){foreach($y in @(300,500,700)){ + $pixel=$image.GetPixel($x,$y);$pixels.Add(@{x=$x;y=$y;r=$pixel.R;g=$pixel.G;b=$pixel.B}) + if($pixel.R-gt20-or$pixel.G-gt20-or$pixel.B-gt20){$world++} + }} + } + return @{ready=$gold-eq4-and$world-ge3;width=$image.Width;height=$image.Height;goldCorners=$gold;nonblackWorldSamples=$world;pixels=$pixels.ToArray()} + }finally{$image.Dispose()} +} +function WaitBoard([string]$Name){ + $observations=[Collections.Generic.List[object]]::new();$ready=$false;$state=$null + try{ + for($attempt=0;$attempt-lt30;$attempt++){ + $stepName="$Name-board-ready-$attempt";Step $stepName @() 500 + $png=Join-Path $EvidenceDir "$stepName.png";$screen=BoardScreen $png + $state=Save "$stepName-save";$savePath=Join-Path $EvidenceDir "$stepName-save.json" + $observations.Add(@{attempt=$attempt;utc=[DateTime]::UtcNow.ToString('o');png=$png;pngSha256=(Get-FileHash -LiteralPath $png).Hash;screen=$screen;save=$savePath;saveSha256=(Get-FileHash -LiteralPath $savePath).Hash}) + if($screen.ready){$ready=$true;break} + } + Check "$Name-board-ui-ready" $ready 'Actual 2400x1080 board: four golden Home-button corners and nonblack world samples; unchanged pulled save alone is never entry proof; at most 30 observations at 500ms intervals' + Copy-Item -LiteralPath $observations[-1].save -Destination (Join-Path $EvidenceDir "$Name-entered.json") + return $state + }finally{@{ready=$ready;finishedUtc=[DateTime]::UtcNow.ToString('o');observations=$observations.ToArray()}|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-board-readiness.json") -Encoding utf8} +} function LoadFixture([string]$Name,$State,[string]$Type,[int]$Stage){ Adb @('shell','am','force-stop',$package)|Out-Null $path=Join-Path $EvidenceDir "$Name-injected.json";$State|ConvertTo-Json -Depth 20|Set-Content -LiteralPath $path -Encoding utf8 @{name=$Name;origin='Actual Fresh30/thirty-completed.json; explicitly labelled injected boundary case';originalFile=$originalPath;originalSha256=(Get-FileHash -LiteralPath $originalPath).Hash;fixtureSha256=(Get-FileHash -LiteralPath $path).Hash;changedFields=@('puzzleStates','latestFreePlay');preservedFields=@('schemaVersion','completedStageIds','seenTutorialIds');utc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Depth 5|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-origin.json") Adb @('push',$path,$remote)|Out-Null;Adb @('push',$path,($remote+'.bak'))|Out-Null StartHome "$Name-home" - if($Type-eq'free'){Step "$Name-enter-free" @('shell','input','tap','1200','810') 1500}else{SelectStage "$Name-enter-puzzle" $Stage} - return Save "$Name-entered" + if($Type-eq'free'){Step "$Name-enter-free" @('shell','input','tap','1200','810') 0}else{SelectStage "$Name-enter-puzzle" $Stage} + return WaitBoard $Name } function PreservesOtherSlots([string]$Name,$Before,$After,[string]$Type,[int]$Stage){ Check "$Name-progress-preserved" ((Same $Before.completedStageIds $After.completedStageIds)-and(Same $Before.seenTutorialIds $After.seenTutorialIds)-and$After.schemaVersion-eq1) 'Completion/unlock/tutorial/schema unchanged' diff --git a/tools/android/test-playground-contracts.ps1 b/tools/android/test-playground-contracts.ps1 index 1137cc4..0c31fdb 100644 --- a/tools/android/test-playground-contracts.ps1 +++ b/tools/android/test-playground-contracts.ps1 @@ -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) +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) $ErrorActionPreference='Stop' New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null $checks=[Collections.Generic.List[object]]::new() @@ -128,6 +128,64 @@ exit ([int]$env:PLAYGROUND_CONTROL_TOUCH_EXIT) $friends=@(BlueFriends $path);$expectedCount=if($kind-eq'two-blue-friends'){2}else{0} Probe "actual-live-steering-vision-$kind" ($friends.Count-eq$expectedCount) @{friends=$friends;expectedCount=$expectedCount;scope='Synthetic semantic control, not native screenshot verdict'} } + $boardReader=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq'BoardScreen'},$true) + $boardWait=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq'WaitBoard'},$true) + Probe 'actual-board-readiness-functions-present' ($null-ne$boardReader-and$null-ne$boardWait) 'Actual suite screen predicate and bounded wait' + if($boardReader-and$boardWait){ + . ([scriptblock]::Create($boardReader.Extent.Text));. ([scriptblock]::Create($boardWait.Extent.Text)) + foreach($kind in @('board','home','black','black-with-ui','partial-ui','wrong-size')){ + $width=if($kind-eq'wrong-size'){1200}else{2400};$height=if($kind-eq'wrong-size'){540}else{1080} + $bitmap=[Drawing.Bitmap]::new($width,$height);$graphics=[Drawing.Graphics]::FromImage($bitmap) + try{ + $background=if($kind-in@('black','black-with-ui')){[Drawing.Color]::Black}else{[Drawing.Color]::FromArgb(147,211,223)} + $graphics.Clear($background) + if($kind-in@('board','black-with-ui','partial-ui','wrong-size')){ + $brush=[Drawing.SolidBrush]::new([Drawing.Color]::FromArgb(245,196,72)) + try{$graphics.FillRectangle($brush,58,59,$(if($kind-eq'partial-ui'){100}else{365}),99)}finally{$brush.Dispose()} + } + $path=Join-Path $EvidenceDir "readiness-$kind.png";$bitmap.Save($path,[Drawing.Imaging.ImageFormat]::Png) + }finally{$graphics.Dispose();$bitmap.Dispose()} + $screen=BoardScreen $path + Probe "actual-board-screen-$kind" ($screen.ready-eq($kind-eq'board')) $screen + } + if($BoardReadyPng-and$HomePng){ + foreach($control in @(@{name='actual-free';path=$BoardReadyPng;expected=$true},@{name='actual-home';path=$HomePng;expected=$false})){ + $screen=BoardScreen $control.path + Probe "actual-board-screen-$($control.name)" ($screen.ready-eq$control.expected) @{path=$control.path;sha256=(Get-FileHash -LiteralPath $control.path).Hash;screen=$screen;scope='Actual captured PNG replay; no live native readiness claim'} + } + } + } + # Execute the actual LoadFixture path with capture/save transport doubles only; no ADB. + $load=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq'LoadFixture'},$true) + . ([scriptblock]::Create($load.Extent.Text)) + function Adb([string[]]$Arguments){} + function StartHome([string]$Name){} + function SelectStage([string]$Name,[int]$Stage){Step "$Name-stage" @() 0} + function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500){ + $controlSteps.Add(@{name=$Name;delay=$Delay}) + if($Name-like'*-board-ready-*'){ + $capture=if($controlHomeOnly-or$controlAttempt-eq0){$controlHome}else{$controlBoard} + Copy-Item -LiteralPath $capture -Destination (Join-Path $EvidenceDir "$Name.png") + $script:controlAttempt++ + } + } + function Save([string]$Name){Copy-Item -LiteralPath $originalPath -Destination (Join-Path $EvidenceDir "$Name.json");return Clone $original} + $originalPath=Join-Path $EvidenceDir 'readiness-original.json' + if($PreservationFixtureDir){Copy-Item -LiteralPath (Join-Path $PreservationFixtureDir 'safe-free-injected.json') -Destination $originalPath;$original=Get-Content -LiteralPath $originalPath -Raw|ConvertFrom-Json}else{$original|ConvertTo-Json -Depth 20|Set-Content -LiteralPath $originalPath} + $originalHash=(Get-FileHash -LiteralPath $originalPath).Hash + $controlBoard=if($BoardReadyPng){$BoardReadyPng}else{Join-Path $EvidenceDir 'readiness-board.png'} + $controlHome=if($HomePng){$HomePng}else{Join-Path $EvidenceDir 'readiness-home.png'} + foreach($type in @('free','puzzle')){foreach($controlHomeOnly in @($false,$true)){ + $script:controlAttempt=0;$controlSteps=[Collections.Generic.List[object]]::new();$accepted=$false;$message=$null + $label="readiness-$type-$(if($controlHomeOnly){'timeout'}else{'home-to-board'})" + try{$loaded=LoadFixture $label $original $type 1;$accepted=$true}catch{$message=$_.Exception.Message} + $polls=@($controlSteps|Where-Object name -like '*-board-ready-*') + $reportPath=Join-Path $EvidenceDir "$label-board-readiness.json";$report=if(Test-Path -LiteralPath $reportPath){Get-Content -LiteralPath $reportPath -Raw|ConvertFrom-Json}else{$null} + $expectedPolls=if($controlHomeOnly){30}else{2} + Probe "actual-entry-$type-$(if($controlHomeOnly){'home-timeout-rejects-unchanged-save'}else{'waits-for-ui-despite-unchanged-save'})" ($accepted-eq!$controlHomeOnly-and$polls.Count-eq$expectedPolls-and@($polls|Where-Object delay -ne 500).Count-eq0-and$report.observations.Count-eq$expectedPolls-and$report.ready-eq!$controlHomeOnly) @{accepted=$accepted;polls=$polls.Count;observations=$report.observations.Count;error=$message;scope='Actual suite LoadFixture/WaitBoard, transport doubles replay actual PNGs and unchanged JSON; no native run'} + $enteredPath=Join-Path $EvidenceDir "$label-entered.json" + Probe "actual-entry-$type-$(if($controlHomeOnly){'timeout'}else{'ready'})-raw-save-preserved" ($report.observations.Count-eq$expectedPolls-and@($report.observations|Where-Object saveSha256 -ne $originalHash).Count-eq0-and$(if($accepted){(Get-FileHash -LiteralPath $enteredPath).Hash-eq$originalHash-and(Same $loaded $original)}else{!(Test-Path -LiteralPath $enteredPath)})) @{original=$originalPath;sha256=$originalHash;observations=$report.observations.Count;enteredExists=(Test-Path -LiteralPath $enteredPath)} + }} } }catch{Probe 'control-execution' $false $_.Exception.Message} $passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0