Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 87 additions & 6 deletions Tools/windows/Invoke-UiCaptureWorker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -716,9 +716,92 @@ function Invoke-CapSceneAction {
Assert-CapWorkerDeadline "after action $Action"
}

function Wait-CapMatchedWireMode {
param(
[Parameter(Mandatory)]$AppRun,
[Parameter(Mandatory)][DateTime]$DeadlineUtc,
[scriptblock]$StatusProbe,
[scriptblock]$NowProvider,
[scriptblock]$SleepAction
)
if (-not $StatusProbe -and -not $NowProvider -and -not $SleepAction) {
return Wait-CapAutomationElement -Window $AppRun.Window `
-ProcessIdentity $AppRun.Run.Identity `
-Name 'Choose a target loop to wire this entry' -AutomationId 'status' `
-DeadlineUtc $DeadlineUtc `
-BeforePoll {Assert-CapWorkerDeadline 'Wire mode status'} `
-FindElement {
param([ref]$Snapshot)
Find-CapAutomationElement -RootHandle $AppRun.Window `
-ProcessIdentity $AppRun.Run.Identity `
-ExpectedProcessId $AppRun.Run.Identity.Pid `
-Name 'Choose a target loop to wire this entry' -AutomationId 'status' `
-RefusalSnapshot $Snapshot
}
}
$now={
if ($NowProvider) {return & $NowProvider}
return [DateTime]::UtcNow
}
$sleep={
param([int]$Milliseconds)
if ($SleepAction) {& $SleepAction $Milliseconds;return}
Start-Sleep -Milliseconds $Milliseconds
}
while ((& $now) -lt $DeadlineUtc) {
Assert-CapWorkerDeadline 'Wire mode status'
$status=if ($StatusProbe) {
& $StatusProbe $AppRun
} else {
Find-CapAutomationElement -RootHandle $AppRun.Window -ProcessIdentity $AppRun.Run.Identity `
-ExpectedProcessId $AppRun.Run.Identity.Pid -Name 'Choose a target loop to wire this entry' `
-AutomationId 'status'
}
if ($null -ne $status -and
$status.Current.Name -ceq 'Choose a target loop to wire this entry' -and
$status.Current.AutomationId -ceq 'status') {
return $status
}
& $sleep 50
}
throw 'Wire mode status did not appear before the action deadline.'
}

function Send-CapMatchedEscape {
param([Parameter(Mandatory)]$AppRun)
return [GraphCodeUiaGateState]::PostKeyboard($AppRun.Window,0x1B)
param(
[Parameter(Mandatory)]$AppRun,
[scriptblock]$ForegroundProbe,
[scriptblock]$DialogProbe,
[scriptblock]$KeyboardSender
)
if (-not $ForegroundProbe -and -not $DialogProbe -and -not $KeyboardSender) {
$null=Assert-CapRootProcess -RootHandle $AppRun.Window -ProcessIdentity $AppRun.Run.Identity
}
$foreground=if ($ForegroundProbe) {
[bool](& $ForegroundProbe $AppRun.Window)
} else {
[GraphCodeUiaGateState]::IsForegroundWindow($AppRun.Window)
}
if (-not $foreground) {
throw 'Wire cancellation requires the owned GraphCode shell foreground.'
}
$dialog=if ($DialogProbe) {
[IntPtr](& $DialogProbe $AppRun.Run.Identity.Pid)
} else {
[GraphCodeUiaGateState]::FindTopLevel('GraphCodeNativeForm',[uint32]$AppRun.Run.Identity.Pid)
}
if ($dialog -ne [IntPtr]::Zero) {
throw 'Wire cancellation refuses to inject Escape while an owned native dialog is present.'
}
$sent=if ($KeyboardSender) {
[bool](& $KeyboardSender $AppRun.Window 0x1B)
} else {
[GraphCodeUiaGateState]::PostKeyboard($AppRun.Window,0x1B)
}
if (-not $sent) {
throw 'Wire cancellation Escape injection was rejected.'
}
return $true
}

function Measure-CapSceneZoom {
Expand Down Expand Up @@ -2249,10 +2332,8 @@ function Invoke-CapMatchedScenes {
$markReceipt=$null
if ($runWire) {
$null=Invoke-CapSceneAction -AppRun $app -Action 'wire-node' -SubjectId $recovery.wireIdentity
$wireStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Choose a target loop to wire this entry' `
-AutomationId 'status'
if ($null -eq $wireStatus) {throw "Wire it up did not expose its armed result for $scene."}
$wireStatus=Wait-CapMatchedWireMode -AppRun $app `
-DeadlineUtc (Get-CapActionDeadline -WorkerDeadlineUtc $leaseDeadlineUtc)
if (-not (Send-CapMatchedEscape -AppRun $app)) {
throw "Wire it up could not be cancelled after matched evidence for $scene."
}
Expand Down
93 changes: 92 additions & 1 deletion Tools/windows/Tests/UiCaptureDriver.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ function Assert-CapRejects {
'GraphCodeUiaGateState|PostMessage|user32.dll|PostMessageA',
'GraphCodeUiaGateState|EnumWindows|user32.dll|EnumWindows',
'GraphCodeUiaGateState|GetWindowThreadProcessId|user32.dll|GetWindowThreadProcessId',
'GraphCodeUiaGateState|GetForegroundWindow|user32.dll|GetForegroundWindow',
'GraphCodeUiaGateState|GetClassName|user32.dll|GetClassNameW',
'CapNative|NtQueryInformationProcess|ntdll.dll|NtQueryInformationProcess',
'CapNative|NtQueryInformationProcess|ntdll.dll|NtQueryInformationProcess',
Expand Down Expand Up @@ -751,7 +752,7 @@ function Assert-CapRejects {
'CapProcessJob|SetHandleInformation|kernel32.dll|SetHandleInformation'
)
$actual = @(Test-CapNativeBindings | ForEach-Object { "$($_.Type)|$($_.Method)|$($_.Library)|$($_.EntryPoint)" })
Assert-CapTest ($actual.Count -eq 36 -and @(Compare-Object $expected $actual).Count -eq 0) 'Exact static36 export contract differs.'
Assert-CapTest ($actual.Count -eq 37 -and @(Compare-Object $expected $actual).Count -eq 0) 'Exact static37 export contract differs.'
return @{ subcases = 36; evidence = 'API export resolution only, no native window or GUI action.' }
}
Invoke-CapTest 'legacy mutable seed journal proof is refused and exact numbered terminal proof is accepted' {
Expand Down Expand Up @@ -3003,6 +3004,8 @@ function Invoke-CapMatchedContracts {
$workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and
$workerSource.Contains('function Get-CapWorkerExpectedImageCount') -and
$workerSource.Contains('$images.Count -eq (Get-CapWorkerExpectedImageCount)') -and
$workerSource.Contains('function Wait-CapMatchedWireMode') -and
$workerSource.Contains('[GraphCodeUiaGateState]::PostKeyboard') -and
$workerSource.Contains("-Action 'attention-action'") -and
$workerSource.Contains("fixtureIsolation='ONE_SCENE_ONE_DISPOSABLE_PHASE_ROOT'") -and
$prepareSource.Contains("[switch]`$PlanOnly") -and
Expand Down Expand Up @@ -3045,6 +3048,7 @@ function Invoke-CapMatchedContracts {
names=$names;focused=$plan;postFixFocused=$postFixPlan;focusedMutations=$focusedMutations
prelaunchPaths=(Invoke-CapFocusedPrelaunchPathContract)
expectedImageCounts=(Invoke-CapFocusedExpectedImageCountContract)
wireCancellation=(Invoke-CapMatchedWireCancellationContract)
routing=(Invoke-CapMatchedSceneRoutingContract)}
}
Invoke-CapTest 'matched tab observations preserve exact owned IDs parents selection and close bounds' {
Expand Down Expand Up @@ -4223,6 +4227,11 @@ function Invoke-CapMatchedSceneRoutingContract {
}
return $rows.ToArray()
}
function Wait-CapMatchedWireMode {
param($AppRun,$DeadlineUtc)
if (-not $model.wireArmed) {throw 'Wire mode was not armed before cancellation.'}
return (New-CapMockElement 'status' 'Choose a target loop to wire this entry' 4242 @(0,880,1200,900))
}
function Send-CapMatchedEscape {param($AppRun) $model.wireArmed=$false;return $true}
function Read-CapMatchedRows {
param($AppRun)
Expand Down Expand Up @@ -4365,6 +4374,88 @@ function Invoke-CapMatchedSceneRoutingContract {
} $selectionFunction.Extent.Text $function.Extent.Text
}

function Invoke-CapMatchedWireCancellationContract {
$ast=[Management.Automation.Language.Parser]::ParseFile($workerPath,[ref]$null,[ref]$null)
$waitFunction=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -ceq 'Wait-CapMatchedWireMode'},$true)
$cancelFunction=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -ceq 'Send-CapMatchedEscape'},$true)
Assert-CapTest ($null -ne $waitFunction -and $null -ne $cancelFunction) `
'Canonical matched Wire cancellation helpers are missing.'
& {
param($WaitDefinition,$CancelDefinition)
. ([scriptblock]::Create($WaitDefinition))
. ([scriptblock]::Create($CancelDefinition))
function Assert-CapWorkerDeadline {param($Label)}
$app=[pscustomobject]@{
Window=[IntPtr]123
Run=[pscustomobject]@{Identity=[pscustomobject]@{Pid=4242}}
}
$script:clock=[DateTime]::new(2026,10,28,12,0,0,[DateTimeKind]::Utc)
$script:polls=0
$status=Wait-CapMatchedWireMode -AppRun $app -DeadlineUtc $script:clock.AddSeconds(1) `
-StatusProbe {
param($AppRun)
$script:polls++
if ($script:polls -ge 2) {
return [pscustomobject]@{Current=[pscustomobject]@{
Name='Choose a target loop to wire this entry';AutomationId='status'
}}
}
return $null
} -NowProvider {
$script:clock=$script:clock.AddMilliseconds(10)
return $script:clock
} -SleepAction {param($Milliseconds)}
Assert-CapTest ($status.Current.Name -ceq 'Choose a target loop to wire this entry' -and
$script:polls -eq 2) 'Wire-mode wait did not observe the exact armed state.'

