118 lines
12 KiB
PowerShell
118 lines
12 KiB
PowerShell
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
|
. (Join-Path $PSScriptRoot 'final-common.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
|
try {
|
|
$result.scope='Separate QA planar collected10 replay20 fixture; real30 source record preserved. Actual offline native11 taps and complete numeric PCM; not fresh progression evidence.'
|
|
Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
|
$sourceDir=Join-Path (Split-Path $EvidenceDir -Parent) 'Fresh30'
|
|
$fresh=Get-Content (Join-Path $sourceDir 'android-final-progression-evidence.json') -Raw|ConvertFrom-Json
|
|
Check 'actual30-source-provenance' ($fresh.status-eq'passed'-and$fresh.sourceCommit-eq$result.sourceCommit-and$fresh.apkSha256-eq$result.apkSha256) 'Authoritative fresh suite, actual30 no fixture'
|
|
$sourcePath=Join-Path $sourceDir 'thirty-completed.json'
|
|
$sourceCopy=Join-Path $EvidenceDir 'actual-thirty-completed-source.json';Copy-Item -LiteralPath $sourcePath -Destination $sourceCopy
|
|
$save=Get-Content $sourceCopy -Raw|ConvertFrom-Json
|
|
Check 'actual30-source-records' ($save.completedStageIds.Count-eq30-and$save.latestFreePlay.sourceStageId-eq30) 'Source is real UI success snapshot, not current boundary-corrupted save'
|
|
$result.fixtureProvenance=@{sourcePath=$sourcePath;sourceSha256=(Get-FileHash $sourceCopy).Hash;changedFields=@('puzzleStates: QA replay20', 'QA board ball positions', 'QA single group', 'QA collected10/counted0', 'QA camera');completedRecords='Unchanged actual30 source; no new progression claim'}
|
|
$audioBefore=Adb @('shell','dumpsys','audio');$audioBefore|Set-Content (Join-Path $EvidenceDir 'music-before.txt')
|
|
$current=[regex]::Match($audioBefore,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
|
|
$musicBefore=@{};foreach($m in [regex]::Matches($current,'([0-9a-f]+) \([^)]+\): (\d+)')){$musicBefore[$m.Groups[1].Value]=[int]$m.Groups[2].Value}
|
|
Check 'music-original-routes' ($musicBefore.ContainsKey('2')-and$musicBefore.ContainsKey('8000')-and$musicBefore.ContainsKey('40000000')) $current
|
|
$networkBefore=@{airplane=(Adb @('shell','settings','get','global','airplane_mode_on'));wifi=(Adb @('shell','settings','get','global','wifi_on'));data=(Adb @('shell','settings','get','global','mobile_data'))};$result.networkBefore=$networkBefore
|
|
Adb @('shell','cmd','connectivity','airplane-mode','enable')|Out-Null;Adb @('shell','svc','wifi','disable')|Out-Null;Adb @('shell','svc','data','disable')|Out-Null
|
|
for($n=0;$n-lt30;$n++){$offline=(Adb @('shell','settings','get','global','airplane_mode_on'))-eq'1'-and(Adb @('shell','settings','get','global','wifi_on'))-eq'0'-and(Adb @('shell','settings','get','global','mobile_data'))-eq'0'-and(Adb @('shell','dumpsys','connectivity'))-match'Active default network: none';if($offline){break};Start-Sleep -Milliseconds 500}
|
|
Check 'rapid-actual-offline' $offline 'Airplane1 wifi0 data0 no active network'
|
|
$boardPath=Join-Path $sourceDir 'stage-20-success.json'
|
|
$result.fixtureProvenance.actualStage20Sha256=(Get-FileHash $boardPath).Hash
|
|
$collectedPath=Join-Path $sourceDir 'stage-20-collected.json'
|
|
$result.fixtureProvenance.actualStage20CollectedSha256=(Get-FileHash $collectedPath).Hash
|
|
$board=@((Get-Content $collectedPath -Raw|ConvertFrom-Json).puzzleStates|Where-Object stageId -eq 20)[0]
|
|
for($i=0;$i-lt10;$i++){$board.balls[$i].position=[pscustomobject][ordered]@{x=[double]($i%5-2);y=(2.2+[Math]::Floor($i/5));z=[double]-1}}
|
|
$board.groups=@([pscustomobject][ordered]@{id='qa-collected10';ballIds=@($board.balls.id)});$board.collectedBallIds=@($board.balls.id);$board.countedBallIds=@()
|
|
$board.camera=[pscustomobject][ordered]@{target=[pscustomobject][ordered]@{x=[double]0;y=.6;z=[double]0};rotation=[pscustomobject][ordered]@{x=[double]0;y=[double]0;z=[double]0;w=[double]1};distance=[double]11}
|
|
$save.puzzleStates=@($board);$fixture=Join-Path $EvidenceDir 'qa-planar-collected10-fixture.json';$save|ConvertTo-Json -Depth 12|Set-Content $fixture -Encoding utf8
|
|
$result.fixtureProvenance.fixtureSha256=(Get-FileHash $fixture).Hash
|
|
Adb @('shell','am','force-stop',$package)|Out-Null;Adb @('push',$fixture,$remote)|Out-Null;Adb @('shell','rm','-f',($remote+'.bak'))|Out-Null
|
|
StartHome 'rapid-qa-home';SelectStage 'rapid-qa-restored' 20
|
|
$actual=@((Save 'rapid-qa-restored-state').puzzleStates|Where-Object stageId -eq 20)[0]
|
|
Check 'rapid-exact-labelled-fixture-restore' (Same $board $actual) 'Exact QA positions IDs memberships collected/counted camera, real30 completion source unchanged'
|
|
$points=@($actual.balls|ForEach-Object{,(BallPoint $actual $_.id)})
|
|
Check 'rapid-safe-native-tap-centres' (@($points|Where-Object{!(WorldSafe $_)}).Count-eq0) ($points|ConvertTo-Json -Compress)
|
|
Start-Sleep -Seconds 3
|
|
StartAudio 'audio-ten-rapid' 24
|
|
$result.rapidStartedUtc=[DateTime]::UtcNow.ToString('o')
|
|
for($pindex=0;$pindex-lt$points.Count;$pindex++){$p=$points[$pindex];Adb @('shell','input','tap',"$($p[0])","$($p[1])")|Out-Null;if($pindex-eq0){Adb @('shell','input','tap',"$($p[0])","$($p[1])")|Out-Null}}
|
|
$result.rapidFinishedUtc=[DateTime]::UtcNow.ToString('o')
|
|
$result.freeObservations=@();$freeObserved=$false
|
|
for($poll=0;$poll-lt30;$poll++){
|
|
$done=Save "twenty-rapid-observation-$poll"
|
|
$freeObserved=$done.latestFreePlay.sourceStageId-eq20-and@($done.puzzleStates|Where-Object stageId -eq 20).Count-eq0-and(Same $save.completedStageIds $done.completedStageIds)-and(Same $actual.balls $done.latestFreePlay.balls)-and(Same $actual.camera $done.latestFreePlay.camera)
|
|
$result.freeObservations+=@{poll=$poll;utc=[DateTime]::UtcNow.ToString('o');passed=$freeObserved;path=(Join-Path $EvidenceDir "twenty-rapid-observation-$poll.json")}
|
|
if($freeObserved){Copy-Item -LiteralPath (Join-Path $EvidenceDir "twenty-rapid-observation-$poll.json") -Destination (Join-Path $EvidenceDir 'twenty-rapid-after.json');break}
|
|
Start-Sleep -Milliseconds 50
|
|
}
|
|
Check 'twenty-native-rapid-complete' $freeObserved 'Bounded native input/save observation: ten direct taps plus duplicate remove replay20 and make exact same board Free20; original actual30 records unchanged'
|
|
$result.sameBoardFreeObservedUtc=[DateTime]::UtcNow.ToString('o')
|
|
# Wrapper launch precedes native PCM start: this elapsed time bounds Free observation conservatively.
|
|
$result.freeObservationPcmAgeUpperBoundSeconds=([DateTime]::UtcNow-$recorders[-1].startedUtc).TotalSeconds
|
|
Start-Sleep -Seconds 12
|
|
CompareAudio 'audio-ten-rapid' @(1..10|ForEach-Object{'count_{0:D2}'-f$_})
|
|
$matches=@(1..10|ForEach-Object{Get-Content (Join-Path $EvidenceDir ('audio-ten-rapid-count_{0:D2}-match.json'-f$_)) -Raw|ConvertFrom-Json})
|
|
for($i=1;$i-lt10;$i++){Check "rapid-fifo-$i" ($matches[$i-1].onsetSeconds+$matches[$i-1].referenceDurationSeconds-le$matches[$i].onsetSeconds+.015) 'Entire numeric clip completes before next onset'}
|
|
Check 'rapid-last-complete-before-record-end' ($matches[-1].onsetSeconds+$matches[-1].referenceDurationSeconds-le$matches[-1].capturedDurationSeconds) 'Entire tenth numeric clip retained after same-board Free transition'
|
|
Check 'same-board-free-before-last-word-completes' ($result.freeObservationPcmAgeUpperBoundSeconds-lt($matches[-1].onsetSeconds+$matches[-1].referenceDurationSeconds)) 'Actual Free state read immediately after native11 taps, before full tenth word ends; conservative age measured from wrapper launch before PCM started'
|
|
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'check-count-audio-residual.ps1') -CapturedPath (Join-Path $EvidenceDir 'audio-ten-rapid.wav') -MatchDir $EvidenceDir -Prefix 'audio-ten-rapid' -OutputPath (Join-Path $EvidenceDir 'rapid-no-extra-speech.json')
|
|
Check 'rapid-no-extra-or-duplicate-speech' ($LASTEXITCODE-eq0) 'All gaps and trailing PCM outside ten complete matched words are below PCM32 RMS'
|
|
$result.speechMatches=$matches
|
|
|
|
$result.sourceCommitEnd=(& git rev-parse HEAD);$result.sourceDirtyEnd=[bool](& git status --porcelain)
|
|
Check 'unchanged-source-end' (!$result.sourceDirtyEnd-and$result.sourceCommitEnd-eq$result.sourceCommit) $result.sourceCommitEnd
|
|
$result.status='passed'
|
|
}catch{$result.error=$_.Exception.Message;Write-Host $result.error -ForegroundColor Red}
|
|
finally {
|
|
foreach ($recorder in $recorders) {
|
|
if (!$recorder.process.HasExited -and !$recorder.process.WaitForExit(($recorder.seconds+30)*1000)) {
|
|
$result.status='failed'; $result.recorderError='Recorder exceeded completion bound; own native process stopped before restoration'
|
|
$ready=Get-Content -LiteralPath (Join-Path $EvidenceDir "$($recorder.name).wav.ready.json") -Raw | ConvertFrom-Json
|
|
$native=$recorder.nativeProcess
|
|
if ($native -and !$native.HasExited) { $native.Kill(); $native.WaitForExit() }
|
|
$recorder.process.WaitForExit(10000) | Out-Null
|
|
if (!$recorder.process.HasExited) { $recorder.process.Kill(); $recorder.process.WaitForExit() }
|
|
}
|
|
$readyPath=Join-Path $EvidenceDir "$($recorder.name).wav.ready.json"
|
|
if (Test-Path -LiteralPath $readyPath) {
|
|
$ready=Get-Content -LiteralPath $readyPath -Raw | ConvertFrom-Json
|
|
$native=$recorder.nativeProcess
|
|
if ($native -and !$native.HasExited -and !$native.WaitForExit(10000)) { throw 'Own native recorder did not terminate' }
|
|
$commands.Add(@{command='recorder termination boundary';recorderPid=$ready.recorderPid;wrapperPid=$recorder.process.Id;nativeExited=$true;wrapperExited=$recorder.process.HasExited;utc=[DateTime]::UtcNow.ToString('o')})
|
|
}
|
|
}
|
|
if ($musicBefore) {
|
|
try {
|
|
Adb @('shell','cmd','audio','set-volume','3',"$($musicBefore['2'])") | Out-Null
|
|
Adb @('shell','cmd','audio','set-device-volume','3',"$($musicBefore['8000'])",'32768') | Out-Null
|
|
Adb @('shell','cmd','audio','set-device-volume','3',"$($musicBefore['40000000'])",'1073741824') | Out-Null
|
|
$audioAfter=Adb @('shell','dumpsys','audio')
|
|
$audioAfter | Set-Content -LiteralPath (Join-Path $EvidenceDir 'music-after.txt') -Encoding utf8
|
|
$current=[regex]::Match($audioAfter,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
|
|
Check 'music-routes-restored' ($current -match "2 \(speaker\): $($musicBefore['2'])(,|$)" -and $current -match "8000 \(remote_submix\): $($musicBefore['8000'])(,|$)" -and $current -match "40000000 \(default\): $($musicBefore['40000000'])(,|$)") $current
|
|
} catch { $result.status='failed'; $result.volumeRestoreError=$_.Exception.Message }
|
|
}
|
|
if ($networkBefore) {
|
|
try {
|
|
Adb @('shell','cmd','connectivity','airplane-mode',$(if ($networkBefore.airplane -eq '1') {'enable'} else {'disable'})) | Out-Null
|
|
Adb @('shell','svc','wifi',$(if ($networkBefore.wifi -eq '1') {'enable'} else {'disable'})) | Out-Null
|
|
Adb @('shell','svc','data',$(if ($networkBefore.data -eq '1') {'enable'} else {'disable'})) | Out-Null
|
|
for ($attempt=0; $attempt -lt 30; $attempt++) {
|
|
$result.networkAfter=@{airplane=(Adb @('shell','settings','get','global','airplane_mode_on'));wifi=(Adb @('shell','settings','get','global','wifi_on'));data=(Adb @('shell','settings','get','global','mobile_data'))}
|
|
if (Same $networkBefore $result.networkAfter) { break }; Start-Sleep -Milliseconds 500
|
|
}
|
|
Check 'network-restored' (Same $networkBefore $result.networkAfter) ($result.networkAfter | ConvertTo-Json -Compress)
|
|
} catch { $result.status='failed'; $result.restoreError=$_.Exception.Message }
|
|
}
|
|
|
|
$result.checks=$checks.ToArray(); $result.commands=$commands.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
|
$result | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-final-rapid-evidence.json') -Encoding utf8
|
|
}
|
|
if ($result.status -ne 'passed') { exit 1 }
|
|
Write-Host "Final labelled rapid-count boundary checks passed: $($checks.Count) checks."
|
|
exit 0
|
|
|