Files
orca/native/computer-use-windows/runtime-pointer.test.ps1
T
Kelvin Amoaba 10302c46a8 fix(computer-use): stop Windows clicks, scrolls and drags from taking the screen without --restore-window (#42161)
* fix(computer-use): refuse Windows pointer fallbacks without --restore-window

Coordinate, right, middle, modified and multi-clicks, clicks on elements with no
primary pattern, unscrollable wheel scrolls and drags raised the window and
moved the real cursor without being asked. They now refuse with
background_unavailable before anything is sent. Coordinate and fractional
scrolls go through ScrollPattern first. With --restore-window only the target
window is raised (never a cloaked one), with a UIA activation fallback and a
second attempt, points are validated first and the cursor is put back behind
the input. A failing UIA pattern or disabled element is an error instead of a
real click, and delivery is measured after the post-action observe.

* fix(computer-use): settle Windows clicks before restoring the cursor and keep scroll writes honest

* fix(computer-use): restore the Windows cursor with SetCursorPos and hit-test coordinate scrolls

* test(computer-use): assert Windows raise outcomes, not attempt order

* fix(computer-use): report a Windows page scroll the provider ignores as not scrolled
2026-11-13 22:44:25 +00:00

311 lines
17 KiB
PowerShell

param([string]$RuntimePath = (Join-Path $PSScriptRoot "runtime.ps1"))
$ErrorActionPreference = "Stop"
function Assert-TestEqual($Actual, $Expected, [string]$Label) {
if ($Actual -ne $Expected) { throw "$Label expected '$Expected', received '$Actual'" }
}
function Assert-TestLike([string]$Actual, [string]$Pattern, [string]$Label) {
if ($Actual -notlike $Pattern) { throw "$Label expected '$Pattern', received '$Actual'" }
}
Add-Type -TypeDefinition @"
using System;
using System.Collections.Generic;
public static class OrcaDesktopWin32 {
public struct POINT { public int X; public int Y; }
public static readonly List<string> Calls = new List<string>();
// Which SetForegroundWindow call (1-based) Windows honours; 0 refuses them all.
public static int HonouredRaise;
public static bool HonourUiaActivation;
public static bool Cloaked;
public static IntPtr Foreground;
public static IntPtr Pending;
public static int ReadsSinceActivation;
public static int Raises;
public static int WindowX;
public static int TargetIntegrity;
public static void Reset(int honouredRaise) {
TargetIntegrity = 0x2000;
Calls.Clear();
HonouredRaise = honouredRaise;
HonourUiaActivation = false;
Cloaked = false;
Foreground = new IntPtr(202);
Pending = IntPtr.Zero;
ReadsSinceActivation = 0;
Raises = 0;
WindowX = 100;
}
public static void Activate(IntPtr hwnd) {
Pending = hwnd;
ReadsSinceActivation = 0;
}
public static int GetProcessIntegrityLevel(int processId) {
return processId == 4242 ? TargetIntegrity : 0x2000;
}
public static bool IsIconic(IntPtr hwnd) {
return false;
}
public static bool IsWindowCloaked(IntPtr hwnd) {
return Cloaked;
}
public static bool ShowWindow(IntPtr hwnd, int command) {
Calls.Add("restore");
return true;
}
public static bool SetForegroundWindow(IntPtr hwnd) {
Calls.Add("activate");
Raises++;
if (Raises != HonouredRaise) { return false; }
Activate(hwnd);
return true;
}
// Activation lands a moment later, and restoring the window moves its frame.
public static IntPtr GetForegroundWindow() {
if (Pending != IntPtr.Zero && ++ReadsSinceActivation >= 2) {
Foreground = Pending;
Pending = IntPtr.Zero;
WindowX = 500;
}
return Foreground;
}
public static uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId) {
processId = hwnd == new IntPtr(101) ? 4242u : 1u;
return 1;
}
public static bool GetCursorPos(out POINT point) {
point = new POINT { X = 7, Y = 8 };
return true;
}
public static bool SetCursorPos(int x, int y) {
Calls.Add("move:" + x + "," + y);
return true;
}
public static void mouse_event(uint flags, uint dx, uint dy, int data, UIntPtr extra) {
Calls.Add("mouse");
}
public static void SendModifiedClick(byte[] modifiers, uint down, uint up) {
Calls.Add("modified");
}
}
"@
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($RuntimePath, [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw ($errors | Out-String) }
$functions = @(
"Test-OrcaWindowFocused", "Wait-OrcaWindowFocused", "Assert-OrcaKeyboardFocus", "Restore-OrcaWindow",
"Restore-OrcaPointerWindow", "New-OrcaForegroundRefusal", "Get-OrcaCursorPosition", "Restore-OrcaCursorPosition",
"Assert-OrcaWindowTarget", "Get-OrcaRequiredNumber", "Get-OrcaPositiveInteger", "Get-OrcaPositiveNumber",
"Assert-OrcaElementFound", "Get-OrcaPointerPoint", "Get-OrcaElementScreenPoint", "Get-OrcaModifierVirtualKey",
"Get-OrcaClickModifierVirtualKeys", "Send-OrcaMouseClick", "Send-OrcaDrag", "Invoke-OrcaOperation",
"Assert-OrcaInputAllowed", "Test-OrcaTargetTookForeground", "Invoke-OrcaPrimaryAction", "Get-OrcaScrollPattern",
"Get-OrcaScrollPercent", "Get-OrcaScrollViewSize", "Get-OrcaRuntimeId", "Test-OrcaSameRuntimeId", "Wait-OrcaScrollPercentChange", "Invoke-OrcaScrollPercent",
"Invoke-OrcaScrollPattern"
)
foreach ($definition in $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)) {
if ($definition.Name -in $functions) { . ([scriptblock]::Create($definition.Extent.Text)) }
}
$MouseEvents = @{ LeftDown = 2; LeftUp = 4; RightDown = 8; RightUp = 16; MiddleDown = 32; MiddleUp = 64; Wheel = 2048; HorizontalWheel = 4096 }
function Find-OrcaProcess { [pscustomobject]@{ MainWindowHandle = 101; Id = 4242; ProcessName = "Fixture" } }
function Get-OrcaAppName($Process) { $Process.ProcessName }
function Get-OrcaRootElement { $null }
function Get-OrcaWindowFrame { @{ x = [OrcaDesktopWin32]::WindowX; y = 200 } }
function Find-OrcaElement($RootElement, $Record) { if ($null -ne $Record -and -not $Record.stale) { $Record } else { $null } }
function Get-OrcaElementScreenPoint($Element) { if ($null -ne $Element) { @{ x = [OrcaDesktopWin32]::WindowX + 40; y = 240 } } else { $null } }
function Test-OrcaElementDisabled($Element) { [bool]$Element.disabled }
function Invoke-OrcaWindowPatternActivation($RootElement) {
[OrcaDesktopWin32]::Calls.Add("uia-activate")
if ([OrcaDesktopWin32]::HonourUiaActivation) { [OrcaDesktopWin32]::Activate([IntPtr]101) }
}
function Find-OrcaScrollContainerAt { $null }
function New-OrcaSnapshot { [pscustomobject]@{} }
function Invoke-TestOperation([hashtable]$Fields) {
$operation = [pscustomobject]@{ app = "Fixture"; noScreenshot = $true }
foreach ($key in $Fields.Keys) { $operation | Add-Member -NotePropertyName $key -NotePropertyValue $Fields[$key] }
try {
[pscustomobject]@{ result = Invoke-OrcaOperation $operation; message = $null }
} catch {
[pscustomobject]@{ result = $null; message = $_.Exception.Message }
}
}
function Get-TestPointerCalls {
@([OrcaDesktopWin32]::Calls | Where-Object { $_ -like "move:*" -or $_ -in @("mouse", "modified") })
}
# Elements without a primary pattern; pattern-capable ones are records with pattern = $true.
function Invoke-OrcaPrimaryActionStub($Element) { [bool]$Element.pattern }
Set-Item -Path function:Invoke-OrcaPrimaryActionReal -Value (Get-Item function:Invoke-OrcaPrimaryAction).ScriptBlock
Set-Item -Path function:Invoke-OrcaPrimaryAction -Value { param($Element, $Index) Invoke-OrcaPrimaryActionStub $Element }
Set-Item -Path function:Invoke-OrcaScrollPatternReal -Value (Get-Item function:Invoke-OrcaScrollPattern).ScriptBlock
function Invoke-OrcaScrollPattern($Element, $RootElement, $Direction, $Pages) { [bool]$Element.pattern }
$pointerCases = @(
@{ tool = "click"; x = 5; y = 6 },
@{ tool = "click"; element = [pscustomobject]@{ index = 3 } },
@{ tool = "click"; element = [pscustomobject]@{ index = 3; pattern = $true }; mouse_button = "right" },
@{ tool = "click"; element = [pscustomobject]@{ index = 3; pattern = $true }; click_count = 2 },
@{ tool = "click"; element = [pscustomobject]@{ index = 3; pattern = $true }; modifiers = "shift" },
@{ tool = "scroll"; x = 5; y = 6; direction = "down"; pages = 1 },
@{ tool = "scroll"; element = [pscustomobject]@{ index = 3 }; direction = "down"; pages = 0.5 },
@{ tool = "drag"; from_x = 5; from_y = 6; to_x = 25; to_y = 26 }
)
# Without restoreWindow, anything that needs the real mouse is refused before any raise or input.
foreach ($case in $pointerCases) {
[OrcaDesktopWin32]::Reset(1)
$outcome = Invoke-TestOperation $case
$label = "$($case.tool) $(($case.Keys | Sort-Object) -join ',')"
Assert-TestLike $outcome.message "background_unavailable: *nothing was sent.*Ask the user before retrying with --restore-window." "$label refusal"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "$label refusal sends no raise or input"
}
# With restoreWindow: one raise, geometry read after it, and the user's pointer put back behind the input.
foreach ($case in $pointerCases) {
[OrcaDesktopWin32]::Reset(1)
$outcome = Invoke-TestOperation ($case + @{ restoreWindow = $true })
$label = "$($case.tool) $(($case.Keys | Sort-Object) -join ',') restored"
if ($null -ne $outcome.message) { throw "$label failed: $($outcome.message)" }
Assert-TestEqual $outcome.result.action.delivery "foreground" "$label delivery"
Assert-TestEqual @([OrcaDesktopWin32]::Calls | Where-Object { $_ -eq "activate" }).Count 1 "$label raises once"
$pointer = Get-TestPointerCalls
$expectedFirst = if ($null -ne $case.element) { "move:540,240" } else { "move:505,206" }
Assert-TestEqual $pointer[0] $expectedFirst "$label geometry after delayed activation"
Assert-TestEqual $pointer[$pointer.Count - 1] "move:7,8" "$label puts the pointer back last"
}
# A refused raise falls back to the UIA route and one more raise, and sends nothing when all fail.
[OrcaDesktopWin32]::Reset(0)
$outcome = Invoke-TestOperation @{ tool = "click"; x = 5; y = 6; restoreWindow = $true }
Assert-TestLike $outcome.message "window_not_focused: *no pointer input was sent" "refused raise"
Assert-TestEqual (Get-TestPointerCalls).Count 0 "refused raise sends no pointer input"
[OrcaDesktopWin32]::Reset(2)
$outcome = Invoke-TestOperation @{ tool = "click"; x = 5; y = 6; restoreWindow = $true }
Assert-TestEqual $outcome.result.action.path "synthetic" "second raise honoured"
[OrcaDesktopWin32]::Reset(0)
[OrcaDesktopWin32]::HonourUiaActivation = $true
$outcome = Invoke-TestOperation @{ tool = "click"; x = 5; y = 6; restoreWindow = $true }
Assert-TestEqual $outcome.result.action.path "synthetic" "UIA activation honoured"
# A window on another virtual desktop is never raised.
[OrcaDesktopWin32]::Reset(1)
[OrcaDesktopWin32]::Cloaked = $true
$outcome = Invoke-TestOperation @{ tool = "click"; x = 5; y = 6; restoreWindow = $true }
Assert-TestLike $outcome.message "window_not_focused: *another virtual desktop*" "cloaked window"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "cloaked window sends no raise or input"
# Stale and disabled targets fail before any raise.
foreach ($case in @(
@{ tool = "click"; element = [pscustomobject]@{ index = 3; stale = $true } },
@{ tool = "drag"; fromElement = [pscustomobject]@{ index = 3; stale = $true }; to_x = 25; to_y = 26 }
)) {
[OrcaDesktopWin32]::Reset(1)
$outcome = Invoke-TestOperation ($case + @{ restoreWindow = $true })
Assert-TestLike $outcome.message "stale element frame*" "$($case.tool) stale target"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "$($case.tool) stale target sends no raise or input"
}
[OrcaDesktopWin32]::Reset(1)
$outcome = Invoke-TestOperation @{ tool = "click"; element = [pscustomobject]@{ index = 3; pattern = $true; disabled = $true }; restoreWindow = $true }
Assert-TestLike $outcome.message "element_not_enabled: element 3 is disabled*" "disabled element"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "disabled element sends no raise or input"
# UI Automation routes need no raise, even with restoreWindow, and report measured delivery.
foreach ($case in @(
@{ tool = "click"; element = [pscustomobject]@{ index = 1; pattern = $true } },
@{ tool = "click"; element = [pscustomobject]@{ index = 1; pattern = $true }; restoreWindow = $true },
@{ tool = "scroll"; element = [pscustomobject]@{ index = 1; pattern = $true }; direction = "down"; pages = 1 }
)) {
[OrcaDesktopWin32]::Reset(1)
$outcome = Invoke-TestOperation $case
Assert-TestEqual $outcome.result.action.path "accessibility" "pattern $($case.tool) path"
Assert-TestEqual $outcome.result.action.delivery "background" "pattern $($case.tool) delivery"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "pattern $($case.tool) bypasses activation and global input"
}
foreach ($case in @(
@{ tool = "click"; element = [pscustomobject]@{ index = 1; pattern = $true } },
@{ tool = "click"; x = 5; y = 6; restoreWindow = $true },
@{ tool = "type_text"; text = "x"; restoreWindow = $true }
)) {
[OrcaDesktopWin32]::Reset(1)
[OrcaDesktopWin32]::TargetIntegrity = 0x3000
$outcome = Invoke-TestOperation $case
Assert-TestLike $outcome.message "target_elevated:*" "$($case.tool) elevated target"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "$($case.tool) elevated target sends no activation or input"
}
# Why: WPF activates its own window for most patterns; the result must say so rather than claim background.
[OrcaDesktopWin32]::Reset(1)
function Invoke-OrcaPrimaryActionStub($Element) { [OrcaDesktopWin32]::Foreground = [IntPtr]101; $true }
$outcome = Invoke-TestOperation @{ tool = "click"; element = [pscustomobject]@{ index = 1 } }
Assert-TestEqual $outcome.result.action.path "accessibility" "self-activating pattern click path"
Assert-TestEqual $outcome.result.action.delivery "foreground" "self-activating pattern click delivery"
$uiaTypes = $false
try {
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
$uiaTypes = $true
} catch {}
if ($uiaTypes) {
function New-TestPatternElement([scriptblock]$Invoke) {
$instance = [pscustomobject]@{}
$instance | Add-Member -MemberType ScriptMethod -Name Invoke -Value $Invoke
$element = [pscustomobject]@{ instance = $instance }
$element | Add-Member -MemberType ScriptMethod -Name TryGetCurrentPattern -Value {
param($pattern, $out)
if ($pattern -ne [Windows.Automation.InvokePattern]::Pattern) { return $false }
$out.Value = $this.instance
$true
}
$element
}
$disabled = New-TestPatternElement { throw (New-Object Windows.Automation.ElementNotEnabledException) }
$message = try { Invoke-OrcaPrimaryActionReal $disabled 4 | Out-Null; $null } catch { $_.Exception.Message }
Assert-TestLike $message "element_not_enabled: element 4 is disabled*" "pattern refusing a disabled element"
$failing = New-TestPatternElement { throw (New-Object InvalidOperationException "provider gone") }
$message = try { Invoke-OrcaPrimaryActionReal $failing 4 | Out-Null; $null } catch { $_.Exception.Message }
Assert-TestLike $message "UI Automation Invoke failed on element 4: *provider gone*no mouse click was sent*" "failing pattern"
$plain = [pscustomobject]@{}
$plain | Add-Member -MemberType ScriptMethod -Name TryGetCurrentPattern -Value { param($pattern, $out) $false }
Assert-TestEqual (Invoke-OrcaPrimaryActionReal $plain 4) $false "element without a primary pattern"
# Half a page with a quarter of the content visible moves a sixth of the scroll range.
$scroll = [pscustomobject]@{ Current = [pscustomobject]@{ VerticalViewSize = 25.0; VerticalScrollPercent = 30.0 }; Calls = New-Object System.Collections.Generic.List[string] }
$scroll | Add-Member -MemberType ScriptMethod -Name SetScrollPercent -Value {
param($horizontal, $vertical)
$this.Calls.Add("$horizontal,$([Math]::Round($vertical, 4))")
$this.Current.VerticalScrollPercent = $vertical
}
Assert-TestEqual (Invoke-OrcaScrollPercent $scroll $true $true 0.5 30.0) $true "fractional scroll"
Assert-TestEqual ($scroll.Calls -join " ") "-1,46.6667" "fractional scroll percent"
$scroll.Calls.Clear()
Assert-TestEqual (Invoke-OrcaScrollPercent $scroll $true $false 9.5 46.6667) $true "fractional scroll clamps at the top"
Assert-TestEqual ($scroll.Calls -join " ") "-1,0" "fractional scroll clamped percent"
$scroll | Add-Member -Force -MemberType ScriptMethod -Name SetScrollPercent -Value { param($horizontal, $vertical) }
Assert-TestEqual (Invoke-OrcaScrollPercent $scroll $true $true 0.5 0.0) $false "fractional scroll the provider ignores"
# A page the provider ignores fails unless the view is already at that end.
function New-TestScrollElement([double]$Percent) {
$pattern = [pscustomobject]@{ Current = [pscustomobject]@{ VerticallyScrollable = $true; VerticalScrollPercent = $Percent } }
$pattern | Add-Member -MemberType ScriptMethod -Name Scroll -Value { param($horizontal, $vertical) }
$element = [pscustomobject]@{ pattern = $pattern }
$element | Add-Member -MemberType ScriptMethod -Name GetCurrentPattern -Value { param($id) $this.pattern }
$element | Add-Member -MemberType ScriptMethod -Name GetRuntimeId -Value { @(1) }
$element
}
$ignored = New-TestScrollElement 30.0
Assert-TestEqual (Invoke-OrcaScrollPatternReal $ignored $ignored "down" 1) $false "whole page the provider ignores"
$atEnd = New-TestScrollElement 100.0
Assert-TestEqual (Invoke-OrcaScrollPatternReal $atEnd $atEnd "down" 1) $true "whole page at the end of the content"
} else {
Write-Output "skipping UI Automation pattern checks: UIAutomationClient is unavailable"
}
Write-Output "windows-pointer-tests-ok"