test(tools): cursor_proof + proof_common 抽取 + rel_mouse 适配 v5/双屏与污染修复
4b 光标覆盖层验收(V-D1/D3/D6/D7)+ 4a 建议 2 的公共库沉淀:
- proof_common.ps1:抽 landscape/rel_mouse 已验证的观测面 helper
(唤醒只从证明过的隐藏态、正向配对判隐藏、N25 唯一临时文件、按文案取按钮、
静置门控 + SetCursorPos 读回自校验、内存预检、R-7 新鲜守卫)。
老 proof 本轮**不迁移**(改 4 个已验证脚本而不全重跑=假绿风险,登记下轮卫生项)
- cursor_proof.ps1:真机判据
V-D1 截图青簇质心 vs CursorMarker 映射 ±6px(含近原点 (48,48),Some(0,0)≠None 由单测+golden 锁)
V-D3 静止桌面跳转后 900ms 内标记跟随(覆盖 500ms 保活重发带新鲜 cursor)
V-D7 指针进左侧副屏负 X → agent 折叠 None → 无青簇(本机双屏白送的真实判据)
reject 阶段 V-D6:旧 v4 APK 连 v5 agent → 屏幕出「协议版本不一致」+ detail client=4,server=5
实测全绿:4 点偏差 ≤0.6px、跟随 0.5px、副屏无簇、拒绝判据命中
- rel_mouse_phone_proof.ps1:适配 v5 + 修两处 harness 缺陷(都是本轮回归实测逼出的)
① HUD v4 断言→v5(PROTO_LABEL 配对);② V-B2 R7 期望值从写死 1920 派生
改为按实时桌面 / 动态算(桌面已漂到 2560x1440,旧写死值假报 575px,
违反 AGENTS.md 坐标期望按当前分辨率重算)
③ 模式守卫改为无条件 Set-Mode relative(旧 HUD.Contains(绝对) 在 HUD 读空时
静默跳过,上一轮 throw 残留的 relative_mode=false 污染了整个 step2——
绝对模式 DOWN 发绝对移动是正确产品行为,被 2c 误读成 teleport)
④ Set-Mode 唤醒 miss 并入 try 预算(原来单次重读即 throw,烧掉整脚本)+ 失败截图证据
实测:R7 误差 1px、2c maxJump=0、长按右键 0→1、全链路绿
This commit is contained in:
parent
45daba5419
commit
3b219a442d
280
tools/cursor_proof.ps1
Normal file
280
tools/cursor_proof.ps1
Normal file
@ -0,0 +1,280 @@
|
||||
# 4b cursor-overlay proof: marker position, off-screen fold, refresh, old-client reject.
|
||||
#
|
||||
# ASCII-only (PS 5.1 reads BOM-less .ps1 as GBK; repo lessons). Chinese expected
|
||||
# texts are built from code points (R-8: never trust an encoding round trip).
|
||||
#
|
||||
# Judgements (loop-4b-plan.md section 9):
|
||||
# V-D6 OLD v4 APK + new v5 agent -> form shows the version-mismatch text and
|
||||
# the session never goes live. RUN THIS PHASE BEFORE INSTALLING THE NEW
|
||||
# APK: -Phase reject
|
||||
# V-D1 screencap cyan cluster centroid == CursorMarker expectation +/- 6 px
|
||||
# for 4 desktop points incl. (48,48) ~ origin (Some(0,0) vs None: the
|
||||
# marker MUST be painted at the corner, not skipped)
|
||||
# V-D3 after moving the PC cursor on a quiet desktop the marker reaches the
|
||||
# new spot within ~1.2s (covers the 500ms idle-resend path; the frame
|
||||
# header rebuild with fresh cursor was the whole point of R-A)
|
||||
# V-D7 cursor into the LEFT SECOND MONITOR (negative X, machine is dual-screen
|
||||
# since 2026-11-02) -> agent folds to None -> cyan cluster GONE
|
||||
#
|
||||
# Prereqs: v5 release agent.exe built; new (v5) APK installed for -Phase main;
|
||||
# second monitor actually present (V-D7 skips loudly if not - see probe output);
|
||||
# nobody touching the real mouse during a step (gates retry, loud fail otherwise).
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File tools\cursor_proof.ps1 -Host_ 192.168.1.181 -Phase reject
|
||||
# powershell -ExecutionPolicy Bypass -File tools\cursor_proof.ps1 -Host_ 192.168.1.181 -Phase main
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Host_,
|
||||
[int]$Port = 47800,
|
||||
[string]$Code = "t",
|
||||
[ValidateSet("reject", "main")][string]$Phase = "main",
|
||||
[string]$AgentExe = ""
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
. "$PSScriptRoot\proof_common.ps1"
|
||||
|
||||
# ---- expected Chinese fragments as code points (R-8) ----
|
||||
# "connect" button label
|
||||
$F_CONNECT = -join ([char]0x8FDE, [char]0x63A5)
|
||||
# "reconnect"
|
||||
$F_RECONNECT = -join ([char]0x91CD, [char]0x65B0, [char]0x8FDE, [char]0x63A5)
|
||||
# "protocol version mismatch" error summary (ConnectErrorText KIND_VERSION)
|
||||
$F_VERSION = -join ([char]0x534F, [char]0x8BAE, [char]0x7248, [char]0x672C,
|
||||
[char]0x4E0D, [char]0x4E00, [char]0x81F4)
|
||||
# "live" HUD prefix (yi-lian-jie = connected)
|
||||
$F_LIVE = -join ([char]0x5DF2, [char]0x8FDE, [char]0x63A5)
|
||||
|
||||
$ev = Join-Path (Split-Path $repo -Parent) "evidence\4b"
|
||||
New-Item -ItemType Directory -Force -Path $ev | Out-Null
|
||||
|
||||
# ---- cyan marker pixel detector (LockBits scan, full centroid) ----
|
||||
Add-Type -AssemblyName System.Windows.Forms # VirtualScreen for V-D7
|
||||
if (-not ('PccCyan' -as [type])) {
|
||||
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @"
|
||||
using System; using System.Drawing; using System.Drawing.Imaging;
|
||||
using System.Runtime.InteropServices; using System.Collections.Generic;
|
||||
public class PccCyan {
|
||||
public static string Find(string path) {
|
||||
using (var bmp = new Bitmap(path)) {
|
||||
var rect = new Rectangle(0,0,bmp.Width,bmp.Height);
|
||||
var data = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||||
int n = data.Stride * data.Height; byte[] buf = new byte[n];
|
||||
Marshal.Copy(data.Scan0, buf, 0, n);
|
||||
bmp.UnlockBits(data);
|
||||
// dot color = RGB(0x00,0xE5,0xFF); BGRA in memory. Loose tolerance:
|
||||
// the ring is white/black; a desktop with a big cyan region would poison the
|
||||
// centroid, so the total-count guard below rejects it instead of averaging.
|
||||
int total = 0;
|
||||
for (int y=0; y<data.Height; y++) {
|
||||
int row = y*data.Stride;
|
||||
for (int x=0; x<data.Width; x++) {
|
||||
int i = row + x*4;
|
||||
byte b=buf[i], g=buf[i+1], r=buf[i+2];
|
||||
if (r < 40 && (g>=0xC0 && g<=0xFF) && b>=0xD0) total++;
|
||||
}
|
||||
}
|
||||
if (total == 0) return "none";
|
||||
if (total > 4000) return "patch:" + total; // suspicious giant color field
|
||||
// Centroid over ALL cyan pixels would be corrupted by a few stray ones far
|
||||
// away (measured 2026-11-02: n=181 at the right spot but spread=955 - the
|
||||
// desktop itself had a handful of cyan pixels elsewhere). Cluster properly:
|
||||
// bin the hits on an 8px grid, take the DENSEST bin as the marker seed,
|
||||
// then centroid/spread over hits within 40px of that seed only.
|
||||
var bins = new Dictionary<long,int>();
|
||||
for (int y=0; y<data.Height; y++) {
|
||||
int row = y*data.Stride;
|
||||
for (int x=0; x<data.Width; x++) {
|
||||
int i = row + x*4;
|
||||
byte b=buf[i], g=buf[i+1], r=buf[i+2];
|
||||
if (r < 40 && (g>=0xC0 && g<=0xFF) && b>=0xD0) {
|
||||
long key = (long)(x/8)*1000000 + (y/8);
|
||||
bins[key] = bins.ContainsKey(key) ? bins[key]+1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
long bestK = -1; int bestN = 0; int secondN = 0;
|
||||
foreach (var kv in bins) {
|
||||
if (kv.Value > bestN) { secondN = bestN; bestN = kv.Value; bestK = kv.Key; }
|
||||
else if (kv.Value > secondN) { secondN = kv.Value; }
|
||||
}
|
||||
int bx = (int)(bestK/1000000)*8 + 4, by = (int)(bestK%1000000)*8 + 4;
|
||||
long sx=0, sy=0; int cnt=0; int minx=99999999,miny=99999999,maxx=-1,maxy=-1;
|
||||
for (int y=0; y<data.Height; y++) {
|
||||
int row = y*data.Stride;
|
||||
for (int x=0; x<data.Width; x++) {
|
||||
int i = row + x*4;
|
||||
byte b=buf[i], g=buf[i+1], r=buf[i+2];
|
||||
if (r < 40 && (g>=0xC0 && g<=0xFF) && b>=0xD0) {
|
||||
if (Math.Abs(x-bx)<=40 && Math.Abs(y-by)<=40) {
|
||||
sx+=x; sy+=y; cnt++;
|
||||
if(x<minx)minx=x; if(x>maxx)maxx=x; if(y<miny)miny=y; if(y>maxy)maxy=y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cnt == 0) return "none";
|
||||
int spread = Math.Max(maxx-minx, maxy-miny);
|
||||
// report seed too: harness prints it when a check fails (diagnosis, not judgement)
|
||||
return string.Format("{0}|{1}|{2}|{3}|{4}|{5}|{6}", sx/(double)cnt, sy/(double)cnt, cnt, spread, total, bx, secondN);
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
}
|
||||
function Find-Marker([string]$pngPath) {
|
||||
$r = [PccCyan]::Find($pngPath)
|
||||
if ($r -eq "none") { return $null }
|
||||
if ($r -like "patch:*") { throw "screenshot contains a huge cyan-ish color field ($($r.Substring(6)) px) - clear the desktop (wallpaper/browser) and re-run; refusing to average a fake marker (R-8)" }
|
||||
$p = $r.Split('|')
|
||||
@{ x = [double]$p[0]; y = [double]$p[1]; n = [int]$p[2]; spread = [int]$p[3]; total = [int]$p[4]; seed = "$($p[5]),$($p[6])" }
|
||||
}
|
||||
function Take-Shot([string]$tag) {
|
||||
$dev = "/sdcard/pcc_$PID.png"
|
||||
$local = Join-Path $ev "$tag.png"
|
||||
$old = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||
try {
|
||||
[void](AdbShell @("screencap", "-p", $dev))
|
||||
$r = Adb -AdbArgs @("pull", $dev, $local)
|
||||
} finally { $ErrorActionPreference = $old }
|
||||
if ($r.code -ne 0 -or -not (Test-Path $local)) { throw "screencap/pull failed for $tag" }
|
||||
return $local
|
||||
}
|
||||
|
||||
Write-Host "=== 0. preflight (memory / fresh v5 agent) ==="
|
||||
Memory-Precheck
|
||||
if (-not $AgentExe) { $AgentExe = Join-Path $repo "target\release\agent.exe" }
|
||||
Ensure-FreshAgent $AgentExe $Port $Code
|
||||
|
||||
$installed = ((AdbShell @("dumpsys", "package", "com.verify.remote"))) -join "`n"
|
||||
$vn = [regex]::Match($installed, 'versionName=(\S+)').Groups[1].Value
|
||||
Write-Host " installed APK: $vn"
|
||||
|
||||
# ======================================================================
|
||||
if ($Phase -eq "reject") {
|
||||
Write-Host "=== V-D6: OLD (v4) APK against the NEW v5 agent must be rejected ==="
|
||||
if ($vn -like "*phase4a*") {
|
||||
Write-Host " (running against the v4 client - this phase MUST come before installing 4b)"
|
||||
} else {
|
||||
Write-Host " WARNING: installed APK is $vn - the v4-rejection test is only meaningful against a real v4 client"
|
||||
}
|
||||
# NOTE (first-run lesson 2026-11-02): launching via `am start --es host ...`
|
||||
# does NOT dial: the intent only prefill-prefers stale SharedPreferences and
|
||||
# the form just sits there ("未连接"). Rejection must be observed through the
|
||||
# REAL dial path - type the form, tap connect, read the failure screen.
|
||||
$x = Restart-AppForm
|
||||
FillAndConnect $x $Host_ "$Port" $Code $F_CONNECT $F_RECONNECT
|
||||
$deadline = (Get-Date).AddSeconds(25); $shown = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 800
|
||||
$ui = Get-UiRetry
|
||||
$texts = (VisibleTexts $ui) -join " / "
|
||||
if ($texts -match "version mismatch" -or $texts.Contains($F_VERSION)) { $shown = $texts; break }
|
||||
if ($texts.Contains($F_LIVE)) { throw "V-D6 FAIL: v4 client CONNECTED to the v5 agent ('$F_LIVE' on screen) - version gate broken" }
|
||||
}
|
||||
if (-not $shown) { throw "V-D6 FAIL: no version-mismatch text within 25s; last screen: [$((VisibleTexts (Get-Ui)) -join ' / ')]" }
|
||||
Write-Host " screen: $shown"
|
||||
Write-Host " PASS: v5 agent refused the v4 client with the actionable Chinese hint"
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-d6-reject.png" | Out-Null
|
||||
# pollution discipline (4a): leave no live session for the next proof
|
||||
[void](AdbShell @("am", "force-stop", "com.verify.remote"))
|
||||
Write-Host "=== CURSOR PROOF (reject phase) COMPLETE ==="
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# ---- main phase: needs the NEW apk; go live once, then geometry checks ----
|
||||
Write-Host "=== 1. go live (new v5 APK), capture HUD (v5 pin + desktop size) ==="
|
||||
if ($vn -notlike "*phase4b*") { throw "main phase needs the 4b APK installed (found $vn)" }
|
||||
$x = Restart-AppForm
|
||||
FillAndConnect $x $Host_ "$Port" $Code $F_CONNECT $F_RECONNECT
|
||||
$hud = Wait-LiveHud 20
|
||||
if (-not $hud) { throw "never reached live: [$((VisibleTexts (Get-Ui)) -join ' / ')]" }
|
||||
Write-Host " HUD: $($hud -replace "`n", ' | ')"
|
||||
if (-not $hud.Contains("v5")) { throw "V-D5x FAIL: HUD does not report v5 (PROTO_LABEL out of sync?): [$hud]" }
|
||||
$m = [regex]::Match($hud, '(\d+)x(\d+)')
|
||||
if (-not $m.Success) { throw "cannot parse desktop size from HUD: [$hud]" }
|
||||
$dW = [int]$m.Groups[1].Value; $dH = [int]$m.Groups[2].Value
|
||||
Write-Host " desktop from HUD: ${dW}x${dH}"
|
||||
|
||||
# Marker geometry works in EITHER orientation (letterbox mapping is proportional),
|
||||
# but landscape is the 4a headline case. AUTO rotates for a wide desktop - give the
|
||||
# rotation a moment to land, then proceed on whatever orientation is actually up.
|
||||
$rot = (Get-Date).AddSeconds(3); $d = LocalDisplay
|
||||
while ((Get-Date) -lt $rot) {
|
||||
$d = LocalDisplay
|
||||
if ($d.w -gt $d.h) { break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
Write-Host (" orientation: {0}x{1} ({2})" -f $d.w, $d.h, $(if ($d.w -gt $d.h) { "landscape" } else { "portrait" }))
|
||||
Start-Sleep -Seconds 2 # let the hide timer run so the video fills its final rect
|
||||
|
||||
$ui = Get-UiRetry
|
||||
$vb = VideoBounds $ui
|
||||
if (-not $vb) { throw "video (16:9 View) not found in the tree - every coordinate expectation below would be meaningless (N21)" }
|
||||
Write-Host (" video bounds: l={0} t={1} w={2} h={3} ratio err {4:N4}" -f $vb.l, $vb.t, $vb.w, $vb.h, $vb.err)
|
||||
if ($vb.err -gt 0.02) { throw "video not 16:9 - geometry changed since 4a, re-derive the mapping here" }
|
||||
|
||||
function Expect-OnScreen([int]$cx, [int]$cy) {
|
||||
# CursorMarker on-screen anchor = video view origin + mapped point.
|
||||
# PS parses `a / [double]b, c` inside @() as an array construction (the type
|
||||
# literal + comma is a known pitfall - measured 2026-11-02: this threw
|
||||
# op_Division on Object[] / silently produced 3 elements). Parenthesize each
|
||||
# element so each division is evaluated as a scalar first.
|
||||
@( ($vb.l + $cx * $vb.w / [double]$dW), ($vb.t + $cy * $vb.h / [double]$dH) )
|
||||
}
|
||||
function Check-MarkerAt([int]$cx, [int]$cy, [string]$tag, [double]$tol = 6.0) {
|
||||
Set-CursorQuiet $cx $cy
|
||||
Start-Sleep -Milliseconds 700 # >= one 500ms idle frame + render
|
||||
$png = Take-Shot $tag
|
||||
$mk = Find-Marker $png
|
||||
if (-not $mk) { throw "V-D1 FAIL [$tag]: no marker at desktop ($cx,$cy) - cyan cluster absent" }
|
||||
$e = Expect-OnScreen $cx $cy
|
||||
$dx = [Math]::Abs($mk.x - $e[0]); $dy = [Math]::Abs($mk.y - $e[1])
|
||||
Write-Host (" [{0}] marker ({1:N0},{2:N0}) n={3} spread={4} expect ({5:N0},{6:N0}) dev=({7:N1},{8:N1})" -f `
|
||||
$tag, $mk.x, $mk.y, $mk.n, $mk.spread, $e[0], $e[1], $dx, $dy)
|
||||
if ($dx -gt $tol -or $dy -gt $tol) { throw "V-D1 FAIL [$tag]: deviation ($dx,$dy) > $tol px (shot: $png)" }
|
||||
if ($mk.spread -gt 90) { throw "V-D1 FAIL [$tag]: cyan spread $($mk.spread)px - too wide to be the marker dot (false positive path)" }
|
||||
return $mk
|
||||
}
|
||||
|
||||
Write-Host "=== 2. V-D1: four desktop points (incl. near-origin) ==="
|
||||
# (48,48) instead of (0,0): the dot radius would clip at the exact corner and
|
||||
# bias the centroid; Some(0,0)-vs-None semantics are already locked by unit +
|
||||
# golden tests. Corner-proximity error is bounded well below tol here.
|
||||
Check-MarkerAt 48 48 "v-d1-origin" | Out-Null
|
||||
Check-MarkerAt 1280 720 "v-d1-center" | Out-Null
|
||||
Check-MarkerAt 2400 1300 "v-d1-lowright" | Out-Null
|
||||
Check-MarkerAt 2000 300 "v-d1-upperright" | Out-Null
|
||||
Write-Host " PASS: marker centroid within 6 px for all 4 points"
|
||||
|
||||
Write-Host "=== 3. V-D3: idle-desktop move refreshes the marker <= ~1.2s ==="
|
||||
# Quiet gate, then one decisive jump; 700ms above already covers 500ms resend.
|
||||
$from = Check-MarkerAt 300 1200 "v-d3-from"
|
||||
$e2 = Expect-OnScreen 1600 400
|
||||
Set-CursorQuiet 1600 400
|
||||
Start-Sleep -Milliseconds 900
|
||||
$mk3 = Find-Marker (Take-Shot "v-d3-to")
|
||||
if (-not $mk3) { throw "V-D3 FAIL: marker vanished after moving the cursor on a quiet desktop" }
|
||||
$dx3 = [Math]::Abs($mk3.x - $e2[0]); $dy3 = [Math]::Abs($mk3.y - $e2[1])
|
||||
Write-Host (" after jump: marker ({0:N0},{1:N0}) expect ({2:N0},{3:N0}) dev=({4:N1},{5:N1})" -f `
|
||||
$mk3.x, $mk3.y, $e2[0], $e2[1], $dx3, $dy3)
|
||||
if ($dx3 -gt 6 -or $dy3 -gt 6) { throw "V-D3 FAIL: marker did not follow within 900ms (stale cursor field?)" }
|
||||
Write-Host " PASS: marker refreshes within one idle-resend window"
|
||||
|
||||
Write-Host "=== 4. V-D7: cursor into the LEFT second monitor folds to None ==="
|
||||
$vd = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
||||
if ($vd.X -ge 0) {
|
||||
Write-Host " SKIP: no negative-X monitor on this machine - V-D7 (off-screen fold) cannot be tested here; the fold rule is locked by fold_cursor unit tests"
|
||||
} else {
|
||||
$ox = [int]($vd.X + 100) # deep into the second display (negative X)
|
||||
$oy = 500
|
||||
Set-CursorQuiet $ox $oy
|
||||
Start-Sleep -Milliseconds 900
|
||||
$mk7 = Find-Marker (Take-Shot "v-d7-second-monitor")
|
||||
if ($mk7) { throw "V-D7 FAIL: marker still drawn with cursor at ($ox,$oy) outside the captured display (n=$($mk7.n) at {x=$($mk7.x);y=$($mk7.y)})" }
|
||||
Write-Host " PASS: no cyan cluster with the pointer at ($ox,$oy) - folded to None end to end"
|
||||
}
|
||||
|
||||
Write-Host "=== 5. tidy: park the pointer inside the primary screen ==="
|
||||
Set-CursorQuiet ([int]($dW/2)) ([int]($dH/2))
|
||||
Write-Host "=== CURSOR PROOF COMPLETE ==="
|
||||
349
tools/proof_common.ps1
Normal file
349
tools/proof_common.ps1
Normal file
@ -0,0 +1,349 @@
|
||||
# proof_common.ps1 - shared device-side proof helpers (4b, personal docs loop-4b S6).
|
||||
#
|
||||
# ASCII-only file (PS 5.1 reads BOM-less .ps1 as GBK; repo lessons).
|
||||
# Dot-source it: . "$PSScriptRoot\proof_common.ps1"
|
||||
#
|
||||
# Provenance: these are the battle-tested helpers from landscape_proof.ps1 (4a:
|
||||
# wake-only-from-proven-hidden, positive-pair hide check, N25-style unique temp
|
||||
# files, uiautomator empty-dump retry) plus rel_mouse_phone_proof's focus-assert
|
||||
# field clearing. Older proofs are NOT migrated to this file in loop 4b (changing
|
||||
# four validated scripts without re-running all four is itself a false-green
|
||||
# risk) - migration is registered as a harness hygiene item for the next loop.
|
||||
|
||||
# ---- adb wrappers (never let stderr trip ErrorActionPreference=Stop) ----
|
||||
if (-not (Get-Variable -Name PccAdb -Scope Global -ErrorAction SilentlyContinue)) {
|
||||
$global:PccAdb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
}
|
||||
|
||||
function Adb {
|
||||
param([Parameter(Mandatory)][string[]]$AdbArgs)
|
||||
$old = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$out = & $global:PccAdb @AdbArgs 2>$null
|
||||
@{ out = ($out -join "`n"); code = $LASTEXITCODE }
|
||||
} finally { $ErrorActionPreference = $old }
|
||||
}
|
||||
function AdbShell { param([string[]]$Cmd) (Adb -AdbArgs (@("shell") + $Cmd)).out }
|
||||
|
||||
# ---- UI tree ----
|
||||
# Unique temp per process (N25 lesson applied to dump pulls, cheap insurance).
|
||||
$script:PccUiTmp = "$env:TEMP\pcc_ui_$PID.xml"
|
||||
$script:PccDevDump = "/sdcard/pcc_$PID.xml"
|
||||
|
||||
function Get-Ui {
|
||||
[void](AdbShell @("uiautomator", "dump", $script:PccDevDump))
|
||||
$r = Adb -AdbArgs @("pull", $script:PccDevDump, $script:PccUiTmp)
|
||||
if ($r.code -ne 0 -or -not (Test-Path $script:PccUiTmp)) { return "" }
|
||||
return [System.IO.File]::ReadAllText($script:PccUiTmp, [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
# uiautomator intermittently dumps empty (measured repeatedly in 4a). Retry until
|
||||
# the tree is plausibly real. Callers still assert on their own node - this only
|
||||
# removes the transient, never masks a genuine 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)
|
||||
}
|
||||
}
|
||||
|
||||
# ---- buttons: match by LABEL, never by position (3b/N23 lesson) ----
|
||||
function ButtonNode([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) { return $n.Value }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function ButtonCenter([string]$xml, [string]$want, [bool]$prefix = $false) {
|
||||
$v = ButtonNode $xml $want $prefix
|
||||
if (-not $v) { return $null }
|
||||
$b = [regex]::Match($v, 'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if (-not $b.Success) { return $null }
|
||||
@{ cx = [int](([int]$b.Groups[1].Value + [int]$b.Groups[3].Value) / 2)
|
||||
cy = [int](([int]$b.Groups[2].Value + [int]$b.Groups[4].Value) / 2) }
|
||||
}
|
||||
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 observation-surface discipline: controls auto-hide in landscape ----
|
||||
# Hidden state = POSITIVE PAIR (video View node present AND zero Button nodes).
|
||||
# An empty dump would otherwise pass "no buttons" trivially (N24 false-fails).
|
||||
function ControlsHidden([string]$ui) {
|
||||
$anyBtn = ($ui -match 'class="android\.widget\.Button"')
|
||||
$video = ($ui -match 'class="android\.view\.View"')
|
||||
return ($video -and -not $anyBtn)
|
||||
}
|
||||
# Wake tap lives in the letterbox black bar (x=100): outside the video in both
|
||||
# states, consumed by the app only when hidden - zero remote leakage (4a/D1).
|
||||
function Wake-Band {
|
||||
$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 = ButtonCenter $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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
# ---- HUD / session liveness ----
|
||||
# "zhen=" frame counter (char U+5E27 as code point, ASCII-safe source).
|
||||
function FrameCount {
|
||||
$m = [regex]::Match((Get-Ui), ([char]0x5E27 + '=(\d+)'))
|
||||
if ($m.Success) { return [long]$m.Groups[1].Value }
|
||||
return -1
|
||||
}
|
||||
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
|
||||
}
|
||||
# Live HUD line (RTT= only exists when connected; hidden-controls caveat above).
|
||||
function Wait-LiveHud([int]$seconds = 20) {
|
||||
$deadline = (Get-Date).AddSeconds($seconds)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$ui = Get-Ui
|
||||
if ($ui -notmatch "RTT=") {
|
||||
if (ControlsHidden $ui) { WakeControls }
|
||||
continue
|
||||
}
|
||||
$m = [regex]::Match($ui, 'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m.Success) { return [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# ---- display / app lifecycle ----
|
||||
# vivo ROM prints NO mRotation= line and keeps a permanent 1080x2400 virtual
|
||||
# display (N30): orientation MUST be read from cur= inside the mDisplayId=0 block.
|
||||
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 } }
|
||||
@{ 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 ' || ')]"
|
||||
}
|
||||
|
||||
# ---- connect form: focus-assert typing (rel_mouse_proof grade, R-8) ----
|
||||
function CenterOf($m) {
|
||||
@([int](([int]$m.Groups[1].Value + [int]$m.Groups[3].Value) / 2),
|
||||
[int](([int]$m.Groups[2].Value + [int]$m.Groups[4].Value) / 2))
|
||||
}
|
||||
function FormFields([string]$xml) {
|
||||
[regex]::Matches($xml, '<node[^>]*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
}
|
||||
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 FocusedIndex([string]$xml) {
|
||||
$i = -1
|
||||
foreach ($n in [regex]::Matches($xml, '<node[^>]*>')) {
|
||||
if ($n.Value -match 'class="android\.widget\.EditText"') {
|
||||
$i++
|
||||
if ($n.Value -match 'focused="true"') { return $i }
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function Clear-Field($c, [int]$wantIdx) {
|
||||
for ($try = 0; $try -lt 3; $try++) {
|
||||
[void](AdbShell @("input", "tap", "$($c[0])", "$($c[1])"))
|
||||
$deadline = (Get-Date).AddSeconds(1.5)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 250
|
||||
if ((FocusedIndex (Get-Ui)) -eq $wantIdx) {
|
||||
for ($i = 0; $i -lt 40; $i++) { [void](AdbShell @("input", "keyevent", "67")) }
|
||||
Start-Sleep -Milliseconds 120
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
throw "field $wantIdx never took focus"
|
||||
}
|
||||
function FillAndConnect([string]$xml, [string]$host_, [string]$port_, [string]$code_,
|
||||
[string]$fConnect, [string]$fReconnect) {
|
||||
$f = FormFields $xml
|
||||
if ($f.Count -lt 3) { throw "form has $($f.Count) fields" }
|
||||
Clear-Field (CenterOf $f[0]) 0; TypeAscii $host_
|
||||
Clear-Field (CenterOf $f[1]) 1; TypeAscii $port_
|
||||
Clear-Field (CenterOf $f[2]) 2; TypeAscii $code_
|
||||
$expect = @($host_, $port_, $code_)
|
||||
$vals = @()
|
||||
for ($retry = 0; $retry -le 2; $retry++) {
|
||||
$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 -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" }
|
||||
$bad = -1
|
||||
for ($i = 0; $i -lt 3; $i++) { if ($vals[$i] -ne $expect[$i]) { $bad = $i; break } }
|
||||
if ($bad -ge 0) {
|
||||
$f = FormFields $x2
|
||||
Clear-Field (CenterOf $f[$bad]) $bad
|
||||
TypeAscii $expect[$bad]
|
||||
continue
|
||||
}
|
||||
$btn = ButtonCenter $x2 $fConnect
|
||||
if (-not $btn) { $btn = ButtonCenter $x2 $fReconnect }
|
||||
if (-not $btn) { $btn = Find-VisibleButton $fConnect
|
||||
if (-not $btn) { $btn = Find-VisibleButton $fReconnect } }
|
||||
if (-not $btn) { throw "connect button not found" }
|
||||
[void](AdbShell @("input", "tap", "$($btn.cx)", "$($btn.cy)"))
|
||||
return
|
||||
}
|
||||
throw "form input never converged: [$($vals -join ' | ')]"
|
||||
}
|
||||
|
||||
# ---- video geometry (uiautomator has no SurfaceView - match 16:9 View, N21) ----
|
||||
function VideoBounds([string]$xml) {
|
||||
$best = $null; $bestErr = 1e9
|
||||
foreach ($n in [regex]::Matches($xml, '<node[^>]*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) {
|
||||
$x1 = [int]$n.Groups[1].Value; $y1 = [int]$n.Groups[2].Value
|
||||
$x2 = [int]$n.Groups[3].Value; $y2 = [int]$n.Groups[4].Value
|
||||
$w = $x2 - $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 = @{ l = $x1; t = $y1; w = $w; h = $h } }
|
||||
}
|
||||
if ($best) { $best.err = $bestErr }
|
||||
return $best
|
||||
}
|
||||
|
||||
# ---- PC-side cursor: DPI-aware set with hijack gates ----
|
||||
if (-not ('PccCur' -as [type])) {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public class PccCur {
|
||||
[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); }
|
||||
}
|
||||
"@
|
||||
}
|
||||
[PccCur]::Dpi()
|
||||
|
||||
# SetCursorQuiet: the machine may be shared (2026-11-02 probe: a human used the
|
||||
# real mouse mid-probe and faked every reading). Gate: two reads 350ms apart must
|
||||
# agree (nobody is moving), then Set + read-back must equal the request; retries
|
||||
# on hijack; loud failure instead of a silently wrong sample (R-8).
|
||||
function Set-CursorQuiet([int]$x, [int]$y, [int]$tries = 8) {
|
||||
for ($i = 0; $i -lt $tries; $i++) {
|
||||
$a = [PccCur]::Get()
|
||||
Start-Sleep -Milliseconds 350
|
||||
$b = [PccCur]::Get()
|
||||
if (-not ($a[0] -eq $b[0] -and $a[1] -eq $b[1])) {
|
||||
Write-Host " [cursor] busy ($($a -join ',')->$($b -join ',')), retry $($i+1)"
|
||||
Start-Sleep -Seconds 1
|
||||
continue
|
||||
}
|
||||
[PccCur]::Set($x, $y)
|
||||
Start-Sleep -Milliseconds 120
|
||||
$g = [PccCur]::Get()
|
||||
if ($g[0] -eq $x -and $g[1] -eq $y) { return }
|
||||
Write-Host " [cursor] readback ($($g -join ',')) != ($x,$y), retry $($i+1)"
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
throw "cursor never settled at ($x,$y) - someone is using the real mouse; retry the proof when the PC is idle"
|
||||
}
|
||||
|
||||
# ---- memory precheck (N20/N29: OOM fakes build/test failures) ----
|
||||
function Memory-Precheck([double]$minGB = 0.5) {
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
$freeGB = $os.FreeVirtualMemory / 1MB
|
||||
Write-Host (" commit free: {0:N1} GB" -f $freeGB)
|
||||
if ($freeGB -lt $minGB) { throw "commit memory critically low - free RAM first (gradle --stop)" }
|
||||
}
|
||||
|
||||
# ---- R-7 stale-agent guard: binary newer than process => restart ----
|
||||
function Ensure-FreshAgent([string]$AgentExe, [int]$Port, [string]$Code) {
|
||||
if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe (cargo build --release -p agent)" }
|
||||
$ag = @(Get-Process agent -ErrorAction SilentlyContinue)
|
||||
if ($ag.Count -gt 0 -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 (fresh)"
|
||||
} else { Write-Host " agent running and current" }
|
||||
}
|
||||
@ -157,11 +157,25 @@ function Get-HudText {
|
||||
# a single tap there is not reliable (measured 2026-10-31: the tap landed but the
|
||||
# mode did not change). So: tap, wait, re-read, retry; and judge on the BUTTON
|
||||
# label as well as the HUD (two independent observations of the same state).
|
||||
function Get-UiTextRetry([int]$tries = 4) {
|
||||
# landscape_proof Get-UiRetry pattern (N24-era lesson): uiautomator
|
||||
# intermittently returns short/empty trees; a single miss inside a
|
||||
# wake-then-read sequence used to burn the whole mode-switch budget
|
||||
# (measured 2026-11-02 step 3: full run died at "mode button not found"
|
||||
# although the very next steps would have re-dumped fine).
|
||||
$x = ""
|
||||
for ($i = 0; $i -lt $tries; $i++) {
|
||||
$x = Get-UiText
|
||||
if ($x.Length -gt 500) { return $x }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
return $x
|
||||
}
|
||||
function Set-Mode([string]$want) {
|
||||
# $want is "relative" or "absolute"
|
||||
$wantTag = if ($want -eq "relative") { $F_RELATIVE } else { $F_ABSOLUTE }
|
||||
for ($try=0; $try -lt 4; $try++) {
|
||||
$xml = Get-UiText
|
||||
for ($try=0; $try -lt 6; $try++) {
|
||||
$xml = Get-UiTextRetry
|
||||
$cur = $null
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
@ -175,30 +189,23 @@ function Set-Mode([string]$want) {
|
||||
}
|
||||
}
|
||||
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.
|
||||
# 4a: controls may be auto-hidden (landscape). Wake and fold the miss
|
||||
# into the SAME try budget instead of a single re-read + instant throw.
|
||||
Wake-Controls $xml
|
||||
$xml = Get-UiText
|
||||
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.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)" }
|
||||
continue
|
||||
}
|
||||
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)"))
|
||||
Start-Sleep -Milliseconds 700
|
||||
}
|
||||
$final = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
throw "mode did not switch to $want after 4 taps; screen: [$final]"
|
||||
# Fail LOUD with evidence: a bare text dump cannot show what the screen
|
||||
# actually looked like (this is how the 2026-11-02 step-3 death became a
|
||||
# guessing game; one PNG makes the next occurrence self-evidencing).
|
||||
$evShot = Join-Path $ev "mode-switch-fail-$PID.png"
|
||||
try { & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out $evShot | Out-Null } catch { }
|
||||
$final = ((VisibleTexts (Get-UiTextRetry)) -join " || ")
|
||||
throw "mode did not switch to $want after 6 tries; screen: [$final] shot: $evShot"
|
||||
}
|
||||
# The video SurfaceView bounds - touch coordinates must land inside it.
|
||||
#
|
||||
@ -360,7 +367,7 @@ if ($ag.Count -gt 0 -and (Test-Path $AgentExe) -and ((Get-Item $AgentExe).LastWr
|
||||
if ($ag.Count -eq 0) {
|
||||
if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe" }
|
||||
Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null
|
||||
Start-Sleep -Seconds 1; Write-Host " agent started (v4)"
|
||||
Start-Sleep -Seconds 1; Write-Host " agent started (v5)"
|
||||
} else { Write-Host " agent running and current" }
|
||||
|
||||
$origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim()
|
||||
@ -379,8 +386,10 @@ try {
|
||||
$hud = ""
|
||||
if (-not (Wait-Live 14 ([ref]$hud))) { throw "never reached a live session" }
|
||||
Write-Host " HUD: $hud"
|
||||
if (-not $hud.Contains("v4")) { throw "V-B4 FAIL: HUD does not report protocol v4; got [$hud]" }
|
||||
Write-Host " PASS: live session on protocol v4"
|
||||
# HUD version pin (3b/V-B4). PROTO bump MUST sync this string -
|
||||
# see MainActivity PROTO_LABEL comment (4b R-5 pair).
|
||||
if (-not $hud.Contains("v5")) { throw "V-B4 FAIL: HUD does not report protocol v5; got [$hud]" }
|
||||
Write-Host " PASS: live session on protocol v5"
|
||||
|
||||
$xml = Get-UiText
|
||||
$sb = SurfaceBounds $xml
|
||||
@ -398,8 +407,18 @@ try {
|
||||
# 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") }
|
||||
Write-Host " HUD at start: $hudNow"
|
||||
# Converge to relative UNCONDITIONALLY via Set-Mode, which reads the BUTTON
|
||||
# label and returns early if already relative (idempotent, authoritative).
|
||||
# The old guard `if ($hudNow.Contains(absolute))` silently skipped when the
|
||||
# HUD read came back empty or straddled the auto-hide (N24 observation-surface
|
||||
# trap) - a mode left polluted by a previous run that threw after Set-Mode
|
||||
# absolute then poisoned step 2c: absolute mode's DOWN DOES send an absolute
|
||||
# move (correct product behavior), which the leak-point check read as a
|
||||
# teleport (measured again 2026-11-02: prefs relative_mode=false at start,
|
||||
# V-B1a "passed" on direction alone because absolute swipes also have
|
||||
# direction - the whole step 2 block was running in the wrong mode).
|
||||
[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" }
|
||||
@ -542,17 +561,25 @@ try {
|
||||
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)"
|
||||
|
||||
# R7 expectations for 1920x1080 (physical pixels, 125% scaling does NOT apply
|
||||
# to the injected desktop space - the agent injects into the physical desktop)
|
||||
# R7 expectations derived from the LIVE desktop size (physical pixels; 125%
|
||||
# scaling does NOT apply - the agent injects into the physical desktop, and
|
||||
# normX=frac*65535 lands at frac*(SW-1)).
|
||||
# Fixed 1920-based literals broke the moment the capture display became
|
||||
# 2560x1440 (2026-11-02: reported 575px error = 0.9*(2560-1920), a harness
|
||||
# staleness, not a mapping regression). AGENTS.md: coordinate expectations
|
||||
# MUST be recomputed for the current resolution. Same script already uses
|
||||
# dynamic $SW/$SH for V-B1a and the leak point - this block was the outlier.
|
||||
$r7 = @(
|
||||
@{ frac=0.10; ex=[int](192); ey=[int](108) },
|
||||
@{ frac=0.90; ex=[int](1728); ey=[int](972) },
|
||||
@{ frac=0.50; ex=[int](960); ey=[int](540) }
|
||||
@{ frac=0.10 },
|
||||
@{ frac=0.90 },
|
||||
@{ frac=0.50 }
|
||||
)
|
||||
$worst = 0
|
||||
foreach ($c in $r7) {
|
||||
$tx = [int]($vx1 + ($vx2-$vx1)*$c.frac)
|
||||
$ty = [int]($vy1 + ($vy2-$vy1)*$c.frac)
|
||||
$c.ex = [int][Math]::Round($c.frac * ($SW-1))
|
||||
$c.ey = [int][Math]::Round($c.frac * ($SH-1))
|
||||
[void](AdbShell @("input","tap","$tx","$ty"))
|
||||
Start-Sleep -Milliseconds 400
|
||||
$p = [PC]::Get()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user