$script:timeoutClock=[DateTime]::new(2026,10,28,12,0,0,[DateTimeKind]::Utc)
$timeout=Assert-CapRejects {
Wait-CapMatchedWireMode -AppRun $app -DeadlineUtc $script:timeoutClock.AddMilliseconds(20) `
-StatusProbe {param($AppRun) $null} -NowProvider {
$script:timeoutClock=$script:timeoutClock.AddMilliseconds(10)
return $script:timeoutClock
} -SleepAction {param($Milliseconds)}
} 'Wire mode status did not appear before the action deadline'

$script:sent=0
$positive=Send-CapMatchedEscape -AppRun $app `
-ForegroundProbe {param($Window) $true} `
-DialogProbe {param($ProcessId) [IntPtr]::Zero} `
-KeyboardSender {param($Window,$Key) $script:sent++;return $true}
Assert-CapTest ($positive -and $script:sent -eq 1) `
'Wire cancellation did not use the canonical Escape sender exactly once.'
$wrongFocus=Assert-CapRejects {
Send-CapMatchedEscape -AppRun $app `
-ForegroundProbe {param($Window) $false} `
-DialogProbe {param($ProcessId) [IntPtr]::Zero} `
-KeyboardSender {param($Window,$Key) $true}
} 'foreground'
$dialog=Assert-CapRejects {
Send-CapMatchedEscape -AppRun $app `
-ForegroundProbe {param($Window) $true} `
-DialogProbe {param($ProcessId) [IntPtr]321} `
-KeyboardSender {param($Window,$Key) $true}
} 'dialog'
$rejected=Assert-CapRejects {
Send-CapMatchedEscape -AppRun $app `
-ForegroundProbe {param($Window) $true} `
-DialogProbe {param($ProcessId) [IntPtr]::Zero} `
-KeyboardSender {param($Window,$Key) $false}
} 'Escape'
return [pscustomobject]@{
positive=$positive
statusPolls=$script:polls
timeout=$timeout
wrongFocus=$wrongFocus
dialog=$dialog
rejected=$rejected
evidence='ACTUAL_WORKER_FUNCTIONS_SIMULATED_WINDOW_STATE_NO_GUI'
}
} $waitFunction.Extent.Text $cancelFunction.Extent.Text
}

