diff --git a/tools/android/exercise-content.ps1 b/tools/android/exercise-content.ps1 index a8ced7d..074e557 100644 --- a/tools/android/exercise-content.ps1 +++ b/tools/android/exercise-content.ps1 @@ -8,10 +8,11 @@ $checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generi $result=[ordered]@{milestone='Content';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'} $result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'} $networkBefore=$null; $musicBefore=$null -function Adb([string[]]$Arguments) { +function Adb([string[]]$Arguments,[switch]$AllowMissingPid) { $started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE $commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")}) - if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() + $missingPid=$AllowMissingPid -and $code-eq1 -and $Arguments.Count-eq3 -and $Arguments[0]-eq'shell' -and $Arguments[1]-eq'pidof' -and $Arguments[2]-eq$package -and [string]::IsNullOrWhiteSpace(($output -join "`n")) + if ($code -ne 0 -and !$missingPid) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() } function Check([string]$Id,[bool]$Passed,[string]$Actual) { $checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" } @@ -49,7 +50,7 @@ function StartHome([string]$Name) { $ready=$false; $pauseRequested=$false Add-Type -AssemblyName System.Drawing for($attempt=0;$attempt-lt30;$attempt++) { - $pidText=Adb @('shell','pidof',$package) + $pidText=Adb @('shell','pidof',$package) -AllowMissingPid $saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi')) $validSave=$false try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { } @@ -84,6 +85,7 @@ function StartHome([string]$Name) { $pauseRequested=$true } if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break} + if($pidText-notmatch'^\d+'){Adb @('shell','am','start','-W','-n',$activity)|Out-Null} } $observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8 Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review' diff --git a/tools/android/final-common.ps1 b/tools/android/final-common.ps1 index f8d4ce6..69fe94f 100644 --- a/tools/android/final-common.ps1 +++ b/tools/android/final-common.ps1 @@ -9,10 +9,11 @@ $checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generi $result=[ordered]@{milestone='Final';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'} $result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'} $networkBefore=$null; $musicBefore=$null -function Adb([string[]]$Arguments) { +function Adb([string[]]$Arguments,[switch]$AllowMissingPid) { $started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE $commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")}) - if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() + $missingPid=$AllowMissingPid -and $code-eq1 -and $Arguments.Count-eq3 -and $Arguments[0]-eq'shell' -and $Arguments[1]-eq'pidof' -and $Arguments[2]-eq$package -and [string]::IsNullOrWhiteSpace(($output -join "`n")) + if ($code -ne 0 -and !$missingPid) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() } function Check([string]$Id,[bool]$Passed,[string]$Actual) { $checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" } @@ -111,18 +112,26 @@ function BallClear($Board,[string]$Id) { $camera=$Board.camera $rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w) $origin=[Numerics.Vector3]::new($camera.target.x,($camera.target.y+.65),$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation) + $size=Adb @('shell','wm','size'); if ($size -notmatch '(\d+)x(\d+)') { throw "Unknown device resolution: $size" } + $width=[Math]::Max([int]$Matches[1],[int]$Matches[2]); $height=[Math]::Min([int]$Matches[1],[int]$Matches[2]) + $scale=[Math]::Tan(31*[Math]::PI/180)/($height/2) + # Match the rounded native DOWN pixel, including its actual target entry distance. + $direction=[Numerics.Vector3]::Normalize([Numerics.Vector3]::Transform([Numerics.Vector3]::new(($point[0]-$width/2)*$scale,($height/2-$point[1])*$scale,1),$rotation)) $target=@($Board.balls|Where-Object id -eq $Id)[0].position - $direction=[Numerics.Vector3]::new($target.x,$target.y,$target.z)-$origin - $targetHit=$direction.Length()-.5 - $direction=[Numerics.Vector3]::Normalize($direction) + $delta=[Numerics.Vector3]::new($target.x,$target.y,$target.z)-$origin + $along=[Numerics.Vector3]::Dot($direction,$delta) + $perpendicularSquared=($delta-$direction*$along).LengthSquared() + if($perpendicularSquared-gt.25){return $false} + $targetHit=$along-[Math]::Sqrt([Math]::Max(0.0,.25-$perpendicularSquared)) + if($targetHit-lt0){return $false} foreach($ball in $Board.balls){ if($ball.id-eq$Id){continue} $delta=[Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin $along=[Numerics.Vector3]::Dot($direction,$delta) - $perpendicularSquared=$delta.LengthSquared()-$along*$along + $perpendicularSquared=($delta-$direction*$along).LengthSquared() if($perpendicularSquared-le.25){ - $near=$along-[Math]::Sqrt([Math]::Max(0,.25-$perpendicularSquared)) - if($near-ge0-and$near-lt$targetHit){return $false} + $near=$along-[Math]::Sqrt([Math]::Max(0.0,.25-$perpendicularSquared)) + if($near-ge0-and$near-le$targetHit){return $false} } } return $true @@ -160,7 +169,7 @@ function StartHome([string]$Name) { $ready=$false; $pauseRequested=$false Add-Type -AssemblyName System.Drawing for($attempt=0;$attempt-lt30;$attempt++) { - $pidText=Adb @('shell','pidof',$package) + $pidText=Adb @('shell','pidof',$package) -AllowMissingPid $saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi')) $validSave=$false try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { } @@ -195,6 +204,7 @@ function StartHome([string]$Name) { $pauseRequested=$true } if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break} + if($pidText-notmatch'^\d+'){Adb @('shell','am','start','-W','-n',$activity)|Out-Null} } $observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8 Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review' diff --git a/tools/android/test-final-pixel-ray.ps1 b/tools/android/test-final-pixel-ray.ps1 new file mode 100644 index 0000000..1146053 --- /dev/null +++ b/tools/android/test-final-pixel-ray.ps1 @@ -0,0 +1,50 @@ +param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Before,[Parameter(Mandatory)][string]$After,[Parameter(Mandatory)][string]$Baseline,[string]$Source=(Join-Path $PSScriptRoot 'final-common.ps1')) +$ErrorActionPreference='Stop' +New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null +$checks=[Collections.Generic.List[object]]::new() +function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})} +function Definition($Path,$Name){$ast=[Management.Automation.Language.Parser]::ParseFile($Path,[ref]$null,[ref]$null);return $ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true).Extent.Text} +foreach($name in @('BallPoint','BallClear','WorldSafe','Same','AssertNativeTranslation','AssertNativeMerge')){ + $expected=(Definition $Source $name).Replace("function $name","function global:$name") + . ([scriptblock]::Create($expected)) + Record "$name-production-body-identity" ((Get-Command $name).ScriptBlock.Ast.Extent.Text-ceq$expected) $Source +} +function HashText($Text){return [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($Text)))} +foreach($name in @('NativePointer','NativeMoveAndHold','AssertNativeTranslation','AssertNativeMerge')){Record "$name-contract-unchanged" ((Definition $Source $name)-ceq(Definition $Baseline $name)) @{baseline=$Baseline;beforeSha256=(HashText (Definition $Baseline $name));afterSha256=(HashText (Definition $Source $name))}} +$script:resolution='Physical size: 1080x2400';$script:adbCalls=0 +function Adb($Arguments){$script:adbCalls++;return $script:resolution} +function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}} +function Ball($Id,$X,$Y,$Z){return [pscustomobject]@{id=$Id;position=[pscustomobject]@{x=$X;y=$Y;z=$Z}}} +function Board($Balls){return [pscustomobject]@{balls=@($Balls);groups=@([pscustomobject]@{id='same';ballIds=@($Balls.id)});camera=[pscustomobject]@{target=[pscustomobject]@{x=0;y=.6;z=0};rotation=[pscustomobject]@{x=0;y=0;z=0;w=1};distance=8}}} +try{ + $beforeState=Get-Content -LiteralPath $Before -Raw|ConvertFrom-Json;$afterState=Get-Content -LiteralPath $After -Raw|ConvertFrom-Json + $b=@($beforeState.puzzleStates|Where-Object stageId -eq 16)[0];$a=@($afterState.puzzleStates|Where-Object stageId -eq 16)[0] + Record 'integer-max-overload-reproduced-and-double-preserves-radius' ([Math]::Max(0,.25-.09)-eq0-and[Math]::Abs([Math]::Max(0.0,.25-.09)-.16)-lt1e-12) @{integerMax=[Math]::Max(0,.25-.09);doubleMax=[Math]::Max(0.0,.25-.09)} + $point=BallPoint $b 's16-b02' + Record 'captured-wrong-source-pixel-rejected' (!(BallClear $b 's16-b02')-and$point[0]-eq733-and$point[1]-eq346) @{point=$point;requested='s16-b02';actualMoved='s16-b05'} + $errorText=$null;try{AssertNativeMerge 'captured' $b $a 's16-b02'}catch{$errorText=$_.Exception.Message} + Record 'captured-wrong-source-strict-merge-still-rejected' ($errorText-match'requested-source-actually-moved') $errorText + foreach($case in @( + @{id='single';balls=@((Ball 'target' 0 1.25 0));clear=$true}, + @{id='front';balls=@((Ball 'target' 0 1.25 0),(Ball 'front' 0 1.25 -1));clear=$false}, + @{id='behind';balls=@((Ball 'target' 0 1.25 0),(Ball 'behind' 0 1.25 1));clear=$true}, + # Target projects to1200.45 and rounds1200. Center ray misses the left sphere, + # while the actual pixel ray intersects it. Geometry expectations are analytic. + @{id='rounded-pixel-front';balls=@((Ball 'target' .004 1.25 0),(Ball 'front' -.497 1.25 -1));clear=$false}, + @{id='rounded-pixel-outside';balls=@((Ball 'target' .004 1.25 0),(Ball 'front' -.501 1.25 -1));clear=$true}, + @{id='exact-tangent-front';balls=@((Ball 'target' 0 1.25 0),(Ball 'front' -.5 1.25 -1));clear=$false}, + @{id='equal-entry-ambiguous';balls=@((Ball 'target' 0 1.25 0),(Ball 'same' 0 1.25 0));clear=$false}, + @{id='behind-camera';balls=@((Ball 'target' 0 1.25 -9));clear=$false}, + @{id='unsafe-ui';balls=@((Ball 'target' 20 1.25 0));clear=$false} + )){$board=Board $case.balls;$snapshot=$board|ConvertTo-Json -Depth 12;$actual=BallClear $board 'target';Record $case.id ($actual-eq$case.clear-and($board|ConvertTo-Json -Depth 12)-ceq$snapshot) @{expected=$case.clear;actual=$actual}} + foreach($kind in @('missing','duplicate')){ + $board=Board @((Ball 'target' 0 1.25 0));if($kind-eq'duplicate'){$board.balls+=Ball 'target' 0 1.25 1} + $script:adbCalls=0;$rejected=$false;try{BallClear $board $(if($kind-eq'missing'){'absent'}else{'target'})|Out-Null}catch{$rejected=$_.Exception.Message-match'exactly one existing ball ID'} + Record "$kind-ID-rejected-before-projection-input" ($rejected-and$script:adbCalls-eq0) $kind + } + $script:resolution='unavailable';$rejected=$false;try{BallClear (Board @((Ball 'target' 0 1.25 0))) 'target'|Out-Null}catch{$rejected=$_.Exception.Message-match'Unknown device resolution'} + Record 'unknown-resolution-rejected' $rejected $script:resolution +}catch{Record 'production-evaluation' $false $_.Exception.Message} +$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0 +@{passed=$passed;scope='Offline actual function bodies and captured wrong-source save. Analytic edge cases are counterfactual; no native raycast or product pass claim.';source=$Source;sourceSha256=(Get-FileHash $Source).Hash;before=$Before;beforeSha256=(Get-FileHash $Before).Hash;after=$After;afterSha256=(Get-FileHash $After).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 12|Set-Content (Join-Path $EvidenceDir 'pixel-ray-result.json') +if(!$passed){exit 1};exit 0 diff --git a/tools/android/test-final-startup.ps1 b/tools/android/test-final-startup.ps1 new file mode 100644 index 0000000..43e5d3a --- /dev/null +++ b/tools/android/test-final-startup.ps1 @@ -0,0 +1,70 @@ +param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$HomePng,[string]$Source=(Join-Path $PSScriptRoot 'final-common.ps1')) +$ErrorActionPreference='Stop' +New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null +$EvidenceDir=(Resolve-Path $EvidenceDir).Path +$checks=[Collections.Generic.List[object]]::new();$commands=[Collections.Generic.List[object]]::new() +function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})} +foreach($name in @('Adb','StartHome')){ + $ast=[Management.Automation.Language.Parser]::ParseFile($Source,[ref]$null,[ref]$null) + $definition=$ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true) + $expected=$definition.Extent.Text.Replace("function $name","function global:$name") + . ([scriptblock]::Create($expected)) + Record "$name-production-body-identity" ((Get-Command $name).ScriptBlock.Ast.Extent.Text-ceq$expected) $Source +} +$adb='OfflineNative';$Serial='offline';$package='com.magnetismheroes.game';$activity="$package/activity";$remote='/offline/progress.json' +function OfflineNative { + $text=$args-join' ';$global:LASTEXITCODE=0 + if($script:mode-eq'exit-control'){$global:LASTEXITCODE=$script:mockExit;return $script:mockOutput} + if($text-match'pidof'){ + $script:pidCalls++ + if($script:mode-eq'other-error'){$global:LASTEXITCODE=2;return ''} + if($script:mode-eq'absent'-or($script:mode-eq'eventually-ready'-and$script:pidCalls-le2)){$global:LASTEXITCODE=1;return ''} + if($script:mode-eq'empty-zero') {return ''} + if($script:mode-eq'malformed-pid') {return 'unavailable'} + return '77' + } + if($text-match'am start'){$script:starts++;if($script:mode-eq'start-error'){$global:LASTEXITCODE=1;return ''};return 'Status: ok; delivered-to-top-instance'} + if($text-match'cat '){if($script:mode-eq'save-error'){$global:LASTEXITCODE=1;return ''};if($script:mode-eq'invalid-save'){return '{}'};return '{"schemaVersion":1,"completedStageIds":[],"puzzleStates":[]}'} + return '' +} +function Step($Name,$Arguments=@(),$Delay=500){ + $destination=Join-Path $EvidenceDir "$Name.png" + if($script:mode-eq'black-ui'){$image=[Drawing.Bitmap]::new(2400,1080);try{$image.Save($destination)}finally{$image.Dispose()}}else{Copy-Item -LiteralPath $HomePng -Destination $destination -Force} +} +function Start-Sleep {} +function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}} +try{ + $script:mode='exit-control';$script:mockOutput='';$script:mockExit=1 + $rejected=$false;try{Adb @('shell','pidof',$package)|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'} + Record 'default-adb-exit1-still-rejected' ($rejected-and$commands[-1].exitCode-eq1) $commands[-1] + $errorText=$null;try{$empty=Adb @('shell','pidof',$package) -AllowMissingPid}catch{$errorText=$_.Exception.Message} + Record 'allowed-pid-absence-retains-raw-exit1' (!$errorText-and$empty-eq''-and$commands[-1].exitCode-eq1) $commands[-1] + $script:mockExit=2;$rejected=$false;try{Adb @('shell','pidof',$package) -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 2'} + Record 'other-pid-error-still-rejected' ($rejected-and$commands[-1].exitCode-eq2) $commands[-1] + $script:mockExit=1 + foreach($arguments in @(@('shell','wm','size'),@('shell','pidof','another.package'),@('shell','am','start','-W','-n',$activity))){ + $rejected=$false;try{Adb $arguments -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'} + Record 'missing-pid-opt-in-cannot-hide-other-command-exit1' ($rejected-and$commands[-1].exitCode-eq1) $commands[-1] + } + $script:mockOutput='pidof: error';$rejected=$false;try{Adb @('shell','pidof',$package) -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'} + Record 'pidof-exit1-with-error-output-still-rejected' ($rejected-and$commands[-1].output-eq'pidof: error') $commands[-1] + foreach($mode in @('eventually-ready','absent','empty-zero','malformed-pid','black-ui','invalid-save','other-error','start-error','save-error')){ + $script:mode=$mode;$script:pidCalls=0;$script:starts=0;$commands.Clear();$errorText=$null + try{StartHome "offline-$mode"}catch{$errorText=$_.Exception.Message} + $observationPath=Join-Path $EvidenceDir "offline-$mode-readiness.json" + $observations=if(Test-Path $observationPath){@(Get-Content $observationPath -Raw|ConvertFrom-Json)}else{@()} + if($mode-eq'eventually-ready'){ + Record 'absent-pid-relaunches-and-requires-real-home-save-pid' (!$errorText-and$observations.Count-eq3-and$script:starts-eq3-and$observations[0].pid-eq''-and$observations[-1].pid-eq'77'-and$observations[-1].validAppSave-and$observations[-1].readyUi) @{observations=$observations;starts=$script:starts} + Record 'absent-pid-raw-exits-remain-in-command-evidence' (@($commands|Where-Object {$_.command-match'pidof'-and$_.exitCode-eq1}).Count-eq2) $commands.ToArray() + }elseif($mode-eq'other-error'){ + Record 'startup-does-not-swallow-other-adb-error' ($errorText-match'ADB exit 2'-and$script:pidCalls-eq1) $errorText + }elseif($mode-eq'start-error'-or$mode-eq'save-error'){ + Record 'startup-does-not-swallow-other-exit1' ($errorText-match'ADB exit 1') @{kind=$mode;error=$errorText} + }else{ + Record 'unready-home-cannot-pass-thirty-bound' ($errorText-match'home-ready'-and$script:pidCalls-eq30-and$observations.Count-eq30) @{kind=$mode;error=$errorText;observations=$observations.Count;starts=$script:starts} + } + } +}catch{Record 'production-evaluation' $false $_.Exception.Message} +$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0 +@{passed=$passed;scope='Offline StartHome/Adb production bodies with leaf process/PNG mocks. Recorded Home PNG is real; process absence, invalid Save and black screen sequences are counterfactual, not native startup proof.';source=$Source;sourceSha256=(Get-FileHash $Source).Hash;homePng=$HomePng;homePngSha256=(Get-FileHash $HomePng).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 14|Set-Content (Join-Path $EvidenceDir 'startup-result.json') +if(!$passed){exit 1};exit 0