remote/tools/landscape_proof.ps1
郑博文 e96502f888 test(tools): 4a 横屏验收脚本 + 既有 proof 适配自动隐藏与同源缺陷修复
新增 landscape_proof.ps1:V-C1~C7 判据(自动横屏 <=2s、旋转零中断、
动画桌面下 >=45fps、控制条自动隐藏、黑边唤出零远端泄漏、四按钮可点、
方向四态循环、表单竖屏)。方向判定只解析 Display: mDisplayId=0 块内
cur=WxH——这台 vivo 无 mRotation= 行,且 dumpsys 里有常驻 1080x2400 的
虚拟屏,匹配首个 cur= 会在已横屏时误报未旋转(探针实测踩过)。

横屏自动隐藏与既有 proof 的判据自冲突(HUD 随控制条 GONE,RTT= 读不到
被误判断线/未连接),三个 proof 统一加「确认隐藏态才走黑边唤醒」:
phone_chain_proof、ime_chinese_proof、rel_mouse_phone_proof。

修 rel_mouse_phone_proof 两处:
- 2c 基线判据:横屏滑动 768px 的残余相对事件成批延迟落地,旧的「两次
  读数相等」被突发间隙骗过(实测假失败 351/1418px)。改为 6 读数沉降 +
  4 次采样判漂移 + 绝对泄漏点特征比对。
- step 4 靶窗启动缺 3 个 Mandatory 参数,子进程瞬间失败退出(3b 起一直
  如此,右键落在裸屏幕中心,靠近违反 R-11「不得注入他人窗口」)。现按
  契约启动并把指针停在自家窗口内。

R-5 同源排查:靶窗结果目录固定路径 + 启动时删自己的 stop.flag,会让
上一轮失败遗留的靶窗永远收不到退休信号、继续抢前台并写脏 received.txt
(实测收到 abc\nabc)。5 个 proof 统一改每轮唯一目录(\47512 后缀)。

ime_chinese_proof 的切换键标定失效:设备 IME 已换成搜狗(原为讯飞),
旧坐标 (146,2096) 静默点空。IME 按键视图对 uiautomator 不可见、无法机械
标定,按 R-9 转人工确认,不硬凑自动化。
2026-11-01 21:58:19 +08:00

487 lines
24 KiB
PowerShell