function Invoke-CapFocusedPrelaunchPathContract {
$caseRoot=Join-Path $scratch 'focused-prelaunch-paths'
$output=Join-Path $caseRoot 'outputs'
Expand Down
3 changes: 3 additions & 0 deletions Tools/windows/UI-CAPTURE-SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ use separate disposable phase roots: the unselected scene arms and cancels
the controls to disappear with the same card identity retained. The selected
cycle scene invokes the stable `Inspect` action, returns through `Show in
Graph`, and requires the same card, action, reason, and selected identities.
Wire cancellation uses the same native key-down/key-up `PostKeyboard` route as
the live UIA gate, after a bounded exact-status wait, and refuses a non-foreground
shell or an owned native dialog before injecting Escape.
This is bounded review evidence, not an automatic parity-ledger promotion.

Both profiles require the exact 185-case real-zmx
Expand Down
17 changes: 12 additions & 5 deletions Tools/windows/UiCaptureDriver.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ $script:CapTraceFailures = [Collections.Generic.List[object]]::new()

Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, System.Drawing

# Only the required FindTopLevel/PostCommand fragments, not the executable live gate.
# Tools/windows/uia-live-gate.ps1, first Add-Type, FSL-1.1-MIT:
# SHA256 7455148BDCB64A03914812DC415B39EB300293D8970502C1BDFD110044C30795.
# Extracted GraphCodeUiaGateState.cs:
# SHA256 C2514CECBC2637202D6DAE639DF33C77901EB69ADBEDD8CB795C37C8BF92B162.
# Only the required top-level, command, keyboard and foreground fragments, not
# the executable live gate. These stay aligned with the first Add-Type in
# Tools/windows/uia-live-gate.ps1 through focused contracts below.
$helperSource = @'
using System;
using System.Runtime.InteropServices;
Expand All @@ -29,6 +27,8 @@ public static class GraphCodeUiaGateState {
private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr parameter);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(IntPtr window, StringBuilder className, int capacity);
public static IntPtr FindTopLevel(string className, uint processId) {
Expand All @@ -48,6 +48,13 @@ public static class GraphCodeUiaGateState {
public static bool PostCommand(IntPtr window, uint command) {
return PostMessage(window, 0x0111, (UIntPtr)command, IntPtr.Zero);
}
public static bool PostKeyboard(IntPtr window, uint key) {
return PostMessage(window, 0x0100, (UIntPtr)key, IntPtr.Zero) &&
PostMessage(window, 0x0101, (UIntPtr)key, IntPtr.Zero);
}
public static bool IsForegroundWindow(IntPtr window) {
return window != IntPtr.Zero && GetForegroundWindow() == window;
}
public static bool PostHover(IntPtr window, int x, int y) {
return PostMessage(window, 0x0200, UIntPtr.Zero, (IntPtr)((y << 16) | (x & 0xffff)));
}
Expand Down
Loading