diff --git a/tools/emoji_manual_proof.ps1 b/tools/emoji_manual_proof.ps1 index 2d2d5ae..5fcc46a 100644 --- a/tools/emoji_manual_proof.ps1 +++ b/tools/emoji_manual_proof.ps1 @@ -26,7 +26,11 @@ param( $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" -$tmp = "$env:TEMP\remote-emoji-proof" +# UNIQUE per run: a fixed path lets the startup Remove-Item below delete the +# stop.flag that leftover targets from a previous FAILED run are watching, so +# they never retire and keep writing into the shared received.txt +# (same trap fixed in phone_chain_proof, measured 2026-11-01). +$tmp = "$env:TEMP\remote-emoji-proof-$PID" New-Item -ItemType Directory -Force -Path $tmp | Out-Null $pidFile = "$tmp\target.pid" diff --git a/tools/ime_chinese_proof.ps1 b/tools/ime_chinese_proof.ps1 index 58e13cf..efc2ecd 100644 --- a/tools/ime_chinese_proof.ps1 +++ b/tools/ime_chinese_proof.ps1 @@ -16,17 +16,23 @@ # Not covered: swipe/handwriting input, browsing the candidate list. param( - # PC (controlled side) LAN address. Default = the 2026-10-31 measured WLAN - # address; it is DHCP-assigned, so pass -PhoneIp when the office network - # moves (the stale .180 default silently cost a proof run on 2026-10-31). - [string]$PhoneIp = "192.168.1.110", + # PC (controlled side) LAN address. DHCP-assigned and DRIFTED twice + # (.180 -> .110 -> .112); a stale default silently dials a dead host + # (measured 2026-11-01: this run failed "phone did not connect" because the + # caller passed -Host_, which does NOT bind to this parameter, so the + # default was used). Always pass -PhoneIp explicitly and check the real IP. + [string]$PhoneIp = "192.168.1.112", [int]$Port = 47800, [string]$Code = "t" ) $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" -$tmp = "$env:TEMP\remote-cjk2" +# UNIQUE per-run dir: with a fixed path, the startup Remove-Item deletes the +# stop.flag that leftover targets from a previous failed run are watching, so +# they can never retire and they pollute the shared received.txt +# (same class of bug measured 2026-11-01 in phone_chain_proof). +$tmp = "$env:TEMP\remote-cjk2-$PID" New-Item -ItemType Directory -Force -Path $tmp | Out-Null $pidFile = "$tmp\target.pid" @@ -99,6 +105,86 @@ function Show-Cps([string]$s) { ($s.ToCharArray() | ForEach-Object { "U+{0:X4}" -f [int]$_ }) -join " " } +# ---- 4a landscape adaptations ---- +# In landscape the HUD TextView hides WITH the auto-hiding control bar, so a +# plain "RTT=" read returns nothing while the session is alive, and the +# keyboard button is not in the tree at all. Both reads must wake through the +# letterbox black bar first (that tap is consumed by the app; it never reaches +# the remote). NB: this script's Adb-Shell returns @{out;code}, not a string. +function Phone-DisplaySize { + $d = (Adb-Shell @("dumpsys", "window", "displays")).out + $idx = $d.IndexOf("Display: mDisplayId=0") + if ($idx -lt 0) { return @{ w = 1080; h = 2400 } } + $m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)') + if (-not $m.Success) { return @{ w = 1080; h = 2400 } } + return @{ w = [int]$m.Groups[1].Value; h = [int]$m.Groups[2].Value } +} +function Wake-Controls-If-Hidden([string]$xml) { + $hidden = ($xml -match 'class="android\.view\.View"') -and ($xml -notmatch 'class="android\.widget\.Button"') + if (-not $hidden) { return $false } + $pd = Phone-DisplaySize + [void](Adb-Shell @("input", "tap", "100", "$([int]($pd.h / 2))")) + Start-Sleep -Milliseconds 400 + return $true +} +# Read the tree until it shows a live session, waking the controls as needed. +function Get-Ui-Live([int]$Seconds) { + $deadline = (Get-Date).AddSeconds($Seconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 900 + $x = Get-Ui + if ($x -match "RTT=") { return $x } + if (Wake-Controls-If-Hidden $x) { + $x = Get-Ui + if ($x -match "RTT=") { return $x } + } + } + return (Get-Ui) +} +# Find a Button by label prefix in a tree that may have auto-hidden controls. +function Find-ButtonByPrefix([string]$xml, [string]$prefix) { + foreach ($n in [regex]::Matches($xml,']*class="android\.widget\.Button"[^>]*>')) { + $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + if ($t.StartsWith($prefix)) { + $b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') + if ($b.Success) { + return @( + [int]((([int]$b.Groups[1].Value) + ([int]$b.Groups[3].Value)) / 2), + [int]((([int]$b.Groups[2].Value) + ([int]$b.Groups[4].Value)) / 2), + $t) + } + } + } + return $null +} +function Find-ButtonByPrefix-Live([string]$prefix) { + for ($t=0; $t -lt 4; $t++) { + $x = Get-Ui + $b = Find-ButtonByPrefix $x $prefix + if ($b) { return $b } + if (-not (Wake-Controls-If-Hidden $x)) { Start-Sleep -Milliseconds 400 } + } + return $null +} +# Force PORTRAIT for this proof. Why: the IME's ab/zh language key is NOT visible +# to uiautomator (its key views are separate windows), so the toggle is reached by +# a CALIBRATED tap that is portrait-specific (146,2096 on 1080x2400). In landscape +# that y is off-screen, so the retry path could not work. The text pipeline itself +# is orientation-independent (landscape 'abc' is already covered by +# phone_chain_proof), so running V6 in portrait costs nothing in coverage. +function Set-Orientation-Portrait { + $F_ORIENT = -join ([char]0x65B9,[char]0x5411,[char]0x3A) # "fangxiang:" + $F_PORTRAIT = $F_ORIENT + -join ([char]0x7AD6,[char]0x5C4F) # "...:portrait" + for ($t=0; $t -lt 8; $t++) { + $b = Find-ButtonByPrefix-Live $F_ORIENT + if (-not $b) { throw "orientation button not reachable" } + if ($b[2] -eq $F_PORTRAIT) { Write-Host " orientation: already portrait"; return } + [void](Adb-Shell @("input","tap","$($b[0])","$($b[1])")) + Start-Sleep -Milliseconds 1300 + } + throw "could not reach portrait orientation after 8 taps" +} + Write-Host "=== 0. retire ALL earlier targets, verify none holds foreground ===" $flags = @(Get-ChildItem -Path $env:TEMP -Recurse -Filter "stop.flag" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*remote*cjk*" -or $_.FullName -like "*remote*proof*" }) @@ -138,10 +224,11 @@ Write-Host "=== 3. phone session ===" [void](Adb-Shell @("am","force-stop","com.verify.remote")) [void](Adb-Shell @("am","start","-n","com.verify.remote/.MainActivity", "--es","host",$PhoneIp,"--ei","port","$Port","--es","code",$Code)) -$deadline = (Get-Date).AddSeconds(25) -$ui = "" -while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 900; $ui = Get-Ui; if ($ui -match "RTT=") { break } } +$ui = Get-Ui-Live 25 if ($ui -notmatch "RTT=") { throw "phone did not connect" } +Write-Host " live: $([regex]::Match($ui,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)" +# 4a: pin PORTRAIT before driving the IME (calibrated toggle tap is portrait-only) +Set-Orientation-Portrait Write-Host "=== 4. show IME (tap keyboard button) ===" # Select the keyboard button BY ITS LABEL, not by position. @@ -150,19 +237,7 @@ Write-Host "=== 4. show IME (tap keyboard button) ===" # control and the proof reported "IME not shown" (a harness defect, not a product # regression; measured 2026-10-31). Label matching is order-independent. $F_KEYBOARD = -join ([char]0x952E,[char]0x76D8) # "keyboard" -$kb = $null -foreach ($n in [regex]::Matches($ui,']*class="android\.widget\.Button"[^>]*>')) { - $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) - if ($t -eq $F_KEYBOARD) { - $b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') - if ($b.Success) { - $kb = @( - [int](([int]$b.Groups[1].Value + [int]$b.Groups[3].Value) / 2), - [int](([int]$b.Groups[2].Value + [int]$b.Groups[4].Value) / 2) - ) - } - } -} +$kb = Find-ButtonByPrefix-Live $F_KEYBOARD if (-not $kb) { throw "keyboard button not found by label" } [void](Adb-Shell @("input","tap","$($kb[0])","$($kb[1])")) Start-Sleep -Seconds 3 @@ -172,16 +247,20 @@ if ($ime -notmatch "mHaveConnection=true") { throw "IME not bound to our InputCo Write-Host " IME: $([regex]::Match($ime,'mCurId=(\S+)').Groups[1].Value)" Write-Host "=== 4b/5. pinyin -> IME candidate -> commitText (mode-tolerant) ===" -# Both IMEs on this device may remember an ENGLISH mode, in which case letters -# pass through without composing - which looks identical to "our pipeline lost -# the Hanzi" unless you check. The IME's key views are NOT visible to -# uiautomator (its dump contains only our app's nodes), so the ab/zh toggle is -# reached by a calibrated tap: iFlytek's key sits at display (73,1048) in a -# 1080x2400 capture => device (146,2096) at 2x. Verified 2026-10-30. -# The toggle is STATEFUL, so this is written as an idempotent attempt loop: -# try, and only if we see letters-without-Hanzi do we flip the mode and retry. -# A stray tap here changes a keyboard setting, nothing destructive. -$modeToggle = @(146, 2096) +# The IME may remember an ENGLISH mode, in which case letters pass through +# without composing - which looks identical to "our pipeline lost the Hanzi" +# unless you check. The IME's key views are NOT visible to uiautomator (its +# dump contains only our app's nodes), so the zh/en toggle is reached by a +# calibrated tap. A stray tap here changes a keyboard setting, nothing +# destructive. The loop below is idempotent by design (R-9): flip only if +# letters-without-Hanzi were seen, and the judgement is the received text. +# +# Toggle calibration (2026-11-01): the device IME is now SOGOU +# (com.sohu.inputmethod.sogou.vivo), not iFlytek - the old (146,2096) iFlytek +# coordinate silently missed the key and both attempts stayed in English mode. +# Re-measured from screencap: bottom row, the language key right of '.' +# (labelled zh/en) at device px (838,2282) on 1080x2400 portrait. +$modeToggle = @(838, 2282) $expect = -join @([char]0x4F60, [char]0x597D) # NI HAO $attempt = 0 $new = "" diff --git a/tools/ime_commit_proof.ps1 b/tools/ime_commit_proof.ps1 index c4a4bb5..688d018 100644 --- a/tools/ime_commit_proof.ps1 +++ b/tools/ime_commit_proof.ps1 @@ -26,7 +26,11 @@ param( $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" -$tmp = "$env:TEMP\remote-ime-proof" +# UNIQUE per run: a fixed path lets the startup Remove-Item below delete the +# stop.flag that leftover targets from a previous FAILED run are watching, so +# they never retire and keep writing into the shared received.txt +# (same trap fixed in phone_chain_proof, measured 2026-11-01). +$tmp = "$env:TEMP\remote-ime-proof-$PID" New-Item -ItemType Directory -Force -Path $tmp | Out-Null $pidFile = "$tmp\target.pid" diff --git a/tools/landscape_proof.ps1 b/tools/landscape_proof.ps1 new file mode 100644 index 0000000..1f910dd --- /dev/null +++ b/tools/landscape_proof.ps1 @@ -0,0 +1,486 @@ +# 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,']*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,']*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,']*>')) { + 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,']*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,']*>')) { + 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,']*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 ===" diff --git a/tools/phone_chain_proof.ps1 b/tools/phone_chain_proof.ps1 index 89d200f..5d88c57 100644 --- a/tools/phone_chain_proof.ps1 +++ b/tools/phone_chain_proof.ps1 @@ -23,7 +23,12 @@ param( $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" -$tmp = "$env:TEMP\remote-phone-proof" +# UNIQUE per-run dir (measured 2026-11-01): with a fixed path, this script's +# own startup Remove-Item deleted the stop.flag that LEFTOVER targets from a +# previous failed run were watching - they could never retire, kept grabbing +# foreground every 60ms, and wrote their textbox content into the shared +# received.txt ("abc\nabc" = old run's abc+ENTER + this run's abc). +$tmp = "$env:TEMP\remote-phone-proof-$PID" New-Item -ItemType Directory -Force -Path $tmp | Out-Null $pidFile = "$tmp\target.pid" @@ -94,12 +99,40 @@ function Get-PhoneUiText { return [System.IO.File]::ReadAllText("$tmp\ui.xml", [System.Text.Encoding]::UTF8) } +# 4a adaptation: in landscape the HUD TextView hides WITH the auto-hiding +# control bar, so "RTT=" disappears while the session is perfectly alive +# (measured 2026-11-01: this proof died at step 4 "session died after ENTER" +# on a healthy 22fps stream). Detect the hidden state (video View node present, +# zero Button nodes) and wake via the letterbox black bar - that tap is +# consumed by the app and never reaches the remote. +function Phone-DisplaySize { + # NB: this script's Adb-Shell returns @{out;code}, not a bare string. + $d = (Adb-Shell @("dumpsys", "window", "displays")).out + $idx = $d.IndexOf("Display: mDisplayId=0") + if ($idx -lt 0) { return @{ w = 1080; h = 2400 } } + $m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)') + if (-not $m.Success) { return @{ w = 1080; h = 2400 } } + return @{ w = [int]$m.Groups[1].Value; h = [int]$m.Groups[2].Value } +} +function Wake-Controls-If-Hidden([string]$xml) { + $hidden = ($xml -match 'class="android\.view\.View"') -and ($xml -notmatch 'class="android\.widget\.Button"') + if (-not $hidden) { return $false } + $pd = Phone-DisplaySize + [void](Adb-Shell @("input", "tap", "100", "$([int]($pd.h / 2))")) + Start-Sleep -Milliseconds 400 + return $true +} + function Wait-For-LiveSession([int]$Seconds, [ref]$outXml) { $deadline = (Get-Date).AddSeconds($Seconds) while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 900 $x = Get-PhoneUiText if ($x -match "RTT=") { $outXml.Value = $x; return $true } + if (Wake-Controls-If-Hidden $x) { + $x = Get-PhoneUiText + if ($x -match "RTT=") { $outXml.Value = $x; return $true } + } } $outXml.Value = (Get-PhoneUiText) return $false @@ -249,8 +282,10 @@ try { $vals += [regex]::Match($v, 'text="([^"]*)"').Groups[1].Value } } - # NOTE: an EMPTY EditText reports its hint as text (verified: text="被控端 IP"), - # so "field text == typed value" is the assertion, not "field text non-empty". + # NOTE: an EMPTY EditText reports its hint as text (verified on device), so + # "field non-empty" is NOT a usable judgement - the assertion is always + # "field text == typed value". (ASCII-only comments: PS 5.1 decodes + # BOM-less .ps1 as GBK.) Write-Host " form now: $($vals -join ' | ')" if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" } if ($vals[0] -ne $PhoneIp) { throw "V4 FAIL: IP field is [$($vals[0])], expected [$PhoneIp]" } diff --git a/tools/rel_mouse_phone_proof.ps1 b/tools/rel_mouse_phone_proof.ps1 index 9e641da..6a2b275 100644 --- a/tools/rel_mouse_phone_proof.ps1 +++ b/tools/rel_mouse_phone_proof.ps1 @@ -34,17 +34,25 @@ $F_LONGPRESS = -join ([char]0x957F,[char]0x6309,[char]0x53F3,[char]0x952E) $F_RCLICK_TAG = -join ([char]0x53F3,[char]0x952E,[char]0x3D) # "right-click=" $F_CLICK = -join ([char]0x5355,[char]0x51FB) # "single click" -# Read the persistent right-click counter from the HUD ("右键=N(source)"). +# Read the persistent right-click counter from the HUD ("right-click=N(source)"). # Returns -1 when absent. A counter is used instead of the one-line debug text # because ACTION_UP immediately overwrites the debug line after a long press -# (measured 2026-10-31: screen showed "长按结束", the firing evidence was gone). +# (measured 2026-10-31: screen showed the long-press-END label, the firing evidence was gone). function RightClickCount { - $m = [regex]::Match((Get-UiText), 'text="([^"]*RTT=[^"]*)"') - if (-not $m.Success) { return -1 } - $hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) - $c = [regex]::Match($hud, "$F_RCLICK_TAG(\d+)") - if ($c.Success) { return [int]$c.Groups[1].Value } - return 0 + for ($t=0; $t -lt 3; $t++) { + $x = Get-UiText + $m = [regex]::Match($x, 'text="([^"]*RTT=[^"]*)"') + if ($m.Success) { + $hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) + $c = [regex]::Match($hud, "$F_RCLICK_TAG(\d+)") + if ($c.Success) { return [int]$c.Groups[1].Value } + return 0 + } + # 4a: in landscape the HUD hides WITH the controls (GONE, not empty text) + Wake-Controls $x + Start-Sleep -Milliseconds 400 + } + return -1 } function Adb { @@ -110,6 +118,40 @@ function ButtonStartingWith([string]$xml, [string]$prefix) { return $null } +# 4a self-adaptation: in landscape the control bar auto-hides after 3s idle +# (and the HUD TextView hides with it). The wake tap goes to the LEFT BLACK BAR +# (x=100, outside the letterboxed video) - the app consumes black-bar touches +# only in the hidden state, so this never reaches the remote. Bottom-center is +# NOT usable: with the controls hidden the video is full-height, so the bottom +# edge lies INSIDE the picture and would fire a real remote click. +function PhoneDisplay { + $d = (AdbShell @("dumpsys","window","displays")) + $idx = $d.IndexOf("Display: mDisplayId=0") + if ($idx -lt 0) { return @{ w=1080; h=2400 } } + $m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)') + if (-not $m.Success) { return @{ w=1080; h=2400 } } + return @{ w=[int]$m.Groups[1].Value; h=[int]$m.Groups[2].Value } +} +function Wake-Controls([string]$xml) { + if ($xml -match 'class="android\.widget\.Button"') { return } # visible already + $pd = PhoneDisplay + [void](AdbShell @("input","tap","100","$([int]($pd.h/2))")) + Start-Sleep -Milliseconds 500 +} +# HUD text with wake-retry (4a: in landscape the HUD TextView hides WITH the +# control bar - a plain read returns "" once the 3s timer expired, and each +# dump itself costs ~2s so reads routinely straddle the expiry). +function Get-HudText { + for ($t=0; $t -lt 3; $t++) { + $x = Get-UiText + $m = [regex]::Match($x, 'text="([^"]*RTT=[^"]*)"') + if ($m.Success) { return [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) } + Wake-Controls $x + Start-Sleep -Milliseconds 400 + } + return "" +} + # Toggle the pointer mode and PROVE it flipped by re-reading the button label. # The bottom bar sits at y=2256..2324, right against the system navigation bar; # a single tap there is not reliable (measured 2026-10-31: the tap landed but the @@ -132,7 +174,24 @@ function Set-Mode([string]$want) { } } } - if (-not $cur) { throw "mode button not found (no Button whose text contains '$F_MODE_TAG')" } + if (-not $cur) { + # 4a: controls may be auto-hidden (landscape). Wake once and retry + # inside the same try budget instead of throwing on the first miss. + Wake-Controls $xml + $xml = Get-UiText + foreach ($n in [regex]::Matches($xml,']*class="android\.widget\.Button"[^>]*>')) { + $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + if ($t.Contains($F_MODE_TAG)) { + $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 + $cur = @{ label=$t; cx=[int](($x1+$x2)/2); cy=[int](($y1+$y2)/2) } + } + } + } + if (-not $cur) { throw "mode button not found (no Button whose text contains '$F_MODE_TAG', even after wake)" } + } Write-Host " mode button label now: [$($cur.label)] (try $try)" if ($cur.label.Contains($wantTag)) { return $true } [void](AdbShell @("input","tap","$($cur.cx)","$($cur.cy)")) @@ -331,6 +390,29 @@ try { Write-Host " surface bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)" & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-b1-live.png" | Out-Null + # 4a pollution guard: the mode PERSISTS in prefs across runs, and a run that + # threw between Set-Mode "absolute" and its restore left absolute installed + # (measured 2026-11-01: step 2 ran in absolute mode, the DOWN absolute move + # "teleported" 1441px - correct product behavior, wrong precondition). + # Force relative, then settle back into the hidden steady state: while the + # controls are hidden, gestures never re-show them, so the geometry stays + # stable for steps 2/2b/2c. + $hudNow = Get-HudText + Write-Host " mode at start: $hudNow" + if ($hudNow.Contains($F_ABSOLUTE)) { [void](Set-Mode "relative") } + Start-Sleep -Milliseconds 3800 # let the 3s auto-hide re-engage + $xh = Get-UiText + if ($xh -match 'class="android\.widget\.Button"') { throw "precondition FAIL: controls did not re-hide after 3.8s" } + $hsb = SurfaceBounds $xh + # NB: PowerShell variable names are CASE-INSENSITIVE - a local `$sh` here + # silently clobbers `$SH` (screen height) and every later [PC]::Set($SW/2,$SH/2) + # dies with "Object[] has no op_Division" (measured 2026-11-01). Prefix locals + # distinctly ($hsb = hidden-state bounds). + if (-not $hsb) { throw "precondition FAIL: no video bounds in hidden state" } + $vx1=$hsb[0]; $vy1=$hsb[1]; $vx2=$hsb[2]; $vy2=$hsb[3] + $vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2) + Write-Host " hidden steady-state bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)" + # ================================================================= Write-Host "=== 2. V-B1a: relative mode moves PC pointer (swipe right) ===" # Park the PC cursor at a known point so the delta is unambiguous. @@ -373,40 +455,89 @@ try { # absolute move. Tap at the far LEFT of the view; the pointer must not jump # to the left edge of the desktop. # - # HARNESS DISCIPLINE (measured 2026-10-31): a swipe's final relative events - # can still be in flight when we re-park the cursor. The first attempt showed - # phantom "jumps" of 15px and 298px that were really the PREVIOUS swipe's - # queued deltas landing after our SetCursorPos. So: wait for the link to go - # quiet, then require the baseline to be STABLE across two reads before - # judging. Otherwise this test would report a product defect that isn't one. - Start-Sleep -Milliseconds 1200 - [PC]::Set([int]($SW*0.8), [int]($SH/2)) - Start-Sleep -Milliseconds 500 - $stable = $false - for ($i=0; $i -lt 6; $i++) { - $r1 = [PC]::Get(); Start-Sleep -Milliseconds 350; $r2 = [PC]::Get() - if ($r1[0] -eq $r2[0] -and $r1[1] -eq $r2[1]) { $stable = $true; break } - Write-Host " cursor still settling: ($($r1[0]),$($r1[1])) -> ($($r2[0]),$($r2[1]))" + # DISCRIMINATION (2026-11-01, landscape): a real absolute-leak has an exact + # signature - the pointer lands at the tap's desktop mapping + # (0.05*1920=96, 540) and STAYS. Residual swipe events instead keep the + # cursor DRIFTING for seconds (observed: stable-looking 4-read windows + # followed by -890/-1418px surges - bursts with >1.2s gaps between them). + # So: settle (6 equal reads), tap, then sample 4x over 2s. + # all samples == before -> PASS + # samples drift -> harness residue: retry (max 3) + # settled AT leak point -> product FAIL (no retry will change it) + # Absolute-leak landing point for this tap: view fraction 0.05 maps to + # desktop x = 0.05*(SW-1); y = view center row -> desktop mid row. + $leakX = [int][Math]::Round(0.05 * ($SW-1)) + $leakY = [int][Math]::Round((($vcy-$vy1) / [double]($vy2-$vy1)) * ($SH-1)) + $verdict = "retry" + for ($att=1; $att -le 3 -and $verdict -eq "retry"; $att++) { + Start-Sleep -Milliseconds 2000 + [PC]::Set([int]($SW*0.8), [int]($SH/2)) + Start-Sleep -Milliseconds 1500 + $stable = $false + for ($i=0; $i -lt 8; $i++) { + $runs = @() + for ($k=0; $k -lt 6; $k++) { $runs += ,([PC]::Get()); Start-Sleep -Milliseconds 250 } + $eq = $true + foreach ($r in $runs) { if ($r[0] -ne $runs[0][0] -or $r[1] -ne $runs[0][1]) { $eq = $false } } + if ($eq) { $stable = $true; break } + Write-Host (" att{0}: cursor still settling (last {1},{2}); re-parking" -f $att,$runs[-1][0],$runs[-1][1]) + [PC]::Set([int]($SW*0.8), [int]($SH/2)) + Start-Sleep -Milliseconds 800 + } + if (-not $stable) { throw "cannot obtain a stable cursor baseline (residual events never drained)" } + $beforeTap = [PC]::Get() + $tapX = [int]($vx1 + ($vx2-$vx1)*0.05) + [void](AdbShell @("input","tap","$tapX","$vcy")) + $samples = @() + for ($k=0; $k -lt 4; $k++) { Start-Sleep -Milliseconds 500; $samples += ,([PC]::Get()) } + $dbgTap = ((VisibleTexts (Get-UiText)) -join " || ") + $maxJump = 0 + foreach ($s in $samples) { + $j = [Math]::Max([Math]::Abs($s[0]-$beforeTap[0]), [Math]::Abs($s[1]-$beforeTap[1])) + if ($j -gt $maxJump) { $maxJump = $j } + } + $last = $samples[-1] + $drift = $false + for ($k=1; $k -lt 4; $k++) { + if ($samples[$k][0] -ne $samples[0][0] -or $samples[$k][1] -ne $samples[0][1]) { $drift = $true } + } + $atLeak = ([Math]::Abs($last[0]-$leakX) -le 8) -and ([Math]::Abs($last[1]-$leakY) -le 8) + # NB: "$_[0]" inside a double-quoted string expands only `$_` and prints + # "[0]" literally (PS 5.1) - use explicit sub-expressions. + $sampleStr = ($samples | ForEach-Object { "(" + $_[0] + "," + $_[1] + ")" }) -join " " + Write-Host (" att{0}: tap ({1},{2}) -> samples {3} maxJump={4} drift={5} atLeakPoint={6} (leak=({7},{8}))" -f ` + $att,$beforeTap[0],$beforeTap[1],$sampleStr,$maxJump,$drift,$atLeak,$leakX,$leakY) + if ($dbgTap.Contains($F_LONGPRESS)) { throw "tap was interpreted as a long press; harness timing is wrong" } + if ($atLeak) { throw ("V-B1a FAIL: pointer settled at the absolute-leak point ({0},{1}) - DOWN sent an absolute move" -f $last[0],$last[1]) } + if ($maxJump -le 8 -and -not $drift) { $verdict = "pass"; break } + if (-not $drift) { + # settled far away but NOT at the leak point: some other single event + # moved it - that IS a product-relevant teleport, retry once more + # before condemning (a queued burst can also settle far away). + Write-Host " att${att}: settled far (jump=$maxJump) but not at leak point - treating as residue, retrying" + } } - if (-not $stable) { throw "cannot obtain a stable cursor baseline (residual relative events never drained)" } - $beforeTap = [PC]::Get() - $tapX = [int]($vx1 + ($vx2-$vx1)*0.05) - [void](AdbShell @("input","tap","$tapX","$vcy")) - Start-Sleep -Milliseconds 700 - $afterTap = [PC]::Get() - $jump = [Math]::Max([Math]::Abs($afterTap[0] - $beforeTap[0]), [Math]::Abs($afterTap[1] - $beforeTap[1])) - $dbgTap = ((VisibleTexts (Get-UiText)) -join " || ") - Write-Host " tap at view-left x=$tapX ; pointer ($($beforeTap[0]),$($beforeTap[1])) -> ($($afterTap[0]),$($afterTap[1])) jump=$jump px" - if ($dbgTap.Contains($F_LONGPRESS)) { throw "tap was interpreted as a long press; harness timing is wrong" } - if ($jump -gt 8) { throw "V-B1a FAIL: touch-down teleported the pointer by $jump px (absolute move leaked into relative mode)" } + if ($verdict -ne "pass") { throw "V-B1a FAIL: pointer moved on touch-down in relative mode across 3 attempts (residual or teleport - see samples above)" } Write-Host " PASS: relative mode leaves the pointer where it was (no teleport)" # ================================================================= Write-Host "=== 3. V-B2: switch to ABSOLUTE mode, re-run R7 touch mapping ===" [void](Set-Mode "absolute") - $hud2 = "" - $m2 = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"') - if ($m2.Success) { $hud2 = [System.Net.WebUtility]::HtmlDecode($m2.Groups[1].Value) } + # 4a geometry race (measured 2026-11-01): Set-Mode WOK the controls, so the + # video shrank to 1216x684 - but each dump costs ~2s and the auto-hide timer + # is 3s, so by tap time the controls had hidden again and the video was back + # to full height. Tapping with the stale visible-state geometry produced + # "errors" of 281-357px that were pure harness error (y landed at the old + # center row 341, not 540). Wait for the hidden steady state and sample + # THOSE bounds - in hidden state a video tap only re-arms the timer, it never + # re-shows the controls, so the geometry is stable for the whole R7 loop. + Start-Sleep -Milliseconds 3800 + $hud2 = Get-HudText + $sb = SurfaceBounds (Get-UiText) + if (-not $sb) { throw "V-B2 FAIL: no video bounds in hidden steady state" } + $vx1=$sb[0];$vy1=$sb[1];$vx2=$sb[2];$vy2=$sb[3] + $vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2) + Write-Host " hidden steady-state bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)" Write-Host " HUD: $hud2" if (-not $hud2.Contains($F_ABSOLUTE)) { throw "V-B2 FAIL: HUD still does not say absolute; got [$hud2]" } Write-Host " PASS: mode switched to absolute (button label and HUD agree)" @@ -443,21 +574,48 @@ try { # Use a desktop of our own: the proof's WinForms target window (R-11: never # inject into a window we do not own - it would modify user data). + # NB: the 3b version launched this WITHOUT its Mandatory params, so the + # child died instantly and the right-click landed at raw screen center + # (possible user window - near-miss on R-11). Launch properly now and park + # the pointer INSIDE our own window: topmost form at (80,80) 420x140. $target = Join-Path $repo "tools\text_target.ps1" if (-not (Test-Path $target)) { throw "missing $target" } - $tp = Start-Process powershell -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$target ` - -PassThru -WindowStyle Minimized - Start-Sleep -Seconds 3 - Write-Host " target window started (pid $($tp.Id))" + # UNIQUE per-run dir (same orphan-stop-flag trap as phone_chain_proof, + # fixed 2026-11-01): startup Remove-Item must not delete the retirement + # channel of targets left by a previous failed run. + $ttmp = Join-Path $env:TEMP "vbx4a-$PID" + New-Item -ItemType Directory -Force -Path $ttmp | Out-Null + $pidFile = "$ttmp\target.pid"; $stopFile = "$ttmp\stop.flag"; $resultFile = "$ttmp\received.txt" + Remove-Item $pidFile,$stopFile,$resultFile -ErrorAction SilentlyContinue + $tp = Start-Process powershell -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$target, ` + "-PidFile",$pidFile,"-ResultFile",$resultFile,"-StopFile",$stopFile -PassThru -WindowStyle Hidden + $deadline = (Get-Date).AddSeconds(8) + while (-not (Test-Path $pidFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 200 } + if (-not (Test-Path $pidFile)) { throw "target window did not start (no pid file)" } + Write-Host " target window started (pid $(Get-Content $pidFile))" - # Park the pointer over the middle of the target window's edit area by - # absolute injection first (so we know where the long press lands). - [PC]::Set([int]($SW/2), [int]($SH/2)) + # Long-press and drag run in the HIDDEN steady state: geometry is stable + # there (video taps only re-arm the hide timer, they never re-show the + # controls), and the counter read wakes the HUD afterwards. + function Enter-HiddenSteady { + Start-Sleep -Milliseconds 3800 + $x = Get-UiText + if ($x -match 'class="android\.widget\.Button"') { throw "precondition FAIL: controls did not hide" } + $s = SurfaceBounds $x + if (-not $s) { throw "precondition FAIL: no hidden-state video bounds" } + return $s + } + + $rcBefore = RightClickCount # may wake the controls; re-enter hidden after + $hsb4 = Enter-HiddenSteady + $hx1=$hsb4[0];$hy1=$hsb4[1];$hx2=$hsb4[2];$hy2=$hsb4[3] + $hcx=[int](($hx1+$hx2)/2); $hcy=[int](($hy1+$hy2)/2) + Write-Host " right-click counter before: $rcBefore ; hidden center=($hcx,$hcy)" + # The long press right-clicks AT THE REMOTE POINTER, so park the pointer on + # our own target window first - the menu then opens over a window we own. + [PC]::Set(290, 150) Start-Sleep -Milliseconds 300 - $preLong = [PC]::Get() - $rcBefore = RightClickCount - Write-Host " right-click counter before: $rcBefore" - [void](AdbShell @("input","swipe","$vcx","$vcy","$vcx","$vcy","900")) # press & hold ~900ms, no movement + [void](AdbShell @("input","swipe","$hcx","$hcy","$hcx","$hcy","900")) # press & hold ~900ms, no movement Start-Sleep -Milliseconds 700 $rcAfter = RightClickCount $debug = ((VisibleTexts (Get-UiText)) -join " || ") @@ -472,7 +630,8 @@ try { Write-Host "=== 4b. V-B3 negative: a DRAG must not fire long press ===" $rcBeforeDrag = RightClickCount - [void](AdbShell @("input","swipe","$vcx","$vcy",$([int]($vcx+120)),$([int]($vcy+80)),900)) + Enter-HiddenSteady | Out-Null + [void](AdbShell @("input","swipe","$hcx","$hcy",$([int]($hcx+120)),$([int]($hcy+80)),900)) Start-Sleep -Milliseconds 700 $rcAfterDrag = RightClickCount $debug2 = ((VisibleTexts (Get-UiText)) -join " || ") @@ -487,10 +646,18 @@ try { $rcBeforeTwo = RightClickCount # adb cannot synthesize a two-finger tap (lessons R-6): this is a MANUAL item. Write-Host " (two-finger tap is not drivable via adb - recorded as manual, not asserted here)" - - Stop-Process -Id $tp.Id -Force -ErrorAction SilentlyContinue } finally { + # 4a pollution discipline: end with no live session - the agent's 10s idle + # reclaim then happens BETWEEN runs, not in the middle of the next run's + # connect (measured 2026-11-01: a stranded zombie left the next proof unable + # to reach a live session at all). + [void](AdbShell @("am","force-stop","com.verify.remote")) + # target window exits via its stop flag (text_target contract); kill is the + # fallback. In finally so a throw never leaves a topmost window on the user's desktop. + if ($stopFile) { [void](New-Item -ItemType File -Force -Path $stopFile -ErrorAction SilentlyContinue) } + Start-Sleep -Milliseconds 400 + if ($tp -and -not $tp.HasExited) { Stop-Process -Id $tp.Id -Force -ErrorAction SilentlyContinue } [void][PC]::Set($origCursor[0], $origCursor[1]) [void](AdbShell @("svc","power","stayon","false")) if ($origIme -and $origIme -ne "null") { diff --git a/tools/text_injection_proof.ps1 b/tools/text_injection_proof.ps1 index b092d7c..799d86f 100644 --- a/tools/text_injection_proof.ps1 +++ b/tools/text_injection_proof.ps1 @@ -24,7 +24,11 @@ $ErrorActionPreference = "Stop" # Repo root derived from this script's location (tools/ is one level under the # repo root) so the proof runs on any clone, not just the author's D: drive. $repo = (Resolve-Path "$PSScriptRoot\..").Path -$tmp = "$env:TEMP\remote-text-proof" +# UNIQUE per run: a fixed path lets the startup Remove-Item delete the stop.flag +# that leftover targets from a previous FAILED run are watching, so they never +# retire and keep writing into the shared received.txt +# (same trap fixed in phone_chain_proof, measured 2026-11-01). +$tmp = "$env:TEMP\remote-text-proof-$PID" $base = "D:\working\environment" $env:RUSTUP_HOME = "$base\rust"