# 4a landscape proof: rotation, full-height video, auto-hide controls, regressions.
#
# ASCII-only (PS 5.1 reads BOM-less .ps1 as GBK; personal lessons).
#
# Judgements (loop-4a-landscape-plan.md section 6):
# V-C1 connect to 1920x1080 -> app rotates landscape <=2s after live
# (LOCAL display cur= only: this vivo ROM has a permanent 1080x2400
# virtual display and prints no mRotation= line - probe lesson 2026-11-01)
# V-C2 with controls hidden, video view is 16:9 (err <2%) and >=90% screen height
# V-C3 frame counter strictly increases across rotation (session survives)
# V-C4 >=45 fps over a 2.5s window with an ACTIVE desktop animator
# (static desktop is source-limited ~25fps - measuring that would be a
# category error, probe lesson)
# V-C5 controls auto-hide ~3s idle; bottom-edge tap wakes them back
# V-C5b edge-band tap does NOT reach the remote (PC cursor delta <=1px)
# V-C5c all four buttons present after wake; mode tap changes and restores
# V-C6 form page is portrait
# V-C7 orientation button cycles AUTO->L->P->SYS->AUTO, display follows
#
# powershell -ExecutionPolicy Bypass -File tools\landscape_proof.ps1 -Host_ 192.168.1.112
param(
[string]$Host_ = "192.168.1.112",
[int]$Port = 47800,
[string]$Code = "t",
[string]$AgentExe = ""
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path "$PSScriptRoot\..").Path
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
$ev = Join-Path (Split-Path $repo -Parent) "evidence\4a"
New-Item -ItemType Directory -Force -Path $ev | Out-Null
# ---- Chinese fragments as code points (R-8) ----
$F_ORIENT = -join ([char]0x65B9,[char]0x5411,[char]0x3A) # "fangxiang:" (orientation:)
$F_AUTO = $F_ORIENT + -join ([char]0x81EA,[char]0x52A8) # label: orientation AUTO
$F_LAND = $F_ORIENT + -join ([char]0x6A2A,[char]0x5C4F) # label: orientation LANDSCAPE
$F_PORT = $F_ORIENT + -join ([char]0x7AD6,[char]0x5C4F) # label: orientation PORTRAIT
$F_SYS = $F_ORIENT + -join ([char]0x7CFB,[char]0x7EDF) # label: orientation SYSTEM
$F_CONNECT = -join ([char]0x8FDE,[char]0x63A5) # label: connect
$F_RECONNECT = -join ([char]0x91CD,[char]0x65B0,[char]0x8FDE,[char]0x63A5) # label: reconnect
$F_KB = -join ([char]0x952E,[char]0x76D8) # label: keyboard
$F_MODE = -join ([char]0x6A21,[char]0x5F0F,[char]0x3A) # "moshi:" (mode:)
$F_DISC = -join ([char]0x65AD,[char]0x5F00) # label: disconnect
function Adb {
param([Parameter(Mandatory)][string[]]$AdbArgs)
$old = $ErrorActionPreference; $ErrorActionPreference = "Continue"
try { $out = & $adb @AdbArgs 2>$null; @{ out = ($out -join "`n"); code = $LASTEXITCODE } }
finally { $ErrorActionPreference = $old }
}
function AdbShell { param([string[]]$Cmd) (Adb -AdbArgs (@("shell") + $Cmd)).out }
function Get-Ui {
[void](AdbShell @("uiautomator","dump","/sdcard/ls.xml"))
$r = Adb -AdbArgs @("pull","/sdcard/ls.xml","$env:TEMP\ls.xml")
if ($r.code -ne 0 -or -not (Test-Path "$env:TEMP\ls.xml")) { return "" }
return [System.IO.File]::ReadAllText("$env:TEMP\ls.xml", [System.Text.Encoding]::UTF8)
}
# uiautomator intermittently returns an empty/failed dump (measured 2026-11-01:
# a tap lookup got "" and the null-button tap then crashed with an empty-string
# argument). Retry until the tree is plausibly real; callers still assert on
# their own node - this only removes the transient, never masks a real absence.
function Get-UiRetry([int]$tries=4) {
$x = ""
for ($i=0; $i -lt $tries; $i++) {
$x = Get-Ui
if ($x.Length -gt 500) { return $x }
Start-Sleep -Milliseconds 400
}
Write-Host " [ui] retry exhausted, last dump len=$($x.Length)"
return $x
}
function VisibleTexts([string]$xml) {
[regex]::Matches($xml,'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) }
}
function ButtonByName([string]$xml, [string]$want, [bool]$prefix=$false) {
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
$hit = if ($prefix) { $t.StartsWith($want) } else { $t -eq $want }
if ($hit) {
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
if ($b.Success) {
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
return @{ cx=[int](($x1+$x2)/2); cy=[int](($y1+$y2)/2) }
}
}
}
return $null
}
function ButtonLabelByPrefix([string]$xml, [string]$prefix) {
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
if ($t.StartsWith($prefix)) { return $t }
}
return $null
}
# 4a race (measured 2026-11-01): each uiautomator dump costs ~2s, comparable to
# the 3s auto-hide timer - find-then-read sequences straddle expiry and labels
# come back empty. Every observation goes through the helpers below.
#
# CRITICAL SAFETY: the wake tap (screen bottom center) is only consumed by the
# app's edge band WHEN CONTROLS ARE HIDDEN. If controls are visible, that point
# lands on the control bar (mode/orientation buttons!) and would fire a real
# action. So a wake is allowed only after positively proving the hidden state:
# video View node present AND zero Button nodes.
function ControlsHidden([string]$ui) {
$anyBtn = ($ui -match 'class="android\.widget\.Button"')
$video = ($ui -match 'class="android\.view\.View"')
return ($video -and -not $anyBtn)
}
function Wake-Band {
# 4a design change (measured 2026-11-01): the wake zone is the LETTERBOX
# BLACK BAR, not a bottom edge band. With controls hidden the video is
# full-height, so bottom-center lies INSIDE the picture and would fire a
# real remote click. x=100 is outside the video in both states.
$d = LocalDisplay
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
Start-Sleep -Milliseconds 400
}
function Find-VisibleButton([string]$want, [bool]$prefix=$false) {
for ($try=0; $try -lt 5; $try++) {
$ui = Get-UiRetry
$b = ButtonByName $ui $want $prefix
if ($b) { return $b }
if (ControlsHidden $ui) { Wake-Band; continue }
if ($ui.Length -gt 500) { return $null } # real tree, genuine absence
Start-Sleep -Milliseconds 400 # empty-dump transient
}
return $null
}
function Tap-VisibleButton([string]$want, [bool]$prefix=$false) {
$b = Find-VisibleButton $want $prefix
if (-not $b) { return $null }
[void](AdbShell @("input","tap","$($b.cx)","$($b.cy)"))
return $b
}
# Label read that survives the hide race: wake only from a proven-hidden state.
function Read-ButtonLabel([string]$want, [bool]$prefix=$false) {
for ($try=0; $try -lt 6; $try++) {
$ui = Get-UiRetry
$l = ButtonLabelByPrefix $ui $want
if ($l) { return $l }
if (ControlsHidden $ui) { Wake-Band; continue }
if ($ui.Length -gt 500) { return $null }
Start-Sleep -Milliseconds 400
}
return $null
}
function FrameCount {
$m = [regex]::Match((Get-Ui), ([char]0x5E27 + '=(\d+)'))
if ($m.Success) { return [long]$m.Groups[1].Value }
return -1
}
# 4a self-conflict (measured 2026-11-01): once the controls auto-hide, the HUD
# TextView is GONE too - uiautomator's tree has NO text nodes and FrameCount
# returns -1 even though the session is alive (the video View is right there).
# Wake through the bottom edge band first: that tap is consumed by the app
# (zero remote events), so it cannot perturb the fps window.
function WakeControls {
$d = LocalDisplay
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
Start-Sleep -Milliseconds 500
}
function FrameCountOrWake {
$f = FrameCount
if ($f -ge 0) { return $f }
WakeControls
return FrameCount
}
function LocalDisplay {
$d = (AdbShell @("dumpsys","window","displays"))
$idx = $d.IndexOf("Display: mDisplayId=0")
if ($idx -lt 0) { return @{ w=0; h=0 } }
$m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)')
if (-not $m.Success) { return @{ w=0; h=0 } }
return @{ w=[int]$m.Groups[1].Value; h=[int]$m.Groups[2].Value }
}
function Wake {
[void](AdbShell @("input","keyevent","224"))
Start-Sleep -Milliseconds 250
[void](AdbShell @("wm","dismiss-keyguard"))
Start-Sleep -Milliseconds 350
}
function Restart-AppForm {
Wake
[void](AdbShell @("am","force-stop","com.verify.remote"))
Start-Sleep -Milliseconds 600
[void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity"))
$deadline = (Get-Date).AddSeconds(8)
while ((Get-Date) -lt $deadline) {
$x = Get-Ui
if (([regex]::Matches($x,'class="android\.widget\.EditText"')).Count -ge 3) { return $x }
Start-Sleep -Milliseconds 300
}
throw "form did not appear: [$((VisibleTexts (Get-Ui)) -join ' || ')]"
}
function CenterOf($m) {
$a=[int]$m.Groups[1].Value; $b=[int]$m.Groups[2].Value
$c=[int]$m.Groups[3].Value; $e=[int]$m.Groups[4].Value
@([int](($a+$c)/2), [int](($b+$e)/2))
}
function TypeAscii([string]$s) {
foreach ($ch in $s.ToUpper().ToCharArray()) {
$k = if ($ch -ge '0' -and $ch -le '9') { 7 + [int][string]$ch }
elseif ($ch -ge 'A' -and $ch -le 'Z') { 29 + ([int]$ch - [int][char]'A') }
elseif ($ch -eq '.') { 56 } else { throw "unsupported char $ch" }
[void](AdbShell @("input","keyevent","$k"))
Start-Sleep -Milliseconds 70
}
}
function ClearField($c) {
for ($try=0; $try -lt 3; $try++) {
[void](AdbShell @("input","tap","$($c[0])","$($c[1])"))
Start-Sleep -Milliseconds 250
for ($i=0; $i -lt 40; $i++) { [void](AdbShell @("input","keyevent","67")) }
Start-Sleep -Milliseconds 120
$fx = Get-Ui
$focused = $false
$fi = -1
foreach ($n in [regex]::Matches($fx,'<node[^>]*>')) {
if ($n.Value -match 'class="android\.widget\.EditText"') {
$fi++
if ($n.Value -match 'focused="true"') { $focused = $true }
}
}
if ($focused) { return }
}
throw "field focus failed"
}
# PC-side cursor (V-C5b) + desktop animator (V-C4)
Add-Type -TypeDefinition @"
using System; using System.Runtime.InteropServices;
public class Cur4a {
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr c);
public static void Dpi(){ SetProcessDpiAwarenessContext(new IntPtr(-4)); }
public static int[] Get(){ POINT p; GetCursorPos(out p); return new int[]{p.X,p.Y}; }
public static void Set(int x,int y){ SetCursorPos(x,y); }
}
"@
[Cur4a]::Dpi()
Write-Host "=== 0. memory precheck (probe lesson: OOM fakes failures) ==="
$os = Get-CimInstance Win32_OperatingSystem
$freeGB = $os.FreeVirtualMemory/1MB
Write-Host (" commit free: {0:N1} GB" -f $freeGB)
if ($freeGB -lt 0.5) { throw "commit memory critically low - free RAM first (gradle --stop)" }
Write-Host "=== 1. stale-agent guard (R-7) ==="
if (-not $AgentExe) { $AgentExe = Join-Path $repo "target\release\agent.exe" }
$ag = @(Get-Process agent -ErrorAction SilentlyContinue)
if ($ag.Count -gt 0 -and (Test-Path $AgentExe) -and ((Get-Item $AgentExe).LastWriteTime -gt $ag[0].StartTime)) {
Write-Host " agent predates build; restarting"
Stop-Process -Name agent -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 800; $ag = @()
}
if ($ag.Count -eq 0) {
Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 2; Write-Host " agent started"
} else { Write-Host " agent running and current" }
$origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim()
if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","disable",$origIme)) }
[void](AdbShell @("svc","power","stayon","true"))
try {
# =====================================================================
Write-Host "=== 2. V-C6: form is portrait ==="
$x = Restart-AppForm
$d = LocalDisplay
Write-Host (" form display: {0}x{1}" -f $d.w, $d.h)
if (-not ($d.w -lt $d.h)) { throw "V-C6 FAIL: form not portrait ($($d.w)x$($d.h))" }
Write-Host " PASS: form portrait"
# =====================================================================
Write-Host "=== 3. V-C1: connect -> AUTO rotates landscape <=2s after live ==="
$F_edits = [regex]::Matches($x,'<node[^>]*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
ClearField (CenterOf $F_edits[0]); TypeAscii $Host_
ClearField (CenterOf $F_edits[1]); TypeAscii "$Port"
ClearField (CenterOf $F_edits[2]); TypeAscii $Code
# Read back until 3 EditText nodes are actually present: uiautomator
# intermittently returns a truncated/empty tree (measured 2026-11-01, worse
# under memory pressure) and a single Get-Ui then reads "form now: " empty
# and fails the fill check as a false negative.
$vals = @()
for ($rd=0; $rd -lt 5; $rd++) {
$x2 = Get-UiRetry
$vals = @()
foreach ($n in [regex]::Matches($x2,'<node[^>]*>')) {
if ($n.Value -match 'class="android\.widget\.EditText"') {
$vals += [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
}
}
if ($vals.Count -ge 3) { break }
Write-Host " form readback got $($vals.Count) fields, retry $rd"
Start-Sleep -Milliseconds 500
}
Write-Host " form now: $($vals -join ' | ')"
if ($vals.Count -lt 3) { throw "form fill FAIL: only $($vals.Count) fields after retries" }
if ($vals[0] -ne $Host_ -or $vals[1] -ne "$Port" -or $vals[2] -ne $Code) { throw "form fill FAIL" }
$btn = ButtonByName $x2 $F_CONNECT
if (-not $btn) { $btn = ButtonByName $x2 $F_RECONNECT }
if (-not $btn) { $btn = Find-VisibleButton $F_CONNECT; if (-not $btn) { $btn = Find-VisibleButton $F_RECONNECT } }
if (-not $btn) { throw "connect button not found" }
[void](AdbShell @("input","tap","$($btn.cx)","$($btn.cy)"))
$deadline = (Get-Date).AddSeconds(20); $live = $false
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 500
if ((Get-Ui) -match "RTT=") { $live = $true; break }
}
if (-not $live) { throw "never reached live: [$((VisibleTexts (Get-Ui)) -join ' || ')]" }
$tRot = Get-Date
$deadline = $tRot.AddSeconds(2); $rotated = $false
while ((Get-Date) -lt $deadline) {
$d = LocalDisplay
if ($d.w -gt $d.h) { $rotated = $true; break }
Start-Sleep -Milliseconds 150
}
$ms = ((Get-Date) - $tRot).TotalMilliseconds
Write-Host (" display {0}x{1}, landscape within {2:N0}ms of live" -f $d.w,$d.h,$ms)
if (-not $rotated) { throw "V-C1 FAIL: still portrait 2s after live" }
Write-Host " PASS: AUTO rotated to landscape"
# =====================================================================
Write-Host "=== 4. V-C3 + V-C4: counter monotonic; >=45fps with animated desktop ==="
# Static desktop is source-limited (~25fps): the honest window needs motion.
$anim = Start-Job -ScriptBlock {
Add-Type -TypeDefinition @"
using System; using System.Runtime.InteropServices;
public class An { [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); }
"@
$end=(Get-Date).AddSeconds(14); $i=0
while ((Get-Date) -lt $end) {
$a=$i*0.18
[An]::SetCursorPos([int](960+320*[Math]::Cos($a)), [int](540+320*[Math]::Sin($a))) | Out-Null
Start-Sleep -Milliseconds 12; $i++
}
}
Start-Sleep -Seconds 2 # let the encoder see motion
$fA = FrameCountOrWake; $ta = Get-Date
Start-Sleep -Milliseconds 2500
$fB = FrameCountOrWake; $tb = Get-Date
$fps = if ($fA -ge 0 -and $fB -gt $fA) { [Math]::Round(($fB-$fA)/(($tb-$ta).TotalSeconds),1) } else { -1 }
Write-Host " frames $fA -> $fB fps=$fps (animated)"
if ($fB -le $fA) { throw "V-C3 FAIL: counter stalled across rotation" }
Write-Host " PASS: V-C3 session survived rotation"
if ($fps -lt 45) { throw "V-C4 FAIL: $fps fps < 45 with animated desktop" }
Write-Host " PASS: V-C4 fps >= 45"
# =====================================================================
Write-Host "=== 5. V-C5: wake -> visible -> idle -> hidden ==="
# R-8 discipline: "button absent" is NOT a valid judgement on its own - an
# empty/broken dump would pass it trivially (measured 2026-11-01: after the
# hide, the HUD TextView is GONE too, so the tree has zero text nodes).
# Positive pair: keyboard button absent WHILE the video View node is present.
#
# Sequence is explicit (first run's implicit precondition raced: the fps
# step's wake expired its 3s timer before we checked, so controls were
# already hidden - correct product behavior, wrong harness assumption):
# wake -> assert visible -> idle -> assert hidden.
WakeControls
$x5 = Get-Ui
if (-not (ButtonByName $x5 $F_KB)) { throw "V-C5 FAIL: edge wake did not bring controls back" }
Write-Host " visible after wake: OK"
$deadline = (Get-Date).AddSeconds(6); $hidden = $false
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 600
$x5 = Get-Ui
$videoSeen = ($x5 -match 'class="android\.view\.View"')
$kbGone = -not (ButtonByName $x5 $F_KB)
if ($videoSeen -and $kbGone) { $hidden = $true; break }
}
if (-not $hidden) {
throw "V-C5 FAIL: no (kb-gone AND video-present) state within 6s; last dump len=$($x5.Length)" }
Write-Host " PASS: controls auto-hidden (video View still in tree - real hide, not empty dump)"
Write-Host "=== 5b. V-C2: video 16:9 and >=90% screen height (controls hidden) ==="
$ui = Get-UiRetry
$best = $null; $bestErr = 1e9
foreach ($n in [regex]::Matches($ui,'<node[^>]*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) {
$x1=[int]$n.Groups[1].Value; $y1=[int]$n.Groups[2].Value
$x2i=[int]$n.Groups[3].Value; $y2=[int]$n.Groups[4].Value
$w=$x2i-$x1; $h=$y2-$y1
if ($w -lt 300 -or $h -lt 100) { continue }
$err = [Math]::Abs($w/[double]$h - 16.0/9.0)
if ($err -lt $bestErr) { $bestErr=$err; $best=@{w=$w;h=$h} }
}
$d = LocalDisplay
$hFrac = $best.h / [double]$d.h
Write-Host (" video {0}x{1} ratio err {2:N4} height {3:N1}% of {4}x{5}" -f `
$best.w,$best.h,$bestErr,($hFrac*100),$d.w,$d.h)
if ($bestErr -gt 0.02) { throw "V-C2 FAIL: video not 16:9 (err $bestErr)" }
if ($hFrac -lt 0.90) { throw ("V-C2 FAIL: video height {0:N1}% < 90%" -f ($hFrac*100)) }
Write-Host " PASS: full-height 16:9 video (the tiny-video fix)"
Stop-Job $anim -ErrorAction SilentlyContinue; Remove-Job $anim -Force -ErrorAction SilentlyContinue
Write-Host "=== 6. V-C5b: black-bar tap wakes controls WITHOUT touching the remote ==="
Start-Sleep -Seconds 2 # animator fully stopped
# The wake zone IS the letterbox black bar now (4a design change), so this
# is the honest target. Also assert a video-interior tap is NOT consumed as
# a wake - otherwise the black bar would have eaten real clicks too.
$pc0 = [Cur4a]::Get()
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
Start-Sleep -Milliseconds 600
$pc1 = [Cur4a]::Get()
$ui = Get-UiRetry
$kb = ButtonByName $ui $F_KB
$jump = [Math]::Max([Math]::Abs($pc1[0]-$pc0[0]), [Math]::Abs($pc1[1]-$pc0[1]))
Write-Host (" black-bar tap: cursor jump=$jump px ; keyboard back: {0}" -f ($null -ne $kb))
if (-not $kb) { throw "V-C5 FAIL: black-bar tap did not wake controls" }
if ($jump -gt 1) { throw "V-C5b FAIL: black-bar tap leaked $jump px to remote" }
Write-Host " PASS: wake works, zero remote leakage"
Write-Host "=== 6c. four buttons present; mode tap changes and restores ==="
$kb2 = Find-VisibleButton $F_KB
$mode = Find-VisibleButton $F_MODE $true
$disc = Find-VisibleButton $F_DISC
$orient = Find-VisibleButton $F_ORIENT $true
if (-not ($kb2 -and $mode -and $disc -and $orient)) {
throw ("V-C5 FAIL: missing buttons kb={0} mode={1} disc={2} orient={3}" -f `
($null -ne $kb2),($null -ne $mode),($null -ne $disc),($null -ne $orient)) }
$beforeLabel = Read-ButtonLabel $F_MODE $true
if (-not $beforeLabel) { throw "V-C5 precondition FAIL: mode label unreadable" }
[void](Tap-VisibleButton $F_MODE $true)
Start-Sleep -Milliseconds 600
$afterLabel = Read-ButtonLabel $F_MODE $true
Write-Host " mode label: [$beforeLabel] -> [$afterLabel]"
# null would trivially "change" from a real label - assert non-empty too (R-8)
if (-not $afterLabel -or $beforeLabel -eq $afterLabel) { throw "V-C5 FAIL: mode tap changed nothing" }
# converge back to relative (the product default the later proofs assume),
# regardless of which way the flip above landed - tap until it reads relative.
$F_MODE_REL = $F_MODE + -join ([char]0x76F8,[char]0x5BF9) # mode: relative
$converged = $false
for ($i=0; $i -lt 3 -and -not $converged; $i++) {
$cur = Read-ButtonLabel $F_MODE $true
if ($cur -eq $F_MODE_REL) { $converged = $true; break }
[void](Tap-VisibleButton $F_MODE $true)
Start-Sleep -Milliseconds 600
}
$restored = Read-ButtonLabel $F_MODE $true
Write-Host " mode converged to relative: [$restored]"
if (-not $converged -or $restored -ne $F_MODE_REL) {
throw "V-C5 FAIL: mode not converged to relative (pollutes later proofs)" }
Write-Host " PASS: buttons work, state restored"
# =====================================================================
Write-Host "=== 7. V-C7: orientation cycle AUTO->L->P->SYS->AUTO ==="
$seq = @($F_LAND, $F_PORT, $F_SYS, $F_AUTO)
$wantLand = @($true, $false, $null, $true) # null = don't assert SYSTEM policy
for ($i=0; $i -lt 4; $i++) {
$orient = Find-VisibleButton $F_ORIENT $true
if (-not $orient) { throw "V-C7 FAIL: orientation button unreachable after 5 wakes" }
[void](Tap-VisibleButton $F_ORIENT $true)
Start-Sleep -Milliseconds 1400
$label = Read-ButtonLabel $F_ORIENT $true
$d = LocalDisplay
$land = $d.w -gt $d.h
Write-Host (" step {0}: label=[{1}] want=[{2}] display={3}x{4} landscape={5}" -f ($i+1),$label,$seq[$i],$d.w,$d.h,$land)
if ($label -ne $seq[$i]) { throw ("V-C7 FAIL: step {0} label [{1}] != [{2}]" -f ($i+1),$label,$seq[$i]) }
if ($null -ne $wantLand[$i] -and $land -ne $wantLand[$i]) {
throw ("V-C7 FAIL: step {0} landscape={1} want={2}" -f ($i+1),$land,$wantLand[$i]) }
}
Write-Host " PASS: full cycle with display following each policy"
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-c7-final.png" | Out-Null
}
finally {
# 4a pollution discipline: leave no live session behind - the next proof's
# Restart-AppForm force-stops the app mid-session, which strands the agent's
# serial accept in a zombie (N11) and the run after that fails to connect
# (measured 2026-11-01: agent alive but no longer serving handshakes).
[void](AdbShell @("am","force-stop","com.verify.remote"))
Start-Sleep -Milliseconds 500
Get-Job | Stop-Job -ErrorAction SilentlyContinue
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue
[void](AdbShell @("svc","power","stayon","false"))
if ($origIme -and $origIme -ne "null") {
[void](AdbShell @("ime","enable",$origIme))
[void](AdbShell @("ime","set",$origIme))
}
}
Write-Host "=== LANDSCAPE PROOF COMPLETE ==="