[CmdletBinding()] param( [string]$AdbPath = 'D:\Android\Sdk\platform-tools\adb.exe', [string]$ServerBaseUrl = 'http://127.0.0.1:8000', [string]$TokenFile = "$env:LOCALAPPDATA\Temp\n7-device-token.txt", [string]$PackageName = 'com.seeker.n7cockpit.remote', [string]$ModeId = 'baby_nap', [int]$TimeoutSeconds = 40, [string]$ArtifactDirectory = '' ) $ErrorActionPreference = 'Stop' $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if (-not $ArtifactDirectory) { $ArtifactDirectory = Join-Path $projectRoot 'artifacts\remote-link' } $buildConfig = Join-Path $projectRoot 'app\build\generated\source\buildConfig\debug\com\seeker\n7cockpit\BuildConfig.java' if (-not (Test-Path -LiteralPath $TokenFile)) { throw "The device token file does not exist: $TokenFile" } if (-not (Test-Path -LiteralPath $buildConfig)) { throw 'Debug BuildConfig was not found. Build the remote debug APK first.' } $configText = Get-Content -LiteralPath $buildConfig -Raw -Encoding UTF8 if ($configText -notmatch 'ORCHESTRATOR_EXECUTION_TARGET\s*=\s*"dry_run"') { throw 'Safety interlock: the APK execution target is not dry_run.' } if ($configText -notmatch 'ORCHESTRATOR_BASE_URL\s*=\s*"http://10\.0\.2\.2:8000"') { throw 'This test only permits an emulator APK targeting 10.0.2.2:8000.' } $token = (Get-Content -LiteralPath $TokenFile -Raw).Trim() if (-not $token) { throw 'The device token is empty.' } $headers = @{ Authorization = "Bearer $token" } $catalog = Invoke-RestMethod -Uri "$ServerBaseUrl/api/v1/vehicles/denza_n7/modes" ` -Headers $headers -TimeoutSec 5 $mode = @($catalog.modes | Where-Object { $_.id -eq $ModeId -and $_.origin -eq 'preset' }) if ($mode.Count -ne 1) { throw "The preset is not unique in the server catalog: $ModeId" } $expectedModeName = [string]$mode[0].name & (Join-Path $PSScriptRoot 'start-remote-app.ps1') -AdbPath $AdbPath ` -PackageName $PackageName | Out-Null Start-Sleep -Seconds 1 # Standard 2560x1600 emulator: mode navigation, then the first preset button. & $AdbPath shell input tap 94 406 Start-Sleep -Seconds 2 New-Item -ItemType Directory -Force -Path $ArtifactDirectory | Out-Null & $AdbPath shell uiautomator dump /sdcard/n7-before-mode.xml | Out-Null & $AdbPath pull /sdcard/n7-before-mode.xml (Join-Path $ArtifactDirectory 'before-mode.xml') | Out-Null $beforeUi = Get-Content -LiteralPath (Join-Path $ArtifactDirectory 'before-mode.xml') ` -Raw -Encoding UTF8 if (-not $beforeUi.Contains('http://10.0.2.2:8000')) { throw 'The Android mode page did not render its server connection.' } if (-not $beforeUi.Contains($expectedModeName)) { throw "The Android mode page did not render the preset: $ModeId" } $baseline = Invoke-RestMethod ` -Uri "$ServerBaseUrl/api/v1/vehicles/denza_n7/events?after=0" ` -Headers $headers -TimeoutSec 5 $sequence = [long]$baseline.next_sequence & $AdbPath shell input tap 1469 444 $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $observed = New-Object System.Collections.Generic.List[object] $terminal = $null do { Start-Sleep -Seconds 1 $batch = Invoke-RestMethod ` -Uri "$ServerBaseUrl/api/v1/vehicles/denza_n7/events?after=$sequence" ` -Headers $headers -TimeoutSec 5 foreach ($event in @($batch.events)) { $execution = $event.execution if (-not $execution -and $event.data) { $execution = $event.data.execution } if (-not $execution -or $execution.mode_id -ne $ModeId -or $execution.source -ne 'android') { continue } $observed.Add([pscustomobject]@{ sequence = [long]$event.sequence execution_id = $execution.execution_id status = $execution.status target = $execution.execution_target }) if ($execution.status -in @('succeeded', 'failed', 'cancelled')) { $terminal = $execution } } $sequence = [long]$batch.next_sequence } while (-not $terminal -and (Get-Date) -lt $deadline) if (-not $terminal) { throw "No terminal mode event was received within $TimeoutSeconds seconds." } if ($terminal.execution_target -ne 'dry_run') { throw "Safety failure: server execution target was $($terminal.execution_target)." } if ($terminal.status -ne 'succeeded') { throw "Mode execution failed: $($terminal.status) $($terminal.error)" } $statusPath = Join-Path $ArtifactDirectory 'after-execution.xml' $screenPath = Join-Path $ArtifactDirectory 'after-execution.png' Start-Sleep -Seconds 2 & $AdbPath shell uiautomator dump /sdcard/n7-after-execution.xml | Out-Null & $AdbPath pull /sdcard/n7-after-execution.xml $statusPath | Out-Null & $AdbPath shell screencap -p /sdcard/n7-after-execution.png | Out-Null & $AdbPath pull /sdcard/n7-after-execution.png $screenPath | Out-Null $afterUi = Get-Content -LiteralPath $statusPath -Raw -Encoding UTF8 if (-not $afterUi.Contains($expectedModeName) -or -not $afterUi.Contains('100%')) { throw 'The server task succeeded, but Android did not render terminal progress.' } if (-not $afterUi.Contains($terminal.execution_id)) { throw 'The Android execution_id does not match the server execution_id.' } if (-not $afterUi.Contains('N7Cockpit')) { throw 'Android did not render the expected source identity.' } $statuses = @($observed | Sort-Object sequence | Select-Object -ExpandProperty status) foreach ($required in @('queued', 'executing', 'succeeded')) { if ($required -notin $statuses) { throw "The event stream is missing state: $required" } } [pscustomobject]@{ Passed = $true SafetyTarget = $terminal.execution_target CanExecuted = $false ModeId = $terminal.mode_id ExecutionId = $terminal.execution_id EventStates = ($statuses -join ' -> ') AndroidStateSynchronized = $true Screenshot = $screenPath }