feat: exercise interactions, input synthesis, and hit-testing raw

Every interaction records pre-state, action, then an independent re-read --
never the call's return value. Toggle proves why: TogglePattern flips WPF
ToggleState without raising Click, so the fixture's status sink stays silent
while the element itself changed. Trusting the sink alone would have recorded
a failure that did not happen.

Posted keystrokes are not uniformly dead. WM_KEYDOWN posted to a Win32 edit
control does register, because TranslateMessage runs in the target thread's
own pump and synthesises the character regardless of how the message arrived;
what the path cannot carry is modifier state. Against Chromium it does not
register at all, with every call still returning success. Establishing that
needed an idle control pass: the Chromium tree moved on its own between two
reads, and without a quiet baseline that drift would have been filed as a
keystroke landing.

The astral-plane payload survives typing. SendInput forces a surrogate pair
into two separate unicode events and the target reassembles it intact, read
back through WM_GETTEXT so the check is independent of both the injection path
and UIA.

Notepad's edit control is unreachable from the managed client, and not merely
unenumerated: TryGetCurrentPattern returns false for Text, Value and Scroll on
the handle-resolved element, while the COM census sees the same window as a
Document carrying all three.

Hit-testing is the only sound visibility primitive of the three tested. The
zero-size control is addressable by handle, enumerable by no walk, and
returned by no point. A minimized window reports an empty rect at the top
level while its descendants report real dimensions at the -32000 anchor, with
IsOffscreen false throughout -- so neither emptiness nor IsOffscreen can gate
occlusion, but ElementFromPoint correctly returns the occluder.
This commit is contained in:
Lahfir 2026-07-26 20:21:52 -06:00
parent 90fd24b4ed
commit cf596d380d
19 changed files with 4208 additions and 0 deletions

View file

@ -0,0 +1,713 @@
<#
.SYNOPSIS
Probe 05 (sub-phase 2.0, unit U5): every 2.0(3) interaction driven through UIA
patterns against probe-owned fixtures, each verified by an independent re-read.
.DESCRIPTION
Stack: managed System.Windows.Automation. Scope: app/provider.
Every row is pre-state -> action -> independently re-read post-state. The pattern
call's own return value is never the evidence: state is re-read from the element
and, wherever the fixture offers one, from a separate observable sink control
(lblStatus / lblScrollPos), which is a different UIA element than the one acted on.
Target choice is measured, not assumed. U3 established that the WinForms fixture in
default mode exposes ZERO patterns to a managed client (its server-side
IRawElementProviderSimple suppresses both the client-side proxies and WinForms' own
providers, collapsing every node to Pane) and that --host-providers restores only
two interactive elements. The WPF fixture carries the full managed pattern surface,
so it drives the pattern interactions and the WinForms modes are recorded as
comparison rows - a fixture artifact filed as such, not as a platform fact.
A failed interaction is a verdict row, not a script error.
Captures under captures/05-interactions/:
interactions.json one row per interaction with pre/post evidence
text-pattern.json TextPattern exposure on classic Notepad's Edit vs WPF's TextBox
focus.json SetFocus vs foreground, headless-invariant evidence for 2.7
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
Add-Type -AssemblyName UIAutomationClient | Out-Null
Add-Type -AssemblyName UIAutomationTypes | Out-Null
$Probe = '05-interactions'
$AE = [System.Windows.Automation.AutomationElement]
$Walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$Descendants = [System.Windows.Automation.TreeScope]::Descendants
$ControlIds = @{
chkToggle = '1001'; txtValue = '1003'; cboChoice = '1004'; btnAction = '1005'
btnMutateList = '1006'; tbSlider = '1008'; lstItems = '1010'; pnlScroll = '1011'
lblStatus = '1020'; lblScrollPos = '1022'
}
$script:Spawned = New-Object System.Collections.ArrayList
$script:Rows = New-Object System.Collections.ArrayList
function Initialize-InteractNative {
if ('AgentDesktopProbe.Interact' -as [type]) { return }
$src = @'
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
public static class Interact {
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr FindWindowExW(IntPtr parent, IntPtr child, string cls, string title);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
public static string WindowTitle(IntPtr h) {
StringBuilder sb = new StringBuilder(512);
GetWindowTextW(h, sb, sb.Capacity);
return sb.ToString();
}
public static string ForegroundTitle() {
return WindowTitle(GetForegroundWindow());
}
}
}
'@
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
}
function Start-Tracked {
param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25)
$p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec
[void]$script:Spawned.Add($p.ProcessId)
return $p
}
function Get-ChildCount {
param($Element)
$n = 0
try {
$k = $Walker.GetFirstChild($Element)
while ($null -ne $k) { $n++; $k = $Walker.GetNextSibling($k) }
} catch { }
return $n
}
function Wait-TopLevelByTitle {
param([string]$Title, [int]$TimeoutSec = 25)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ($true) {
try {
$c = $Walker.GetFirstChild($AE::RootElement)
while ($null -ne $c) {
try { if ($c.Current.Name -eq $Title) { return $c } } catch { }
$c = $Walker.GetNextSibling($c)
}
} catch { }
if ((Get-Date) -ge $deadline) { return $null }
Start-Sleep -Milliseconds 400
}
}
function Start-ScratchWpfWindow {
param([string]$Tag, [int]$Left, [int]$Top, [int]$SettleSec = 8, [int]$Attempts = 3)
$title = 'AgentDesktop Scratch WPF [' + $Tag + ']'
for ($i = 1; $i -le $Attempts; $i++) {
$proc = Start-Tracked -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
(Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'), '-Tag', $Tag,
'-Left', ([string]$Left), '-Top', ([string]$Top), '-TimeoutSeconds', '600')
Start-Sleep -Seconds $SettleSec
$element = Wait-TopLevelByTitle -Title $title -TimeoutSec 20
if ($null -ne $element -and (Get-ChildCount -Element $element) -gt 0) {
$hostPid = 0
try { $hostPid = [int]$element.Current.ProcessId } catch { }
if ($hostPid -gt 0 -and -not $script:Spawned.Contains($hostPid)) {
Register-ScratchProcessId -ProcessId $hostPid
[void]$script:Spawned.Add($hostPid)
}
return [pscustomobject]@{ Element = $element; ProcessId = $hostPid; timingLaunches = $i }
}
Write-ProbeLog -Message ('WPF automation peer not bound on attempt ' + $i + '; relaunching for a fresh HWND') -Level 'warn'
try { Stop-ScratchProcess -ProcessId $proc.ProcessId } catch { }
}
return $null
}
function Find-ScratchElement {
param($Root, [string]$Symbolic, [string]$Numeric = '')
foreach ($aid in @($Symbolic, $Numeric)) {
if ([string]::IsNullOrEmpty($aid)) { continue }
try {
$c = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $aid)
$e = $Root.FindFirst($Descendants, $c)
if ($null -ne $e) { return $e }
} catch { }
}
return $null
}
function Find-Scratch {
param($Root, [string]$Name)
$numeric = ''
if ($ControlIds.ContainsKey($Name)) { $numeric = $ControlIds[$Name] }
return (Find-ScratchElement -Root $Root -Symbolic $Name -Numeric $numeric)
}
function Get-SinkText {
param($Root, [string]$Name = 'lblStatus')
$e = Find-Scratch -Root $Root -Name $Name
if ($null -eq $e) { return '<sink-not-found>' }
try { return [string]$e.Current.Name } catch { return '<sink-read-failed>' }
}
function Get-SupportedPatternNames {
param($El)
$names = @()
try {
foreach ($p in @($El.GetSupportedPatterns())) {
$names += ($p.ProgrammaticName -replace 'PatternIdentifiers\.Pattern$', '')
}
} catch { }
return @($names | Sort-Object)
}
function Get-ElementPattern {
param($El, $PatternIdentifier)
$obj = $null
try { if ($El.TryGetCurrentPattern($PatternIdentifier, [ref]$obj)) { return $obj } } catch { }
return $null
}
function Get-TextShape {
param([Parameter(Mandatory = $true)][AllowEmptyString()][AllowNull()][string]$Text)
if ($null -eq $Text) { $Text = '' }
$sha = [System.Security.Cryptography.SHA256]::Create()
$hash = (($sha.ComputeHash([System.Text.Encoding]::Unicode.GetBytes($Text))) | ForEach-Object { $_.ToString('x2') }) -join ''
$sha.Dispose()
$codepoints = 0
$pairs = 0
$replacement = 0
for ($i = 0; $i -lt $Text.Length; $i++) {
$c = $Text[$i]
if ($c -eq [char]0xFFFD) { $replacement++ }
$codepoints++
if ([char]::IsHighSurrogate($c) -and ($i + 1) -lt $Text.Length -and [char]::IsLowSurrogate($Text[$i + 1])) {
$pairs++
$i++
}
}
return [ordered]@{
utf16Units = $Text.Length
codepoints = $codepoints
surrogatePairs = $pairs
replacementChars = $replacement
sha256Utf16 = $hash
}
}
function Add-InteractionRow {
param(
[string]$Interaction, [string]$Target, [string]$AutomationId,
[string]$Pattern, $Evidence
)
$row = [ordered]@{
interaction = $Interaction
target = $Target
stack = 'managed-System.Windows.Automation'
automationId = $AutomationId
pattern = $Pattern
}
foreach ($k in $Evidence.Keys) { $row[$k] = $Evidence[$k] }
[void]$script:Rows.Add($row)
}
function Add-PatternAbsentRow {
param([string]$Interaction, [string]$Target, [string]$AutomationId, [string]$Pattern, $El, [string]$Detail)
$supported = @()
$controlType = '<element-not-found>'
if ($null -ne $El) {
$supported = Get-SupportedPatternNames -El $El
try { $controlType = $El.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { }
}
Add-InteractionRow -Interaction $Interaction -Target $Target -AutomationId $AutomationId -Pattern $Pattern -Evidence ([ordered]@{
controlType = $controlType
patternAcquired = $false
supportedPatterns = @($supported)
verdict = 'pattern-unavailable'
detail = $Detail
})
}
$status = 'ok'
$message = ''
$resultData = [ordered]@{}
try {
Initialize-ProbeNative
Initialize-InteractNative
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
if (-not (Test-Path -LiteralPath $scratchExe)) {
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
}
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
$asciiPayload = 'probe-value-ascii-01'
$cjkPayload = [char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x6587) + [char]::ConvertFromUtf32(0x30C6)
$astralPayload = 'a' + [char]::ConvertFromUtf32(0x1F600) + 'z'
$mixedPayload = $asciiPayload + '-' + $cjkPayload + '-' + $astralPayload
# --- WPF first: its automation peer binds once and never re-resolves ----
$wpf = Start-ScratchWpfWindow -Tag 'u5' -Left 400 -Top 250
if ($null -eq $wpf) { throw 'WPF scratch window never bound its automation peer across three launches' }
$wpfRoot = $wpf.Element
$wpfHandle = [IntPtr][int]$wpfRoot.Current.NativeWindowHandle
$wfDefault = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u5-default', '--pos', '0,0')
if ($wfDefault.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms default-mode window never appeared' }
$wfDefaultRoot = $AE::FromHandle($wfDefault.MainWindowHandle)
$wfHosted = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u5-hosted', '--pos', '860,0', '--host-providers')
if ($wfHosted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never appeared' }
$wfHostedRoot = $AE::FromHandle($wfHosted.MainWindowHandle)
$targets = @(
[pscustomobject]@{ Label = 'wpf'; Root = $wpfRoot },
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot },
[pscustomobject]@{ Label = 'winforms-default'; Root = $wfDefaultRoot }
)
# --- invoke -------------------------------------------------------------
foreach ($t in $targets) {
$el = Find-Scratch -Root $t.Root -Name 'btnAction'
$ip = $null
if ($null -ne $el) { $ip = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.InvokePattern]::Pattern) }
if ($null -eq $ip) {
Add-PatternAbsentRow -Interaction 'invoke' -Target $t.Label -AutomationId 'btnAction' -Pattern 'Invoke' -El $el `
-Detail 'no InvokePattern on this element in this fixture mode; recorded as a verdict row'
continue
}
$before = Get-SinkText -Root $t.Root
$ip.Invoke()
Start-Sleep -Milliseconds 500
$after = Get-SinkText -Root $t.Root
Add-InteractionRow -Interaction 'invoke' -Target $t.Label -AutomationId 'btnAction' -Pattern 'Invoke' -Evidence ([ordered]@{
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
supportedPatterns = @(Get-SupportedPatternNames -El $el)
sinkElement = 'lblStatus (a different element than the one invoked)'
sinkBefore = $before
sinkAfter = $after
changed = ($before -ne $after)
verdict = $(if ($after -eq 'action:1') { 'ok' } else { 'unexpected-sink-value' })
})
}
# --- toggle -------------------------------------------------------------
foreach ($t in $targets) {
$el = Find-Scratch -Root $t.Root -Name 'chkToggle'
$tp = $null
if ($null -ne $el) { $tp = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.TogglePattern]::Pattern) }
if ($null -eq $tp) {
Add-PatternAbsentRow -Interaction 'toggle' -Target $t.Label -AutomationId 'chkToggle' -Pattern 'Toggle' -El $el `
-Detail 'no TogglePattern on this element in this fixture mode; recorded as a verdict row'
continue
}
$pre = [string]$tp.Current.ToggleState
$sinkBefore = Get-SinkText -Root $t.Root
$tp.Toggle()
Start-Sleep -Milliseconds 500
$post = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'chkToggle') `
-PatternIdentifier ([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState
$sinkAfter = Get-SinkText -Root $t.Root
Add-InteractionRow -Interaction 'toggle' -Target $t.Label -AutomationId 'chkToggle' -Pattern 'Toggle' -Evidence ([ordered]@{
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
preState = $pre
postState = $post
reReadMethod = 'the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned'
sinkBefore = $sinkBefore
sinkAfter = $sinkAfter
sinkChanged = ($sinkBefore -ne $sinkAfter)
sinkNote = 'the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.'
changed = ($pre -ne $post)
verdict = $(if ($pre -eq 'Off' -and $post -eq 'On') { 'ok' } else { 'unexpected-state' })
})
}
# --- set value (ASCII, CJK, astral plane) -------------------------------
$valueTargets = @(
[pscustomobject]@{ Label = 'wpf'; Root = $wpfRoot },
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot }
)
foreach ($t in $valueTargets) {
$el = Find-Scratch -Root $t.Root -Name 'txtValue'
$vp = $null
if ($null -ne $el) { $vp = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern) }
if ($null -eq $vp) {
Add-PatternAbsentRow -Interaction 'set-value' -Target $t.Label -AutomationId 'txtValue' -Pattern 'Value' -El $el `
-Detail 'no ValuePattern on this element in this fixture mode; recorded as a verdict row'
continue
}
foreach ($payload in @(
[pscustomobject]@{ Kind = 'ascii'; Text = $asciiPayload },
[pscustomobject]@{ Kind = 'cjk'; Text = $cjkPayload },
[pscustomobject]@{ Kind = 'astral-plane'; Text = $astralPayload },
[pscustomobject]@{ Kind = 'mixed'; Text = $mixedPayload })) {
$preValue = [string]$vp.Current.Value
$vp.SetValue($payload.Text)
Start-Sleep -Milliseconds 300
$fresh = Find-Scratch -Root $t.Root -Name 'txtValue'
$freshVp = Get-ElementPattern -El $fresh -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern)
$observed = ''
if ($null -ne $freshVp) { $observed = [string]$freshVp.Current.Value }
$expectedShape = Get-TextShape -Text $payload.Text
$observedShape = Get-TextShape -Text $observed
Add-InteractionRow -Interaction ('set-value/' + $payload.Kind) -Target $t.Label -AutomationId 'txtValue' -Pattern 'Value' -Evidence ([ordered]@{
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
payloadKind = $payload.Kind
payloadOrigin = 'built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file''s encoding (R12)'
preValueShape = (Get-TextShape -Text $preValue)
expectedShape = $expectedShape
observedShape = $observedShape
exactRoundTrip = ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16)
reReadMethod = 'element re-found by AutomationId and ValuePattern re-acquired before reading'
payloadHandling = 'only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture'
verdict = $(if ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16) { 'ok' } else { 'round-trip-mismatch' })
})
}
$vp.SetValue('seed-value')
}
# --- expand / collapse --------------------------------------------------
foreach ($t in $targets) {
$el = Find-Scratch -Root $t.Root -Name 'cboChoice'
$ep = $null
if ($null -ne $el) { $ep = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern) }
if ($null -eq $ep) {
Add-PatternAbsentRow -Interaction 'expand-collapse' -Target $t.Label -AutomationId 'cboChoice' -Pattern 'ExpandCollapse' -El $el `
-Detail 'no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row'
continue
}
$pre = [string]$ep.Current.ExpandCollapseState
$ep.Expand()
Start-Sleep -Milliseconds 600
$expanded = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'cboChoice') `
-PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern)).Current.ExpandCollapseState
$ep.Collapse()
Start-Sleep -Milliseconds 600
$collapsed = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'cboChoice') `
-PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern)).Current.ExpandCollapseState
Add-InteractionRow -Interaction 'expand-collapse' -Target $t.Label -AutomationId 'cboChoice' -Pattern 'ExpandCollapse' -Evidence ([ordered]@{
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
preState = $pre
stateAfterExpand = $expanded
stateAfterCollapse = $collapsed
reReadMethod = 'state re-read after each half of the round trip from a freshly found element'
roundTripped = ($expanded -eq 'Expanded' -and $collapsed -eq 'Collapsed')
verdict = $(if ($expanded -eq 'Expanded' -and $collapsed -eq 'Collapsed') { 'ok' } else { 'round-trip-incomplete' })
})
}
# --- select -------------------------------------------------------------
foreach ($t in $targets) {
$list = Find-Scratch -Root $t.Root -Name 'lstItems'
$item = $null
if ($null -ne $list) {
$item = Find-ScratchElement -Root $list -Symbolic 'lstItem-Item-Charlie'
if ($null -eq $item) {
$itemCondition = New-Object System.Windows.Automation.PropertyCondition($AE::ControlTypeProperty, [System.Windows.Automation.ControlType]::ListItem)
$found = @($list.FindAll($Descendants, $itemCondition))
if ($found.Count -ge 3) { $item = $found[2] }
}
}
$sip = $null
if ($null -ne $item) { $sip = Get-ElementPattern -El $item -PatternIdentifier ([System.Windows.Automation.SelectionItemPattern]::Pattern) }
if ($null -eq $sip) {
Add-PatternAbsentRow -Interaction 'select' -Target $t.Label -AutomationId 'lstItems/item[2]' -Pattern 'SelectionItem' -El $item `
-Detail 'no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row'
continue
}
$selPattern = Get-ElementPattern -El $list -PatternIdentifier ([System.Windows.Automation.SelectionPattern]::Pattern)
$preCount = -1
if ($null -ne $selPattern) { $preCount = @($selPattern.Current.GetSelection()).Count }
$sinkBefore = Get-SinkText -Root $t.Root
$sip.Select()
Start-Sleep -Milliseconds 500
$postCount = -1
$postIsSelected = $false
$freshSel = Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'lstItems') -PatternIdentifier ([System.Windows.Automation.SelectionPattern]::Pattern)
if ($null -ne $freshSel) { $postCount = @($freshSel.Current.GetSelection()).Count }
$freshItem = Get-ElementPattern -El $item -PatternIdentifier ([System.Windows.Automation.SelectionItemPattern]::Pattern)
if ($null -ne $freshItem) { $postIsSelected = [bool]$freshItem.Current.IsSelected }
$sinkAfter = Get-SinkText -Root $t.Root
Add-InteractionRow -Interaction 'select' -Target $t.Label -AutomationId 'lstItems/item[2]' -Pattern 'SelectionItem' -Evidence ([ordered]@{
controlType = ($item.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
selectionCountBefore = $preCount
selectionCountAfter = $postCount
isSelectedAfter = $postIsSelected
sinkBefore = $sinkBefore
sinkAfter = $sinkAfter
reReadMethod = 'container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink'
verdict = $(if ($postIsSelected -and $postCount -eq 1) { 'ok' } else { 'unexpected-selection-state' })
})
}
# --- scroll via pattern -------------------------------------------------
foreach ($t in @(
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot },
[pscustomobject]@{ Label = 'winforms-default'; Root = $wfDefaultRoot })) {
$panel = Find-Scratch -Root $t.Root -Name 'pnlScroll'
$sp = $null
if ($null -ne $panel) { $sp = Get-ElementPattern -El $panel -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern) }
if ($null -eq $sp) {
Add-PatternAbsentRow -Interaction 'scroll-via-pattern' -Target $t.Label -AutomationId 'pnlScroll' -Pattern 'Scroll' -El $panel `
-Detail 'the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict.'
continue
}
$sinkBefore = Get-SinkText -Root $t.Root -Name 'lblScrollPos'
$percentBefore = [double]$sp.Current.VerticalScrollPercent
$sp.Scroll([System.Windows.Automation.ScrollAmount]::NoAmount, [System.Windows.Automation.ScrollAmount]::LargeIncrement)
Start-Sleep -Milliseconds 700
$freshSp = Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'pnlScroll') -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
$percentAfter = $percentBefore
if ($null -ne $freshSp) { $percentAfter = [double]$freshSp.Current.VerticalScrollPercent }
$sinkAfter = Get-SinkText -Root $t.Root -Name 'lblScrollPos'
Add-InteractionRow -Interaction 'scroll-via-pattern' -Target $t.Label -AutomationId 'pnlScroll' -Pattern 'Scroll' -Evidence ([ordered]@{
controlType = ($panel.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
patternAcquired = $true
supportedPatterns = @(Get-SupportedPatternNames -El $panel)
verticalPercentBefore = [Math]::Round($percentBefore, 2)
verticalPercentAfter = [Math]::Round($percentAfter, 2)
sinkElement = 'lblScrollPos, driven by a 100 ms timer inside the fixture, so it registers pattern, wheel and keyboard scrolling alike'
sinkBefore = $sinkBefore
sinkAfter = $sinkAfter
changed = ($sinkBefore -ne $sinkAfter)
wheelCounterpart = 'the SendInput wheel arm on this same control and this same sink is recorded separately in captures/06-input-synthesis/mouse.json'
verdict = $(if ($sinkBefore -ne $sinkAfter) { 'ok' } else { 'no-observed-scroll' })
})
}
$wpfList = Find-Scratch -Root $wpfRoot -Name 'lstItems'
$wpfScroll = $null
if ($null -ne $wpfList) { $wpfScroll = Get-ElementPattern -El $wpfList -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern) }
if ($null -eq $wpfScroll) {
Add-PatternAbsentRow -Interaction 'scroll-via-pattern' -Target 'wpf' -AutomationId 'lstItems' -Pattern 'Scroll' -El $wpfList -Detail 'ScrollPattern not acquirable'
} else {
$shrinkSteps = @(240, 190, 160)
$stepUsed = 0
foreach ($h in $shrinkSteps) {
if ([bool]$wpfScroll.Current.VerticallyScrollable) { break }
Show-WindowNoActivate -WindowHandle $wpfHandle -X 400 -Y 250 -Width 460 -Height $h
Start-Sleep -Milliseconds 700
$wpfList = Find-Scratch -Root $wpfRoot -Name 'lstItems'
$wpfScroll = Get-ElementPattern -El $wpfList -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
$stepUsed = $h
}
$scrollable = [bool]$wpfScroll.Current.VerticallyScrollable
$percentBefore = [double]$wpfScroll.Current.VerticalScrollPercent
$verdict = 'not-scrollable'
$percentAfter = $percentBefore
if ($scrollable) {
$wpfScroll.SetScrollPercent([System.Windows.Automation.ScrollPattern]::NoScroll, 100.0)
Start-Sleep -Milliseconds 600
$fresh = Get-ElementPattern -El (Find-Scratch -Root $wpfRoot -Name 'lstItems') -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
if ($null -ne $fresh) { $percentAfter = [double]$fresh.Current.VerticalScrollPercent }
$verdict = $(if ($percentAfter -gt $percentBefore) { 'ok' } else { 'no-observed-scroll' })
}
Add-InteractionRow -Interaction 'scroll-via-pattern' -Target 'wpf' -AutomationId 'lstItems' -Pattern 'Scroll' -Evidence ([ordered]@{
controlType = 'List'
patternAcquired = $true
makeScrollableMethod = 'the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded'
shrinkStepUsed = $stepUsed
verticallyScrollable = $scrollable
verticalPercentBefore = [Math]::Round($percentBefore, 2)
verticalPercentAfter = [Math]::Round($percentAfter, 2)
reReadMethod = 'percent re-read from a freshly acquired ScrollPattern on a freshly found element'
verdict = $verdict
})
}
Show-WindowNoActivate -WindowHandle $wpfHandle -X 400 -Y 250 -Width 460 -Height 430
Start-Sleep -Milliseconds 500
[void](Write-ProbeJson -Probe $Probe -Name 'interactions.json' -InputObject ([ordered]@{
probe = $Probe
question = 'does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return'
stack = 'managed-System.Windows.Automation'
scope = 'app/provider'
verificationDiscipline = 'no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.'
fixtureArtifactNote = 'winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms'' own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.'
managedVsComNote = 'several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.'
wheelCounterpartNote = 'the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.'
rows = @($script:Rows)
}))
# --- text pattern -------------------------------------------------------
$textRows = New-Object System.Collections.ArrayList
$wpfEdit = Find-Scratch -Root $wpfRoot -Name 'txtValue'
$wpfText = Get-ElementPattern -El $wpfEdit -PatternIdentifier ([System.Windows.Automation.TextPattern]::Pattern)
if ($null -ne $wpfText) {
$wpfValue = Get-ElementPattern -El $wpfEdit -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern)
$wpfValue.SetValue($mixedPayload)
Start-Sleep -Milliseconds 300
$documentText = $wpfText.DocumentRange.GetText(-1)
$selectionBefore = @($wpfText.GetSelection())
$range = $wpfText.DocumentRange.Clone()
[void]$range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 2)
[void]$range.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $range, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start)
[void]$range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, [System.Windows.Automation.Text.TextUnit]::Character, 5)
$range.Select()
Start-Sleep -Milliseconds 300
$selectionAfter = @($wpfText.GetSelection())
$selectedText = ''
if ($selectionAfter.Count -gt 0) { $selectedText = $selectionAfter[0].GetText(-1) }
$caret = $wpfText.DocumentRange.Clone()
[void]$caret.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 4)
[void]$caret.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $caret, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start)
$caret.Select()
Start-Sleep -Milliseconds 300
$caretSelection = @($wpfText.GetSelection())
$caretText = ''
if ($caretSelection.Count -gt 0) { $caretText = $caretSelection[0].GetText(-1) }
[void]$textRows.Add([ordered]@{
target = 'wpf-txtValue'
stack = 'managed-System.Windows.Automation'
controlType = 'Edit'
className = [string]$wpfEdit.Current.ClassName
supportedPatterns = @(Get-SupportedPatternNames -El $wpfEdit)
textPatternExposed = $true
getText = [ordered]@{
expectedShape = (Get-TextShape -Text $mixedPayload)
observedShape = (Get-TextShape -Text $documentText)
exactMatch = ((Get-TextShape -Text $mixedPayload).sha256Utf16 -eq (Get-TextShape -Text $documentText).sha256Utf16)
note = 'the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument'
}
selection = [ordered]@{
rangesBefore = $selectionBefore.Count
rangesAfter = $selectionAfter.Count
requestedUnits = 5
selectedShape = (Get-TextShape -Text $selectedText)
matchesSubstring = ($selectedText -eq $mixedPayload.Substring(2, 5))
}
caret = [ordered]@{
method = 'a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read'
rangesAfter = $caretSelection.Count
degenerate = ($caretText.Length -eq 0)
}
insert = [ordered]@{
method = 'TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above'
verified = ((Get-TextShape -Text $mixedPayload).sha256Utf16 -eq (Get-TextShape -Text $documentText).sha256Utf16)
}
})
$wpfValue.SetValue('seed-value')
}
$notepad = Start-Tracked -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe')
if ($notepad.MainWindowHandle -ne [IntPtr]::Zero) {
Show-WindowNoActivate -WindowHandle $notepad.MainWindowHandle -X 900 -Y 300 -Width 600 -Height 400
Start-Sleep -Milliseconds 900
$notepadRoot = $AE::FromHandle($notepad.MainWindowHandle)
$editHandle = [AgentDesktopProbe.Interact]::FindWindowExW($notepad.MainWindowHandle, [IntPtr]::Zero, 'Edit', $null)
$probes = New-Object System.Collections.ArrayList
$documentCondition = New-Object System.Windows.Automation.PropertyCondition($AE::ControlTypeProperty, [System.Windows.Automation.ControlType]::Document)
[void]$probes.Add([pscustomobject]@{ How = 'FindFirst(Descendants, ControlType=Document)'; El = $notepadRoot.FindFirst($Descendants, $documentCondition) })
if ($editHandle -ne [IntPtr]::Zero) {
[void]$probes.Add([pscustomobject]@{ How = 'AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)'; El = $AE::FromHandle($editHandle) })
}
foreach ($p in $probes) {
$row = [ordered]@{
target = 'notepad-edit'
stack = 'managed-System.Windows.Automation'
lookup = $p.How
resolved = ($null -ne $p.El)
}
if ($null -ne $p.El) {
$row['controlType'] = ($p.El.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
$row['className'] = [string]$p.El.Current.ClassName
$row['supportedPatterns'] = @(Get-SupportedPatternNames -El $p.El)
foreach ($pattern in @(
[pscustomobject]@{ Name = 'Text'; Id = [System.Windows.Automation.TextPattern]::Pattern },
[pscustomobject]@{ Name = 'Value'; Id = [System.Windows.Automation.ValuePattern]::Pattern },
[pscustomobject]@{ Name = 'Scroll'; Id = [System.Windows.Automation.ScrollPattern]::Pattern })) {
$row[('tryGetCurrentPattern_' + $pattern.Name)] = ($null -ne (Get-ElementPattern -El $p.El -PatternIdentifier $pattern.Id))
}
}
[void]$textRows.Add($row)
}
}
[void](Write-ProbeJson -Probe $Probe -Name 'text-pattern.json' -InputObject ([ordered]@{
probe = $Probe
question = 'is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it'
stack = 'managed-System.Windows.Automation'
scope = 'app/provider'
whyTwoLookups = 'GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.'
comCounterpart = 'captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.'
p2o12Relevance = '2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.'
rows = @($textRows)
}))
# --- focus vs foreground ------------------------------------------------
[void][AgentDesktopProbe.Native]::ShowWindow($wfDefault.MainWindowHandle, 6)
Start-Sleep -Milliseconds 500
[void][AgentDesktopProbe.Native]::ShowWindow($wfDefault.MainWindowHandle, 9)
Start-Sleep -Milliseconds 900
$fgPidBefore = [AgentDesktopProbe.Native]::GetForegroundProcessId()
$fgTitleBefore = [AgentDesktopProbe.Interact]::ForegroundTitle()
$focusTarget = Find-Scratch -Root $wpfRoot -Name 'txtValue'
$focusError = ''
$hadFocusBefore = $false
try { $hadFocusBefore = [bool]$focusTarget.Current.HasKeyboardFocus } catch { }
try { $focusTarget.SetFocus() } catch { $focusError = ($_.Exception.Message -replace '[\r\n]+', ' ') }
Start-Sleep -Milliseconds 900
$fgPidAfter = [AgentDesktopProbe.Native]::GetForegroundProcessId()
$fgTitleAfter = [AgentDesktopProbe.Interact]::ForegroundTitle()
$hasFocusAfter = $false
try { $hasFocusAfter = [bool](Find-Scratch -Root $wpfRoot -Name 'txtValue').Current.HasKeyboardFocus } catch { }
$focusedAutomationId = ''
try { $focusedAutomationId = [string]$AE::FocusedElement.Current.AutomationId } catch { }
[void](Write-ProbeJson -Probe $Probe -Name 'focus.json' -InputObject ([ordered]@{
probe = $Probe
question = 'does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window'
why = '2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.'
method = 'a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.'
foregroundBefore = [ordered]@{ windowTitle = $fgTitleBefore; processId = $fgPidBefore }
foregroundAfter = [ordered]@{ windowTitle = $fgTitleAfter; processId = $fgPidAfter }
foregroundChanged = ($fgPidBefore -ne $fgPidAfter)
foregroundWasTheOtherScratchWindow = ($fgTitleBefore -like 'AgentDesktop Scratch WinForms*')
setFocusTarget = 'wpf txtValue'
setFocusError = $focusError
hadKeyboardFocusBefore = $hadFocusBefore
hasKeyboardFocusAfter = $hasFocusAfter
focusedElementAutomationIdAfter = $focusedAutomationId
titleEvidenceNote = 'the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to <pid>. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer''s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.'
verdict = $(if ($fgPidBefore -ne $fgPidAfter) { 'SetFocus moved the desktop foreground' } else { 'SetFocus did not move the desktop foreground' })
}))
$ok = @($script:Rows | Where-Object { $_.verdict -eq 'ok' }).Count
$unavailable = @($script:Rows | Where-Object { $_.verdict -eq 'pattern-unavailable' }).Count
$resultData['interactionRows'] = @($script:Rows).Count
$resultData['okRows'] = $ok
$resultData['patternUnavailableRows'] = $unavailable
$resultData['textRows'] = @($textRows).Count
$resultData['foregroundChangedOnSetFocus'] = ($fgPidBefore -ne $fgPidAfter)
$message = 'interactions: ' + $ok + ' ok / ' + $unavailable + ' pattern-unavailable of ' + @($script:Rows).Count + ' rows'
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,860 @@
<#
.SYNOPSIS
Probe 06 (sub-phase 2.0, unit U6): SendInput keyboard and mouse synthesis, the
PostMessage WM_KEYDOWN control probe behind Engineering Invariant #5, and the
teardown proof that the desktop is left exactly as it was found.
.DESCRIPTION
Stack: Win32 (SendInput, PostMessage, WM_GETTEXT) plus a bounded managed UIA read
used only as the Chromium observable. Scope: system.
Every observation of the scratch fixture is a Win32 read - GetDlgItem by control id
plus WM_GETTEXT - so the evidence does not depend on any UIA provider. That matters
here: 05-interactions measured that a managed client sees ZERO patterns on this
fixture in default mode, so a UIA-based re-read would have been unable to tell a
failed injection from an unreadable control.
KTD5 safety envelope, enforced not asserted:
* every SendInput call is bracketed by Assert-Foreground before AND after;
* on mismatch the probe stops injecting, files an interference row, and never
re-injects;
* the scratch window is brought forward with ShowWindow(SW_MINIMIZE) then
ShowWindow(SW_RESTORE) on the probe's own window - SetForegroundWindow is never
called;
* the clipboard and the cursor position are snapshotted before and restored after,
and both restorations are verified;
* the modifier sweep is conditional: modifier key state is read first and a key-up
is injected only for a modifier actually found down, so the normal path performs
no ungated injection at all.
Payload discipline: typed text and clipboard content never reach a capture. Only
UTF-16 unit counts, codepoint counts, surrogate-pair counts and a SHA-256 of the
UTF-16 bytes are recorded, and clipboard shape is compared against the hash of the
string the probe itself typed - never against the observed clipboard value.
Captures under captures/06-input-synthesis/:
keyboard.json unicode typing round trips, chord probe, modifier state
mouse.json click / absolute move / wheel / drag, all coordinate-driven
postmessage.json WM_KEYDOWN + WM_KEYUP against the scratch control and Chromium
teardown.json clipboard, cursor and modifier restoration evidence
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
Add-Type -AssemblyName System.Windows.Forms | Out-Null
Add-Type -AssemblyName UIAutomationClient | Out-Null
Add-Type -AssemblyName UIAutomationTypes | Out-Null
$Probe = '06-input-synthesis'
$AE = [System.Windows.Automation.AutomationElement]
$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$Id = @{ chkToggle = 1001; txtValue = 1003; cboChoice = 1004; btnAction = 1005
btnMutateList = 1006; btnZeroSize = 1007; tbSlider = 1008; lstItems = 1010
pnlScroll = 1011; lblStatus = 1020; lblScrollPos = 1022; lblSliderValue = 1023 }
$Modifiers = @(
[pscustomobject]@{ Name = 'VK_SHIFT'; Vk = 0x10 }, [pscustomobject]@{ Name = 'VK_CONTROL'; Vk = 0x11 },
[pscustomobject]@{ Name = 'VK_MENU'; Vk = 0x12 }, [pscustomobject]@{ Name = 'VK_LWIN'; Vk = 0x5B },
[pscustomobject]@{ Name = 'VK_RWIN'; Vk = 0x5C }, [pscustomobject]@{ Name = 'VK_LSHIFT'; Vk = 0xA0 },
[pscustomobject]@{ Name = 'VK_RSHIFT'; Vk = 0xA1 }, [pscustomobject]@{ Name = 'VK_LCONTROL'; Vk = 0xA2 },
[pscustomobject]@{ Name = 'VK_RCONTROL'; Vk = 0xA3 }, [pscustomobject]@{ Name = 'VK_LMENU'; Vk = 0xA4 },
[pscustomobject]@{ Name = 'VK_RMENU'; Vk = 0xA5 })
$script:Spawned = New-Object System.Collections.ArrayList
$script:TargetPid = 0
$script:Interference = $null
$script:Injections = 0
function Initialize-InjectorNative {
if ('AgentDesktopProbe.Injector' -as [type]) { return }
$src = @'
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
[StructLayout(LayoutKind.Sequential)]
public struct ProbeRect { public int Left; public int Top; public int Right; public int Bottom; }
[StructLayout(LayoutKind.Sequential)]
public struct ProbePoint { public int X; public int Y; }
[StructLayout(LayoutKind.Sequential)]
public struct MouseInput { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
[StructLayout(LayoutKind.Sequential)]
public struct KeybdInput { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion {
[FieldOffset(0)] public MouseInput mi;
[FieldOffset(0)] public KeybdInput ki;
}
[StructLayout(LayoutKind.Sequential)]
public struct ProbeInput { public uint type; public InputUnion u; }
public static class Injector {
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, ProbeInput[] pInputs, int cbSize);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowRect(IntPtr hWnd, out ProbeRect lpRect);
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessageBuffer(IntPtr hWnd, uint msg, IntPtr wParam, StringBuilder lParam);
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessageString(IntPtr hWnd, uint msg, IntPtr wParam, string lParam);
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessagePtr(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool PostMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr FindWindowExW(IntPtr parent, IntPtr child, string cls, string title);
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out ProbePoint lpPoint);
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll")]
public static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
public static extern short GetKeyState(int nVirtKey);
[DllImport("user32.dll")]
public static extern uint MapVirtualKeyW(uint uCode, uint uMapType);
public static int InputSize() { return Marshal.SizeOf(typeof(ProbeInput)); }
public static string GetControlText(IntPtr h) {
IntPtr len = SendMessagePtr(h, 0x000E, IntPtr.Zero, IntPtr.Zero);
int cap = (int)len + 2;
if (cap < 8) { cap = 8; }
StringBuilder sb = new StringBuilder(cap);
SendMessageBuffer(h, 0x000D, new IntPtr(cap), sb);
return sb.ToString();
}
public static void SetControlText(IntPtr h, string value) {
SendMessageString(h, 0x000C, IntPtr.Zero, value);
}
public static string PostKey(IntPtr h, uint msg, int vk) {
uint scan = MapVirtualKeyW((uint)vk, 0);
long lp = ((long)scan << 16) | 1L;
if (msg == 0x0101) { lp = lp | 0xC0000000L; }
bool ok = PostMessageW(h, msg, new IntPtr(vk), new IntPtr(lp));
int err = Marshal.GetLastWin32Error();
return ok ? "true" : ("false:" + err.ToString());
}
public static string PostChar(IntPtr h, int ch) {
bool ok = PostMessageW(h, 0x0102, new IntPtr(ch), new IntPtr(1));
int err = Marshal.GetLastWin32Error();
return ok ? "true" : ("false:" + err.ToString());
}
public delegate bool EnumWindowProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowProc callback, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassNameW(IntPtr hWnd, StringBuilder buffer, int maxCount);
public static string ClassOf(IntPtr h) {
StringBuilder sb = new StringBuilder(256);
GetClassNameW(h, sb, sb.Capacity);
return sb.ToString();
}
public static string ChildClassCensus(IntPtr parent) {
System.Collections.Generic.List<string> seen = new System.Collections.Generic.List<string>();
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
string c = ClassOf(h);
if (!seen.Contains(c)) { seen.Add(c); }
return true;
}, IntPtr.Zero);
seen.Sort();
return string.Join(";", seen.ToArray());
}
public static IntPtr FindDescendantByClass(IntPtr parent, string wanted) {
IntPtr found = IntPtr.Zero;
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
if (found != IntPtr.Zero) { return false; }
if (ClassOf(h) == wanted) { found = h; return false; }
return true;
}, IntPtr.Zero);
return found;
}
public static uint SendUnicodeUnit(ushort unit) {
ProbeInput[] inputs = new ProbeInput[2];
inputs[0].type = 1;
inputs[0].u.ki.wScan = unit;
inputs[0].u.ki.dwFlags = 0x0004;
inputs[1].type = 1;
inputs[1].u.ki.wScan = unit;
inputs[1].u.ki.dwFlags = 0x0004 | 0x0002;
return SendInput(2, inputs, InputSize());
}
public static uint SendVirtualKey(ushort vk, bool keyUp) {
ProbeInput[] inputs = new ProbeInput[1];
inputs[0].type = 1;
inputs[0].u.ki.wVk = vk;
inputs[0].u.ki.dwFlags = keyUp ? (uint)0x0002 : (uint)0;
return SendInput(1, inputs, InputSize());
}
public static uint MouseMoveAbsolute(int x, int y) {
int cx = GetSystemMetrics(0);
int cy = GetSystemMetrics(1);
ProbeInput[] inputs = new ProbeInput[1];
inputs[0].type = 0;
inputs[0].u.mi.dx = (int)(((double)x * 65535.0) / (double)(cx - 1));
inputs[0].u.mi.dy = (int)(((double)y * 65535.0) / (double)(cy - 1));
inputs[0].u.mi.dwFlags = 0x0001 | 0x8000;
return SendInput(1, inputs, InputSize());
}
public static uint MouseButton(bool down) {
ProbeInput[] inputs = new ProbeInput[1];
inputs[0].type = 0;
inputs[0].u.mi.dwFlags = down ? (uint)0x0002 : (uint)0x0004;
return SendInput(1, inputs, InputSize());
}
public static uint MouseWheel(int delta) {
ProbeInput[] inputs = new ProbeInput[1];
inputs[0].type = 0;
inputs[0].u.mi.mouseData = unchecked((uint)delta);
inputs[0].u.mi.dwFlags = 0x0800;
return SendInput(1, inputs, InputSize());
}
}
}
'@
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
}
function Get-TextShape {
param([Parameter(Mandatory = $true)][AllowEmptyString()][AllowNull()][string]$Text)
if ($null -eq $Text) { $Text = '' }
$sha = [System.Security.Cryptography.SHA256]::Create()
$hash = (($sha.ComputeHash([System.Text.Encoding]::Unicode.GetBytes($Text))) | ForEach-Object { $_.ToString('x2') }) -join ''
$sha.Dispose()
$codepoints = 0
$pairs = 0
$replacement = 0
for ($i = 0; $i -lt $Text.Length; $i++) {
if ($Text[$i] -eq [char]0xFFFD) { $replacement++ }
$codepoints++
if ([char]::IsHighSurrogate($Text[$i]) -and ($i + 1) -lt $Text.Length -and [char]::IsLowSurrogate($Text[$i + 1])) {
$pairs++
$i++
}
}
return [ordered]@{
utf16Units = $Text.Length
codepoints = $codepoints
surrogatePairs = $pairs
replacementChars = $replacement
sha256Utf16 = $hash
}
}
function Get-ControlHandle {
param([IntPtr]$Form, [string]$Name)
return [AgentDesktopProbe.Injector]::GetDlgItem($Form, $Id[$Name])
}
function Get-ControlText {
param([IntPtr]$Handle)
if ($Handle -eq [IntPtr]::Zero) { return '<no-handle>' }
return [AgentDesktopProbe.Injector]::GetControlText($Handle)
}
function Get-ControlCenter {
param([IntPtr]$Handle)
$r = New-Object AgentDesktopProbe.ProbeRect
[void][AgentDesktopProbe.Injector]::GetWindowRect($Handle, [ref]$r)
return [pscustomobject]@{
CenterX = [int](($r.Left + $r.Right) / 2)
CenterY = [int](($r.Top + $r.Bottom) / 2)
Rect = ([string]$r.Left + ',' + $r.Top + ',' + ($r.Right - $r.Left) + ',' + ($r.Bottom - $r.Top))
Left = $r.Left
Top = $r.Top
Right = $r.Right
Bottom = $r.Bottom
}
}
function Invoke-Injected {
param([Parameter(Mandatory = $true)][string]$Stage, [Parameter(Mandatory = $true)][scriptblock]$Action)
if ($null -ne $script:Interference) { return $false }
try { Assert-Foreground -ExpectedProcessId $script:TargetPid -Stage ($Stage + ':pre') }
catch {
$script:Interference = [ordered]@{ stage = ($Stage + ':pre'); detail = ($_.Exception.Message -replace '[\r\n]+', ' ') }
Write-ProbeLog -Message ('interference before ' + $Stage + '; no further injection will be attempted') -Level 'warn'
return $false
}
& $Action | Out-Null
$script:Injections++
Start-Sleep -Milliseconds 120
try { Assert-Foreground -ExpectedProcessId $script:TargetPid -Stage ($Stage + ':post') }
catch {
$script:Interference = [ordered]@{ stage = ($Stage + ':post'); detail = ($_.Exception.Message -replace '[\r\n]+', ' ') }
Write-ProbeLog -Message ('interference after ' + $Stage + '; no further injection will be attempted') -Level 'warn'
return $false
}
return $true
}
function Send-TypedText {
param([string]$Stage, [string]$Text)
$units = 0
for ($i = 0; $i -lt $Text.Length; $i++) {
$unit = [uint16][int][char]$Text[$i]
$ok = Invoke-Injected -Stage ($Stage + ':unit' + $i) -Action ([scriptblock]::Create('[AgentDesktopProbe.Injector]::SendUnicodeUnit(' + $unit + ')'))
if (-not $ok) { break }
$units++
Start-Sleep -Milliseconds 25
}
return $units
}
function Get-ModifierState {
$rows = New-Object System.Collections.ArrayList
foreach ($m in $Modifiers) {
$async = [AgentDesktopProbe.Injector]::GetAsyncKeyState($m.Vk)
$sync = [AgentDesktopProbe.Injector]::GetKeyState($m.Vk)
[void]$rows.Add([ordered]@{
key = $m.Name
asyncKeyIsDown = (($async -band 0x8000) -ne 0)
keyStateIsDown = (($sync -band 0x8000) -ne 0)
})
}
return @($rows)
}
function Get-SubtreeFingerprint {
param($Root, [int]$Budget = 250, [int]$MaxDepth = 10)
$sb = New-Object System.Text.StringBuilder
$count = 0
$stack = New-Object System.Collections.Stack
$stack.Push(@{ El = $Root; Depth = 0 })
while ($stack.Count -gt 0 -and $count -lt $Budget) {
$item = $stack.Pop()
$cur = $null
try { $cur = $item.El.Current } catch { }
if ($null -eq $cur) { continue }
$count++
try { [void]$sb.Append($cur.ControlType.ProgrammaticName).Append('|').Append([string]$cur.Name).Append("`n") } catch { }
if ($item.Depth -ge $MaxDepth) { continue }
try {
$k = $RawWalker.GetFirstChild($item.El)
$kids = New-Object System.Collections.ArrayList
while ($null -ne $k) { [void]$kids.Add($k); $k = $RawWalker.GetNextSibling($k) }
for ($i = $kids.Count - 1; $i -ge 0; $i--) { $stack.Push(@{ El = $kids[$i]; Depth = ($item.Depth + 1) }) }
} catch { }
}
$shape = Get-TextShape -Text $sb.ToString()
return [ordered]@{ nodes = $count; sha256Utf16 = $shape.sha256Utf16 }
}
$status = 'ok'
$message = ''
$resultData = [ordered]@{}
$clipHadText = $false
$clipOriginal = ''
$clipKinds = @()
$clipSnapshotTaken = $false
$cursorOrigin = $null
$teardown = [ordered]@{}
try {
Initialize-ProbeNative
Initialize-InjectorNative
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
if (-not (Test-Path -LiteralPath $scratchExe)) {
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
}
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
$cursorOrigin = New-Object AgentDesktopProbe.ProbePoint
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$cursorOrigin)
$clipKinds = @()
try {
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $clipKinds += 'text' }
if ([System.Windows.Forms.Clipboard]::ContainsImage()) { $clipKinds += 'image' }
if ([System.Windows.Forms.Clipboard]::ContainsFileDropList()) { $clipKinds += 'filedroplist' }
if ([System.Windows.Forms.Clipboard]::ContainsAudio()) { $clipKinds += 'audio' }
$clipHadText = [System.Windows.Forms.Clipboard]::ContainsText()
if ($clipHadText) { $clipOriginal = [System.Windows.Forms.Clipboard]::GetText() }
$clipSnapshotTaken = $true
} catch {
Write-ProbeLog -Message ('clipboard snapshot failed: ' + $_.Exception.Message) -Level 'warn'
}
$clipOriginalShape = Get-TextShape -Text $clipOriginal
$scratch = Start-ScratchProcess -FilePath $scratchExe -ArgumentList @('--tag', 'u6', '--pos', '100,100') -NoActivate -TimeoutSec 25
[void]$script:Spawned.Add($scratch.ProcessId)
if ($scratch.MainWindowHandle -eq [IntPtr]::Zero) { throw 'scratch window never appeared' }
$script:TargetPid = $scratch.ProcessId
$form = $scratch.MainWindowHandle
[void][AgentDesktopProbe.Native]::ShowWindow($form, 6)
Start-Sleep -Milliseconds 500
[void][AgentDesktopProbe.Native]::ShowWindow($form, 9)
Start-Sleep -Milliseconds 900
$foregroundAcquired = ([AgentDesktopProbe.Native]::GetForegroundProcessId() -eq $script:TargetPid)
if (-not $foregroundAcquired) {
$script:Interference = [ordered]@{
stage = 'foreground-acquisition'
detail = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned window did not make it foreground; observed pid ' + [AgentDesktopProbe.Native]::GetForegroundProcessId()
}
}
$hTxt = Get-ControlHandle -Form $form -Name 'txtValue'
$hStatus = Get-ControlHandle -Form $form -Name 'lblStatus'
$hScrollPos = Get-ControlHandle -Form $form -Name 'lblScrollPos'
$hSliderLabel = Get-ControlHandle -Form $form -Name 'lblSliderValue'
$hSlider = Get-ControlHandle -Form $form -Name 'tbSlider'
$hPanel = Get-ControlHandle -Form $form -Name 'pnlScroll'
$hAction = Get-ControlHandle -Form $form -Name 'btnAction'
# --- keyboard: unicode typing round trips -------------------------------
$typedRows = New-Object System.Collections.ArrayList
$payloads = @(
[pscustomobject]@{ Kind = 'ascii'; Text = 'probe-typed-01' },
[pscustomobject]@{ Kind = 'cjk'; Text = ([char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x6587) + [char]::ConvertFromUtf32(0x30C6)) },
[pscustomobject]@{ Kind = 'astral-plane'; Text = ('a' + [char]::ConvertFromUtf32(0x1F600) + 'z') },
[pscustomobject]@{ Kind = 'mixed'; Text = ('x' + [char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x1F600) + 'y') })
$editCenter = Get-ControlCenter -Handle $hTxt
[void](Invoke-Injected -Stage 'focus-edit:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($editCenter.CenterX, $editCenter.CenterY) })
[void](Invoke-Injected -Stage 'focus-edit:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
[void](Invoke-Injected -Stage 'focus-edit:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
Start-Sleep -Milliseconds 300
foreach ($p in $payloads) {
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
Start-Sleep -Milliseconds 150
$cleared = Get-ControlText -Handle $hTxt
$unitsSent = Send-TypedText -Stage ('type-' + $p.Kind) -Text $p.Text
Start-Sleep -Milliseconds 400
$observed = Get-ControlText -Handle $hTxt
$expectedShape = Get-TextShape -Text $p.Text
$observedShape = Get-TextShape -Text $observed
[void]$typedRows.Add([ordered]@{
payloadKind = $p.Kind
payloadOrigin = 'built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks'
clearedBeforeTyping = ($cleared.Length -eq 0)
utf16UnitsSent = $unitsSent
expectedShape = $expectedShape
observedShape = $observedShape
exactRoundTrip = ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16)
surrogatePairSurvived = ($expectedShape.surrogatePairs -eq $observedShape.surrogatePairs)
reReadMethod = 'WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA'
verdict = $(if ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16) { 'ok' } elseif ($null -ne $script:Interference) { 'aborted-on-interference' } else { 'chunk-boundary-loss' })
})
}
# --- keyboard: modifier chord (Ctrl+A, Ctrl+C) --------------------------
$chordPayload = 'chord-probe-' + [char]::ConvertFromUtf32(0x4E2D) + '-42'
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
Start-Sleep -Milliseconds 150
[void](Send-TypedText -Stage 'type-chord-source' -Text $chordPayload)
Start-Sleep -Milliseconds 300
$chordSourceObserved = Get-ControlText -Handle $hTxt
$chordExpectedShape = Get-TextShape -Text $chordPayload
$chordSourceShape = Get-TextShape -Text $chordSourceObserved
$chordSent = $true
foreach ($step in @(
[pscustomobject]@{ Stage = 'ctrl-down-a'; Vk = 0x11; Up = $false },
[pscustomobject]@{ Stage = 'a-down'; Vk = 0x41; Up = $false },
[pscustomobject]@{ Stage = 'a-up'; Vk = 0x41; Up = $true },
[pscustomobject]@{ Stage = 'c-down'; Vk = 0x43; Up = $false },
[pscustomobject]@{ Stage = 'c-up'; Vk = 0x43; Up = $true },
[pscustomobject]@{ Stage = 'ctrl-up'; Vk = 0x11; Up = $true })) {
$action = [scriptblock]::Create('[AgentDesktopProbe.Injector]::SendVirtualKey(' + $step.Vk + ', $' + $step.Up.ToString().ToLower() + ')')
if (-not (Invoke-Injected -Stage ('chord:' + $step.Stage) -Action $action)) { $chordSent = $false; break }
Start-Sleep -Milliseconds 80
}
Start-Sleep -Milliseconds 400
$clipObserved = ''
$clipReadOk = $false
try {
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $clipObserved = [System.Windows.Forms.Clipboard]::GetText(); $clipReadOk = $true }
} catch { Write-ProbeLog -Message ('clipboard read failed: ' + $_.Exception.Message) -Level 'warn' }
$clipObservedShape = Get-TextShape -Text $clipObserved
$modifierAfterChord = Get-ModifierState
[void](Write-ProbeJson -Probe $Probe -Name 'keyboard.json' -InputObject ([ordered]@{
probe = $Probe
question = 'does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register'
stack = 'win32-SendInput'
scope = 'system'
why = "2.8's type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement."
foregroundAcquisitionMethod = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.'
foregroundAcquired = $foregroundAcquired
injectionGate = 'every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference'
interference = $script:Interference
typingRows = @($typedRows)
chord = [ordered]@{
chordSent = $chordSent
keys = 'Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated'
sourceTypedShape = $chordExpectedShape
sourceObservedShape = $chordSourceShape
sourceRoundTrip = ($chordExpectedShape.sha256Utf16 -eq $chordSourceShape.sha256Utf16)
clipboardReadOk = $clipReadOk
clipboardObservedShape = $clipObservedShape
verifiedByShape = ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16)
verificationRule = 'the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.'
verdict = $(if ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16) { 'ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard' } else { 'chord-not-registered-or-clipboard-differs' })
}
modifierStateAfterChord = @($modifierAfterChord)
modifierStateAfterChordVerdict = $(if (@($modifierAfterChord | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown }).Count -eq 0) { 'no modifier left down by the chord' } else { 'a modifier is still down after the chord' })
}))
# --- mouse: click, absolute move, wheel, drag ---------------------------
$actionCenter = Get-ControlCenter -Handle $hAction
$statusBeforeClick = Get-ControlText -Handle $hStatus
[void](Invoke-Injected -Stage 'click-btnAction:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($actionCenter.CenterX, $actionCenter.CenterY) })
Start-Sleep -Milliseconds 150
$landed = New-Object AgentDesktopProbe.ProbePoint
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$landed)
[void](Invoke-Injected -Stage 'click-btnAction:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
[void](Invoke-Injected -Stage 'click-btnAction:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
Start-Sleep -Milliseconds 400
$statusAfterClick = Get-ControlText -Handle $hStatus
$panelCenter = Get-ControlCenter -Handle $hPanel
$scrollBeforeWheel = Get-ControlText -Handle $hScrollPos
[void](Invoke-Injected -Stage 'wheel:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($panelCenter.CenterX, $panelCenter.CenterY) })
Start-Sleep -Milliseconds 200
for ($w = 0; $w -lt 3; $w++) {
[void](Invoke-Injected -Stage ('wheel:tick' + $w) -Action { [AgentDesktopProbe.Injector]::MouseWheel(-120) })
Start-Sleep -Milliseconds 200
}
Start-Sleep -Milliseconds 400
$scrollAfterWheel = Get-ControlText -Handle $hScrollPos
$scrollPixelsBefore = 0
$scrollPixelsAfter = 0
if ($scrollBeforeWheel -match 'scroll:(-?\d+)') { $scrollPixelsBefore = [int]$Matches[1] }
if ($scrollAfterWheel -match 'scroll:(-?\d+)') { $scrollPixelsAfter = [int]$Matches[1] }
$sliderRect = Get-ControlCenter -Handle $hSlider
$sliderBefore = Get-ControlText -Handle $hSliderLabel
$dragSamples = New-Object System.Collections.ArrayList
$thumbY = $sliderRect.Top + 14
[void](Invoke-Injected -Stage 'drag:move-to-thumb' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute(($sliderRect.Left + 16), $thumbY) })
Start-Sleep -Milliseconds 200
[void](Invoke-Injected -Stage 'drag:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
Start-Sleep -Milliseconds 200
for ($s = 1; $s -le 6; $s++) {
$x = $sliderRect.Left + 16 + ($s * 44)
[void](Invoke-Injected -Stage ('drag:step' + $s) -Action ([scriptblock]::Create('[AgentDesktopProbe.Injector]::MouseMoveAbsolute(' + $x + ', ' + $thumbY + ')')))
Start-Sleep -Milliseconds 250
$label = Get-ControlText -Handle $hSliderLabel
$v = -1
if ($label -match 'slider:(-?\d+)') { $v = [int]$Matches[1] }
[void]$dragSamples.Add([ordered]@{ step = $s; sliderValue = $v })
}
[void](Invoke-Injected -Stage 'drag:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
Start-Sleep -Milliseconds 400
$sliderAfter = Get-ControlText -Handle $hSliderLabel
$values = @($dragSamples | ForEach-Object { $_.sliderValue })
$monotonic = $true
for ($i = 1; $i -lt $values.Count; $i++) { if ($values[$i] -lt $values[$i - 1]) { $monotonic = $false } }
$sliderValueBefore = 0
$sliderValueAfter = 0
if ($sliderBefore -match 'slider:(-?\d+)') { $sliderValueBefore = [int]$Matches[1] }
if ($sliderAfter -match 'slider:(-?\d+)') { $sliderValueAfter = [int]$Matches[1] }
[void](Write-ProbeJson -Probe $Probe -Name 'mouse.json' -InputObject ([ordered]@{
probe = $Probe
question = 'do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target'
stack = 'win32-SendInput'
scope = 'system'
coordinateModel = 'MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.'
interference = $script:Interference
click = [ordered]@{
targetControl = 'btnAction (control id 1005)'
targetRect = $actionCenter.Rect
requestedPoint = ([string]$actionCenter.CenterX + ',' + $actionCenter.CenterY)
cursorLandedPoint = ([string]$landed.X + ',' + $landed.Y)
absoluteMoveExact = ($landed.X -eq $actionCenter.CenterX -and $landed.Y -eq $actionCenter.CenterY)
absoluteMoveNote = 'the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.'
statusBefore = $statusBeforeClick
statusAfter = $statusAfterClick
registered = ($statusBeforeClick -ne $statusAfterClick)
reReadMethod = 'WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked'
}
wheel = [ordered]@{
targetControl = 'pnlScroll (control id 1011)'
targetRect = $panelCenter.Rect
ticks = 3
deltaPerTick = -120
sinkBefore = $scrollBeforeWheel
sinkAfter = $scrollAfterWheel
scrollPixelsBefore = $scrollPixelsBefore
scrollPixelsAfter = $scrollPixelsAfter
deltaScrollPixels = ($scrollPixelsAfter - $scrollPixelsBefore)
registered = ($scrollPixelsAfter -gt $scrollPixelsBefore)
patternCounterpart = 'the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.'
measuredFactNaming = 'the observed delta is named deltaScrollPixels, not "y" or "height", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away.'
}
drag = [ordered]@{
targetControl = 'tbSlider (control id 1008), Minimum 0 Maximum 100'
targetRect = $sliderRect.Rect
method = 'mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step'
sliderValueBefore = $sliderValueBefore
sliderValueAfter = $sliderValueAfter
samples = @($dragSamples)
monotonicNonDecreasing = $monotonic
increased = ($sliderValueAfter -gt $sliderValueBefore)
verdict = $(if ($monotonic -and $sliderValueAfter -gt $sliderValueBefore) { 'ok - monotonic increase under drag' } else { 'drag did not produce a monotonic increase' })
}
}))
# --- PostMessage control probe ------------------------------------------
$postRows = New-Object System.Collections.ArrayList
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
Start-Sleep -Milliseconds 200
$scratchBefore = Get-ControlText -Handle $hTxt
$downResult = [AgentDesktopProbe.Injector]::PostKey($hTxt, 0x0100, 0x5A)
Start-Sleep -Milliseconds 500
$scratchAfterDown = Get-ControlText -Handle $hTxt
$upResult = [AgentDesktopProbe.Injector]::PostKey($hTxt, 0x0101, 0x5A)
Start-Sleep -Milliseconds 500
$scratchAfterKey = Get-ControlText -Handle $hTxt
$charResult = [AgentDesktopProbe.Injector]::PostChar($hTxt, 0x5A)
Start-Sleep -Milliseconds 500
$scratchAfterChar = Get-ControlText -Handle $hTxt
[void]$postRows.Add([ordered]@{
target = 'scratch WinForms Edit (control id 1003), same session, same integrity level as the probe'
stack = 'win32-PostMessage'
integrityRelation = 'High -> High. 09-elevation-uipi measured the Medium -> High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?'
keyDownPostResult = $downResult
keyUpPostResult = $upResult
charPostResult = $charResult
resultFormat = '"true" on success, "false:<GetLastError>" on failure. The error code is only meaningful when the call failed, so it is only recorded then.'
textBeforeShape = (Get-TextShape -Text $scratchBefore)
textAfterKeyDownShape = (Get-TextShape -Text $scratchAfterDown)
textAfterKeyUpShape = (Get-TextShape -Text $scratchAfterKey)
textAfterCharShape = (Get-TextShape -Text $scratchAfterChar)
keyDownRegistered = ($scratchAfterDown -ne $scratchBefore)
keyUpAddedAnything = ($scratchAfterKey -ne $scratchAfterDown)
charMessageRegistered = ($scratchAfterChar -ne $scratchAfterKey)
measuredResult = 'the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.'
interpretation = 'this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread''s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread''s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.'
invariantRelevance = 'Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism.'
})
$obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe'
$obsidianLaunched = $false
if (-not (Test-Path -LiteralPath $obsidianExe)) {
[void]$postRows.Add([ordered]@{ target = 'chromium/electron'; verdict = 'SKIPPED - Obsidian is not installed on this box' })
} elseif (@(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue).Count -gt 0) {
[void]$postRows.Add([ordered]@{
target = 'chromium/electron'
verdict = 'SKIPPED - an Obsidian instance the probe did not launch was already running. KTD5 confines the probe to windows it launched itself, and posting synthetic keystrokes into an operator session is exactly what that rule exists to prevent.'
})
} else {
[void](Start-ScratchProcess -FilePath $obsidianExe -NoActivate -TimeoutSec 40)
$obsidianLaunched = $true
Start-Sleep -Seconds 6
foreach ($op in @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) {
if (-not $script:Spawned.Contains($op.Id)) { Register-ScratchProcessId -ProcessId $op.Id; [void]$script:Spawned.Add($op.Id) }
}
Start-Sleep -Seconds 14
$chromiumTop = [IntPtr]::Zero
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$c = $walker.GetFirstChild($AE::RootElement)
while ($null -ne $c) {
try {
if ($c.Current.ClassName -eq 'Chrome_WidgetWin_1' -and $script:Spawned.Contains([int]$c.Current.ProcessId)) {
$chromiumTop = [IntPtr][int]$c.Current.NativeWindowHandle
break
}
} catch { }
$c = $walker.GetNextSibling($c)
}
if ($chromiumTop -eq [IntPtr]::Zero) {
[void]$postRows.Add([ordered]@{ target = 'chromium/electron'; verdict = 'SKIPPED - no probe-owned Chrome_WidgetWin_1 top-level window appeared' })
} else {
$childClasses = [AgentDesktopProbe.Injector]::ChildClassCensus($chromiumTop)
$renderWidget = [AgentDesktopProbe.Injector]::FindDescendantByClass($chromiumTop, 'Chrome_RenderWidgetHostHWND')
$chromiumRoot = $AE::FromHandle($chromiumTop)
$fpSettled = Get-SubtreeFingerprint -Root $chromiumRoot
$stabilizationPasses = 0
for ($i = 1; $i -le 20; $i++) {
Start-Sleep -Seconds 2
$fpNow = Get-SubtreeFingerprint -Root $chromiumRoot
$stabilizationPasses = $i
if ($fpNow.sha256Utf16 -eq $fpSettled.sha256Utf16) { break }
$fpSettled = $fpNow
}
Start-Sleep -Seconds 5
$fpIdle = Get-SubtreeFingerprint -Root $chromiumRoot
$topDown = [AgentDesktopProbe.Injector]::PostKey($chromiumTop, 0x0100, 0x5A)
Start-Sleep -Milliseconds 200
$topUp = [AgentDesktopProbe.Injector]::PostKey($chromiumTop, 0x0101, 0x5A)
$childDown = '<no-render-widget-child>'
$childUp = '<no-render-widget-child>'
$childChar = '<no-render-widget-child>'
if ($renderWidget -ne [IntPtr]::Zero) {
Start-Sleep -Milliseconds 200
$childDown = [AgentDesktopProbe.Injector]::PostKey($renderWidget, 0x0100, 0x5A)
Start-Sleep -Milliseconds 200
$childUp = [AgentDesktopProbe.Injector]::PostKey($renderWidget, 0x0101, 0x5A)
Start-Sleep -Milliseconds 200
$childChar = [AgentDesktopProbe.Injector]::PostChar($renderWidget, 0x5A)
}
Start-Sleep -Seconds 4
$fpAfter = Get-SubtreeFingerprint -Root $chromiumRoot
$churnWithoutInput = ($fpSettled.sha256Utf16 -ne $fpIdle.sha256Utf16)
$changedAfterPost = ($fpIdle.sha256Utf16 -ne $fpAfter.sha256Utf16)
[void]$postRows.Add([ordered]@{
target = 'chromium/electron (Obsidian, launched by this probe)'
stack = 'win32-PostMessage'
obsidianVersion = (Get-Item -LiteralPath $obsidianExe).VersionInfo.FileVersion
topLevelClassName = 'Chrome_WidgetWin_1'
childWindowClasses = $childClasses
renderWidgetChildFound = ($renderWidget -ne [IntPtr]::Zero)
topLevelKeyDownPostResult = $topDown
topLevelKeyUpPostResult = $topUp
renderWidgetKeyDownPostResult = $childDown
renderWidgetKeyUpPostResult = $childUp
renderWidgetCharPostResult = $childChar
observable = 'a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.'
controlPass = 'the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.'
timingStabilizationPasses = $stabilizationPasses
nodesAtSettle = $fpSettled.nodes
nodesAfterIdleWait = $fpIdle.nodes
nodesAfterPostedKeys = $fpAfter.nodes
treeChurnWithoutInput = $churnWithoutInput
treeChangedAfterPostedKeys = $changedAfterPost
keystrokeRegistered = $changedAfterPost
verdict = $(if (-not $changedAfterPost) { 'not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk' } elseif ($churnWithoutInput) { 'INCONCLUSIVE - the tree was still changing on its own during the idle control pass, so the post-injection change cannot be attributed to the keystroke' } else { 'registered - the posted keystroke changed a Chromium tree that the idle control pass had just shown to be quiet' })
caveat = 'an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such.'
})
}
}
[void](Write-ProbeJson -Probe $Probe -Name 'postmessage.json' -InputObject ([ordered]@{
probe = $Probe
question = 'does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium'
stack = 'win32-PostMessage'
scope = 'system'
why = 'Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.'
foregroundIrrelevantNote = 'PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.'
rows = @($postRows)
}))
$resultData['injections'] = $script:Injections
$resultData['typingRows'] = @($typedRows).Count
$resultData['astralRoundTrip'] = @($typedRows | Where-Object { $_.payloadKind -eq 'astral-plane' } | ForEach-Object { $_.exactRoundTrip })
$resultData['chordVerified'] = ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16)
$resultData['wheelRegistered'] = ($scrollPixelsAfter -gt $scrollPixelsBefore)
$resultData['dragMonotonic'] = $monotonic
$resultData['interference'] = ($null -ne $script:Interference)
$message = 'sendinput ' + $script:Injections + ' gated injections; postmessage rows ' + @($postRows).Count
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
$modifierBeforeSweep = @()
$modifierAfterSweep = @()
$sweepInjected = @()
try {
$modifierBeforeSweep = Get-ModifierState
foreach ($m in @($modifierBeforeSweep | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown })) {
$vk = ($Modifiers | Where-Object { $_.Name -eq $m.key }).Vk
[void][AgentDesktopProbe.Injector]::SendVirtualKey([uint16]$vk, $true)
$sweepInjected += $m.key
Start-Sleep -Milliseconds 60
}
$modifierAfterSweep = Get-ModifierState
} catch { Write-ProbeLog -Message ('modifier sweep failed: ' + $_.Exception.Message) -Level 'warn' }
$clipRestored = $false
$clipAfterRestoreShape = $null
if ($clipSnapshotTaken) {
try {
if ($clipHadText) { [System.Windows.Forms.Clipboard]::SetText($clipOriginal) } else { [System.Windows.Forms.Clipboard]::Clear() }
Start-Sleep -Milliseconds 200
$back = ''
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $back = [System.Windows.Forms.Clipboard]::GetText() }
$clipAfterRestoreShape = Get-TextShape -Text $back
$clipRestored = ($clipAfterRestoreShape.sha256Utf16 -eq (Get-TextShape -Text $clipOriginal).sha256Utf16)
} catch { Write-ProbeLog -Message ('clipboard restore failed: ' + $_.Exception.Message) -Level 'warn' }
}
$cursorRestored = $false
$cursorAfter = $null
if ($null -ne $cursorOrigin) {
try {
[void][AgentDesktopProbe.Injector]::SetCursorPos($cursorOrigin.X, $cursorOrigin.Y)
Start-Sleep -Milliseconds 150
$cursorAfter = New-Object AgentDesktopProbe.ProbePoint
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$cursorAfter)
$cursorRestored = ($cursorAfter.X -eq $cursorOrigin.X -and $cursorAfter.Y -eq $cursorOrigin.Y)
} catch { Write-ProbeLog -Message ('cursor restore failed: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
$survivors = @(@($script:Spawned) | Where-Object { $null -ne (Get-Process -Id $_ -ErrorAction SilentlyContinue) })
$teardown = [ordered]@{
probe = $Probe
question = 'does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process'
why = 'KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.'
modifierSweep = [ordered]@{
policy = 'conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.'
stateBeforeSweep = @($modifierBeforeSweep)
keysSwept = @($sweepInjected)
stateAfterSweep = @($modifierAfterSweep)
allClear = (@($modifierAfterSweep | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown }).Count -eq 0)
}
clipboard = [ordered]@{
snapshotTaken = $clipSnapshotTaken
originalFormats = @($clipKinds)
originalTextPresent = $clipHadText
restoredExactly = $clipRestored
verificationMethod = 'the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.'
formatCaveat = 'only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent.'
}
cursor = [ordered]@{
snapshotTaken = ($null -ne $cursorOrigin)
offsetAfterRestore = $(if ($null -ne $cursorAfter -and $null -ne $cursorOrigin) { [string]($cursorAfter.X - $cursorOrigin.X) + ',' + ($cursorAfter.Y - $cursorOrigin.Y) } else { '<not-captured>' })
restoredExactly = $cursorRestored
recordingChoice = 'the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while "0,0" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error.'
}
processes = [ordered]@{
spawnedCount = @($script:Spawned).Count
survivorCount = @($survivors).Count
confirmedGone = (@($survivors).Count -eq 0)
confirmationMethod = 'Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown.'
}
}
try { [void](Write-ProbeJson -Probe $Probe -Name 'teardown.json' -InputObject $teardown) } catch { }
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,439 @@
<#
.SYNOPSIS
Probe 07 (sub-phase 2.0, unit U6): ElementFromPoint against occluded, zero-size and
minimized targets - the primitives 2.6's occlusion gate is built on.
.DESCRIPTION
Stack: managed System.Windows.Automation for ElementFromPoint, Win32 for the
independent cross-checks (WindowFromPoint, GetWindowRect, GetDlgItem). Scope: system.
Three questions, all measured against probe-owned scratch windows:
1. Occlusion. Two scratch windows are launched at deterministic origins so their
rects overlap by construction, the second is raised with ShowWindow(SW_MINIMIZE)
then ShowWindow(SW_RESTORE) on its own window - SetForegroundWindow is never
called - and ElementFromPoint is asked about a point inside the overlap. The
occlusion gate is only sound if the answer is the occluder.
2. Zero size. btnZeroSize is a real 0x0 Win32 control with control id 1007. U7's COM
census recorded it as absent from both RawView and ControlView; this probe
confirms that from the managed side, shows it is still reachable by GetDlgItem,
and asks what ElementFromPoint returns at its origin.
3. Minimize. U3 measured that minimizing degenerates geometry in two different
shapes: the top-level window reports an empty rect while its descendants report
REAL dimensions anchored at -32000, and every node still reports IsOffscreen
false. An occlusion gate that tests emptiness or IsOffscreen alone accepts both
as visible. This probe re-confirms both shapes and adds the consequence the
earlier unit could not measure: what ElementFromPoint returns at the coordinates
the window used to occupy.
Rectangles are recorded as comma-joined strings, not as numeric x/y/width/height
keys, because the KTD9 normalizer buckets those to 8 px - which would round -32000
into a neighbouring bucket and quietly destroy the evidence.
Captures under captures/07-hittest/:
hittest.json occlusion, zero-size and minimized rows
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
Add-Type -AssemblyName UIAutomationClient | Out-Null
Add-Type -AssemblyName UIAutomationTypes | Out-Null
$Probe = '07-hittest'
$AE = [System.Windows.Automation.AutomationElement]
$Walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$Descendants = [System.Windows.Automation.TreeScope]::Descendants
$UnderOrigin = @{ X = 100; Y = 100 }
$OverOrigin = @{ X = 260; Y = 180 }
$ZeroSizeControlId = 1007
$script:Spawned = New-Object System.Collections.ArrayList
function Initialize-HitTestNative {
if ('AgentDesktopProbe.HitTest' -as [type]) { return }
$src = @'
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
[StructLayout(LayoutKind.Sequential)]
public struct HitRect { public int Left; public int Top; public int Right; public int Bottom; }
[StructLayout(LayoutKind.Sequential)]
public struct HitPoint { public int X; public int Y; }
public static class HitTest {
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowRect(IntPtr hWnd, out HitRect lpRect);
[DllImport("user32.dll")]
public static extern IntPtr WindowFromPoint(HitPoint p);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);
[DllImport("user32.dll")]
public static extern IntPtr GetAncestor(IntPtr hWnd, uint flags);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder buffer, int maxCount);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassNameW(IntPtr hWnd, StringBuilder buffer, int maxCount);
public static string TitleOf(IntPtr h) {
if (h == IntPtr.Zero) { return "<null-hwnd>"; }
StringBuilder sb = new StringBuilder(512);
GetWindowTextW(h, sb, sb.Capacity);
return sb.ToString();
}
public static string ClassOf(IntPtr h) {
if (h == IntPtr.Zero) { return "<null-hwnd>"; }
StringBuilder sb = new StringBuilder(256);
GetClassNameW(h, sb, sb.Capacity);
return sb.ToString();
}
public static string RootTitleAt(int x, int y) {
HitPoint p = new HitPoint();
p.X = x;
p.Y = y;
IntPtr h = WindowFromPoint(p);
if (h == IntPtr.Zero) { return "<no-window>"; }
IntPtr root = GetAncestor(h, 2);
return TitleOf(root);
}
public static string RectOf(IntPtr h) {
HitRect r;
if (!GetWindowRect(h, out r)) { return "<getwindowrect-failed>"; }
return r.Left.ToString() + "," + r.Top.ToString() + "," +
(r.Right - r.Left).ToString() + "," + (r.Bottom - r.Top).ToString();
}
}
}
'@
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
}
function Start-Tracked {
param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25)
$p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec
[void]$script:Spawned.Add($p.ProcessId)
return $p
}
function Get-WindowRectString {
param([IntPtr]$Handle)
return [AgentDesktopProbe.HitTest]::RectOf($Handle)
}
function Get-ElementRectString {
param($El)
try {
$r = $El.Current.BoundingRectangle
if ($r.IsEmpty) { return '<empty>' }
return ([string][int]$r.Left + ',' + [int]$r.Top + ',' + [int]$r.Width + ',' + [int]$r.Height)
} catch { return '<read-failed>' }
}
function Get-SafeTopLevelName {
param([AllowEmptyString()][string]$Name)
if ([string]::IsNullOrEmpty($Name)) { return '' }
if ($Name -like 'AgentDesktop Scratch*') { return $Name }
if (@('Program Manager', 'Desktop', 'Windows Input Experience') -contains $Name) { return $Name }
return (Protect-ProbeName -Name $Name)
}
function Get-TopLevelName {
param($El)
$cur = $El
for ($i = 0; $i -lt 40; $i++) {
if ($null -eq $cur) { break }
$parent = $null
try { $parent = $Walker.GetParent($cur) } catch { }
if ($null -eq $parent) { break }
try { if ([System.Windows.Automation.Automation]::Compare($parent, $AE::RootElement)) { return (Get-SafeTopLevelName -Name ([string]$cur.Current.Name)) } } catch { }
$cur = $parent
}
return '<no-top-level-reached>'
}
function Get-ElementAtPoint {
param([int]$X, [int]$Y)
$point = New-Object System.Windows.Point($X, $Y)
$el = $null
try { $el = $AE::FromPoint($point) } catch { }
if ($null -eq $el) {
return [ordered]@{ resolved = $false }
}
$controlType = ''
$automationId = ''
$className = ''
try { $controlType = $el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { }
try { $automationId = [string]$el.Current.AutomationId } catch { }
try { $className = [string]$el.Current.ClassName } catch { }
return [ordered]@{
resolved = $true
controlType = $controlType
automationId = $automationId
className = $className
topLevelWindow = (Get-TopLevelName -El $el)
rect = (Get-ElementRectString -El $el)
}
}
function Test-PointInRect {
param([string]$Rect, [int]$X, [int]$Y)
$parts = $Rect -split ','
if ($parts.Count -ne 4) { return $false }
$l = [int]$parts[0]
$t = [int]$parts[1]
$r = $l + [int]$parts[2]
$b = $t + [int]$parts[3]
return ($X -ge $l -and $X -lt $r -and $Y -ge $t -and $Y -lt $b)
}
function Find-ByAutomationId {
param($Root, [string]$AutomationId, $Scope)
try {
$c = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $AutomationId)
return $Root.FindFirst($Scope, $c)
} catch { return $null }
}
function Measure-Descendants {
param($Root, $TreeWalkerToUse, [int]$Budget = 400)
$n = 0
$stack = New-Object System.Collections.Stack
$stack.Push($Root)
while ($stack.Count -gt 0 -and $n -lt $Budget) {
$el = $stack.Pop()
$n++
try {
$k = $TreeWalkerToUse.GetFirstChild($el)
while ($null -ne $k) { $stack.Push($k); $k = $TreeWalkerToUse.GetNextSibling($k) }
} catch { }
}
return $n
}
$status = 'ok'
$message = ''
$resultData = [ordered]@{}
try {
Initialize-ProbeNative
Initialize-HitTestNative
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
if (-not (Test-Path -LiteralPath $scratchExe)) {
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
}
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
$under = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u6-under', '--pos', ($UnderOrigin.X.ToString() + ',' + $UnderOrigin.Y))
if ($under.MainWindowHandle -eq [IntPtr]::Zero) { throw 'the under window never appeared' }
$over = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u6-over', '--pos', ($OverOrigin.X.ToString() + ',' + $OverOrigin.Y))
if ($over.MainWindowHandle -eq [IntPtr]::Zero) { throw 'the over window never appeared' }
Start-Sleep -Milliseconds 800
$underTitle = [AgentDesktopProbe.HitTest]::TitleOf($under.MainWindowHandle)
$overTitle = [AgentDesktopProbe.HitTest]::TitleOf($over.MainWindowHandle)
[void][AgentDesktopProbe.Native]::ShowWindow($over.MainWindowHandle, 6)
Start-Sleep -Milliseconds 500
[void][AgentDesktopProbe.Native]::ShowWindow($over.MainWindowHandle, 9)
Start-Sleep -Milliseconds 900
$underRect = Get-WindowRectString -Handle $under.MainWindowHandle
$overRect = Get-WindowRectString -Handle $over.MainWindowHandle
$u = $underRect -split ','
$o = $overRect -split ','
$overlapLeft = [Math]::Max([int]$u[0], [int]$o[0])
$overlapTop = [Math]::Max([int]$u[1], [int]$o[1])
$overlapRight = [Math]::Min(([int]$u[0] + [int]$u[2]), ([int]$o[0] + [int]$o[2]))
$overlapBottom = [Math]::Min(([int]$u[1] + [int]$u[3]), ([int]$o[1] + [int]$o[3]))
$probeX = [int](($overlapLeft + $overlapRight) / 2)
$probeY = [int](($overlapTop + $overlapBottom) / 2)
$occludedElement = Get-ElementAtPoint -X $probeX -Y $probeY
$win32RootTitle = Get-SafeTopLevelName -Name ([AgentDesktopProbe.HitTest]::RootTitleAt($probeX, $probeY))
$occlusion = [ordered]@{
question = 'at a point covered by a second window, does ElementFromPoint return the occluder or the covered target'
why = "2.6's occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess."
underWindowTitle = $underTitle
overWindowTitle = $overTitle
underWindowRect = $underRect
overWindowRect = $overRect
raiseMethod = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).'
overlapRect = ([string]$overlapLeft + ',' + $overlapTop + ',' + ($overlapRight - $overlapLeft) + ',' + ($overlapBottom - $overlapTop))
probePoint = ([string]$probeX + ',' + $probeY)
probePointInsideUnder = (Test-PointInRect -Rect $underRect -X $probeX -Y $probeY)
probePointInsideOver = (Test-PointInRect -Rect $overRect -X $probeX -Y $probeY)
elementFromPoint = $occludedElement
win32WindowFromPointRootTitle = $win32RootTitle
resolvedToOccluder = ($occludedElement.topLevelWindow -eq $overTitle)
resolvedToOccluded = ($occludedElement.topLevelWindow -eq $underTitle)
win32AgreesWithUia = ($win32RootTitle -eq $occludedElement.topLevelWindow)
titleEvidenceNote = 'the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.'
verdict = $(if ($occludedElement.topLevelWindow -eq $overTitle) { 'ok - ElementFromPoint returned the occluder' } elseif ($occludedElement.topLevelWindow -eq $underTitle) { 'FAIL - ElementFromPoint returned the covered target' } else { 'unexpected - ElementFromPoint returned neither scratch window' })
}
# --- zero-size element --------------------------------------------------
# the remaining two blocks ask what is at a coordinate INSIDE the under window, so
# the under window is raised first with the same probe-owned minimize/restore. On
# the first run of this probe it was not, and a hit test at btnZeroSize's origin
# resolved to an unrelated console window that happened to sit above it - a foreign
# element in the capture and a run-varying one.
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 6)
Start-Sleep -Milliseconds 500
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 9)
Start-Sleep -Milliseconds 900
$underRoot = $AE::FromHandle($under.MainWindowHandle)
$zeroHandle = [AgentDesktopProbe.HitTest]::GetDlgItem($under.MainWindowHandle, $ZeroSizeControlId)
$zeroRect = '<no-handle>'
$zeroClass = '<no-handle>'
$zeroVisible = $false
if ($zeroHandle -ne [IntPtr]::Zero) {
$zeroRect = Get-WindowRectString -Handle $zeroHandle
$zeroClass = [AgentDesktopProbe.HitTest]::ClassOf($zeroHandle)
$zeroVisible = [AgentDesktopProbe.HitTest]::IsWindowVisible($zeroHandle)
}
$zeroInControlView = Find-ByAutomationId -Root $underRoot -AutomationId 'btnZeroSize' -Scope $Descendants
$zeroFromHandle = $null
if ($zeroHandle -ne [IntPtr]::Zero) {
try { $zeroFromHandle = $AE::FromHandle($zeroHandle) } catch { }
}
$zeroOrigin = $zeroRect -split ','
$zeroPointElement = [ordered]@{ resolved = $false }
if ($zeroOrigin.Count -eq 4) {
$zeroPointElement = Get-ElementAtPoint -X ([int]$zeroOrigin[0]) -Y ([int]$zeroOrigin[1])
}
$zeroSize = [ordered]@{
question = 'what does a 0x0 control look like to a hit test, and is it reachable at all'
controlId = $ZeroSizeControlId
automationId = 'btnZeroSize'
reachableByGetDlgItem = ($zeroHandle -ne [IntPtr]::Zero)
win32Rect = $zeroRect
win32ClassName = $zeroClass
win32IsWindowVisible = $zeroVisible
foundByFindFirstAutomationId = ($null -ne $zeroInControlView)
controlViewNodeCount = (Measure-Descendants -Root $underRoot -TreeWalkerToUse $Walker)
rawViewNodeCount = (Measure-Descendants -Root $underRoot -TreeWalkerToUse $RawWalker)
fromHandleResolved = ($null -ne $zeroFromHandle)
fromHandleControlType = $(if ($null -ne $zeroFromHandle) { ($zeroFromHandle.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '') } else { '<not-resolved>' })
fromHandleRect = $(if ($null -ne $zeroFromHandle) { (Get-ElementRectString -El $zeroFromHandle) } else { '<not-resolved>' })
fromHandleIsOffscreen = $(if ($null -ne $zeroFromHandle) { [bool]$zeroFromHandle.Current.IsOffscreen } else { $false })
elementFromPointAtItsOrigin = $zeroPointElement
priorEvidence = 'U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.'
consequence = 'a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats "not hit-testable" as "does not exist" will be right about the click and wrong about the element.'
}
# --- minimized window ---------------------------------------------------
$underOnlyX = [int]$u[0] + 30
$underOnlyY = [int]$u[1] + [int]([int]$u[3] / 2)
$visiblePointBefore = Get-ElementAtPoint -X $underOnlyX -Y $underOnlyY
$descendantBefore = Find-ByAutomationId -Root $underRoot -AutomationId 'btnAction' -Scope $Descendants
$topRectBefore = Get-ElementRectString -El $underRoot
$topOffscreenBefore = [bool]$underRoot.Current.IsOffscreen
$descRectBefore = '<not-found>'
$descOffscreenBefore = $false
if ($null -ne $descendantBefore) {
$descRectBefore = Get-ElementRectString -El $descendantBefore
$descOffscreenBefore = [bool]$descendantBefore.Current.IsOffscreen
}
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 6)
Start-Sleep -Milliseconds 1200
$underRootAfter = $AE::FromHandle($under.MainWindowHandle)
$descendantAfter = Find-ByAutomationId -Root $underRootAfter -AutomationId 'btnAction' -Scope $Descendants
$topRectAfter = Get-ElementRectString -El $underRootAfter
$topOffscreenAfter = [bool]$underRootAfter.Current.IsOffscreen
$descRectAfter = '<not-found>'
$descOffscreenAfter = $false
if ($null -ne $descendantAfter) {
$descRectAfter = Get-ElementRectString -El $descendantAfter
$descOffscreenAfter = [bool]$descendantAfter.Current.IsOffscreen
}
$visiblePointAfter = Get-ElementAtPoint -X $underOnlyX -Y $underOnlyY
$minimizedDescendantPoint = [ordered]@{ resolved = $false }
$descParts = $descRectAfter -split ','
if ($descParts.Count -eq 4 -and [int]$descParts[0] -gt -32768) {
$minimizedDescendantPoint = Get-ElementAtPoint -X ([int]$descParts[0] + 2) -Y ([int]$descParts[1] + 2)
}
$win32IsIconic = [AgentDesktopProbe.HitTest]::IsIconic($under.MainWindowHandle)
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 9)
Start-Sleep -Milliseconds 900
$topRectRestored = Get-ElementRectString -El ($AE::FromHandle($under.MainWindowHandle))
$minimized = [ordered]@{
question = 'what does a minimized window report, and what does a hit test at its former coordinates return'
priorEvidence = 'U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.'
pointProbed = ([string]$underOnlyX + ',' + $underOnlyY)
pointChoice = 'a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself'
topLevelRectBefore = $topRectBefore
topLevelRectAfterMinimize = $topRectAfter
topLevelRectAfterRestore = $topRectRestored
topLevelIsOffscreenBefore = $topOffscreenBefore
topLevelIsOffscreenAfterMinimize = $topOffscreenAfter
descendantAutomationId = 'btnAction'
descendantFoundAfterMinimize = ($null -ne $descendantAfter)
descendantRectBefore = $descRectBefore
descendantRectAfterMinimize = $descRectAfter
descendantIsOffscreenBefore = $descOffscreenBefore
descendantIsOffscreenAfterMinimize = $descOffscreenAfter
win32IsIconic = $win32IsIconic
elementFromPointBefore = $visiblePointBefore
elementFromPointAfterMinimize = $visiblePointAfter
elementFromPointAtDescendantAnchor = $minimizedDescendantPoint
hitTestFreedTheCoordinates = ($visiblePointBefore.topLevelWindow -ne $visiblePointAfter.topLevelWindow)
gateConsequence = 'ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.'
rectRecordingNote = 'every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record.'
}
[void](Write-ProbeJson -Probe $Probe -Name 'hittest.json' -InputObject ([ordered]@{
probe = $Probe
question = 'do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets'
stack = 'managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint'
scope = 'system'
safety = 'both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.'
occlusion = $occlusion
zeroSize = $zeroSize
minimized = $minimized
}))
$resultData['occlusionVerdict'] = $occlusion.verdict
$resultData['zeroSizeInTree'] = $zeroSize.foundByFindFirstAutomationId
$resultData['zeroSizeReachableByHandle'] = $zeroSize.reachableByGetDlgItem
$resultData['minimizedTopRect'] = $minimized.topLevelRectAfterMinimize
$resultData['minimizedDescendantRect'] = $minimized.descendantRectAfterMinimize
$resultData['minimizedDescendantIsOffscreen'] = $minimized.descendantIsOffscreenAfterMinimize
$message = 'hittest: ' + $occlusion.verdict + '; zero-size in tree=' + $zeroSize.foundByFindFirstAutomationId + '; minimized descendant rect=' + $minimized.descendantRectAfterMinimize
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,23 @@
{
"probe": "05-interactions",
"question": "does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window",
"why": "2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.",
"method": "a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.",
"foregroundBefore": {
"windowTitle": "AgentDesktop Scratch WinForms [u5-default]",
"processId": 4052
},
"foregroundAfter": {
"windowTitle": "AgentDesktop Scratch WPF [u5]",
"processId": 9180
},
"foregroundChanged": true,
"foregroundWasTheOtherScratchWindow": true,
"setFocusTarget": "wpf txtValue",
"setFocusError": "",
"hadKeyboardFocusBefore": false,
"hasKeyboardFocusAfter": true,
"focusedElementAutomationIdAfter": "txtValue",
"titleEvidenceNote": "the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to \u003cpid\u003e. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer\u0027s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.",
"verdict": "SetFocus moved the desktop foreground"
}

View file

@ -0,0 +1,23 @@
{
"probe": "05-interactions",
"question": "does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window",
"why": "2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.",
"method": "a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.",
"foregroundBefore": {
"windowTitle": "AgentDesktop Scratch WinForms [u5-default]",
"processId": <pid>
},
"foregroundAfter": {
"windowTitle": "AgentDesktop Scratch WPF [u5]",
"processId": <pid>
},
"foregroundChanged": true,
"foregroundWasTheOtherScratchWindow": true,
"setFocusTarget": "wpf txtValue",
"setFocusError": "",
"hadKeyboardFocusBefore": false,
"hasKeyboardFocusAfter": true,
"focusedElementAutomationIdAfter": "txtValue",
"titleEvidenceNote": "the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to \u003cpid\u003e. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer\u0027s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.",
"verdict": "SetFocus moved the desktop foreground"
}

View file

@ -0,0 +1,394 @@
{
"probe": "05-interactions",
"question": "does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return",
"stack": "managed-System.Windows.Automation",
"scope": "app/provider",
"verificationDiscipline": "no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.",
"fixtureArtifactNote": "winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms\u0027 own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.",
"managedVsComNote": "several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.",
"wheelCounterpartNote": "the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.",
"rows": [
{
"interaction": "invoke",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Button",
"patternAcquired": true,
"supportedPatterns": [
"Invoke",
"SynchronizedInput"
],
"sinkElement": "lblStatus (a different element than the one invoked)",
"sinkBefore": "status:ready",
"sinkAfter": "action:1",
"changed": true,
"verdict": "ok"
},
{
"interaction": "invoke",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "invoke",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "toggle",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "CheckBox",
"patternAcquired": true,
"preState": "Off",
"postState": "On",
"reReadMethod": "the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned",
"sinkBefore": "action:1",
"sinkAfter": "action:1",
"sinkChanged": false,
"sinkNote": "the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.",
"changed": true,
"verdict": "ok"
},
{
"interaction": "toggle",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "toggle",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "set-value/ascii",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "ascii",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 10,
"codepoints": 10,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "730365c1825e69a9171fd18ccdefab7962df67a8fcb7b64e4e3c8dd8bdf8ec6b"
},
"expectedShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"observedShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/cjk",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "cjk",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"expectedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"observedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/astral-plane",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "astral-plane",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"expectedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"observedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/mixed",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "mixed",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"expectedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"observedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ValuePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "expand-collapse",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "ComboBox",
"patternAcquired": true,
"preState": "Collapsed",
"stateAfterExpand": "Expanded",
"stateAfterCollapse": "Collapsed",
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
"roundTripped": true,
"verdict": "ok"
},
{
"interaction": "expand-collapse",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "ComboBox",
"patternAcquired": true,
"preState": "Collapsed",
"stateAfterExpand": "Expanded",
"stateAfterCollapse": "Collapsed",
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
"roundTripped": true,
"verdict": "ok"
},
{
"interaction": "expand-collapse",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "select",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "ListItem",
"patternAcquired": true,
"selectionCountBefore": 0,
"selectionCountAfter": 1,
"isSelectedAfter": true,
"sinkBefore": "action:1",
"sinkAfter": "list-sel:Item-Charlie",
"reReadMethod": "container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink",
"verdict": "ok"
},
{
"interaction": "select",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "\u003celement-not-found\u003e",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
},
{
"interaction": "select",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "\u003celement-not-found\u003e",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
},
{
"interaction": "scroll-via-pattern",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "pnlScroll",
"pattern": "Scroll",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
},
{
"interaction": "scroll-via-pattern",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "pnlScroll",
"pattern": "Scroll",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
},
{
"interaction": "scroll-via-pattern",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems",
"pattern": "Scroll",
"controlType": "List",
"patternAcquired": true,
"makeScrollableMethod": "the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded",
"shrinkStepUsed": 240,
"verticallyScrollable": true,
"verticalPercentBefore": 0,
"verticalPercentAfter": 80,
"reReadMethod": "percent re-read from a freshly acquired ScrollPattern on a freshly found element",
"verdict": "ok"
}
]
}

View file

@ -0,0 +1,394 @@
{
"probe": "05-interactions",
"question": "does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return",
"stack": "managed-System.Windows.Automation",
"scope": "app/provider",
"verificationDiscipline": "no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.",
"fixtureArtifactNote": "winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms\u0027 own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.",
"managedVsComNote": "several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.",
"wheelCounterpartNote": "the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.",
"rows": [
{
"interaction": "invoke",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Button",
"patternAcquired": true,
"supportedPatterns": [
"Invoke",
"SynchronizedInput"
],
"sinkElement": "lblStatus (a different element than the one invoked)",
"sinkBefore": "status:ready",
"sinkAfter": "action:1",
"changed": true,
"verdict": "ok"
},
{
"interaction": "invoke",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "invoke",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "btnAction",
"pattern": "Invoke",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "toggle",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "CheckBox",
"patternAcquired": true,
"preState": "Off",
"postState": "On",
"reReadMethod": "the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned",
"sinkBefore": "action:1",
"sinkAfter": "action:1",
"sinkChanged": false,
"sinkNote": "the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.",
"changed": true,
"verdict": "ok"
},
{
"interaction": "toggle",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "toggle",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "chkToggle",
"pattern": "Toggle",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "set-value/ascii",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "ascii",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 10,
"codepoints": 10,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "730365c1825e69a9171fd18ccdefab7962df67a8fcb7b64e4e3c8dd8bdf8ec6b"
},
"expectedShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"observedShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/cjk",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "cjk",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 20,
"codepoints": 20,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
},
"expectedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"observedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/astral-plane",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "astral-plane",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"expectedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"observedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value/mixed",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Edit",
"patternAcquired": true,
"payloadKind": "mixed",
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
"preValueShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"expectedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"observedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"exactRoundTrip": true,
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
"verdict": "ok"
},
{
"interaction": "set-value",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "txtValue",
"pattern": "Value",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ValuePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "expand-collapse",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "ComboBox",
"patternAcquired": true,
"preState": "Collapsed",
"stateAfterExpand": "Expanded",
"stateAfterCollapse": "Collapsed",
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
"roundTripped": true,
"verdict": "ok"
},
{
"interaction": "expand-collapse",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "ComboBox",
"patternAcquired": true,
"preState": "Collapsed",
"stateAfterExpand": "Expanded",
"stateAfterCollapse": "Collapsed",
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
"roundTripped": true,
"verdict": "ok"
},
{
"interaction": "expand-collapse",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "cboChoice",
"pattern": "ExpandCollapse",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row"
},
{
"interaction": "select",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "ListItem",
"patternAcquired": true,
"selectionCountBefore": 0,
"selectionCountAfter": 1,
"isSelectedAfter": true,
"sinkBefore": "action:1",
"sinkAfter": "list-sel:Item-Charlie",
"reReadMethod": "container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink",
"verdict": "ok"
},
{
"interaction": "select",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "\u003celement-not-found\u003e",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
},
{
"interaction": "select",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems/item[2]",
"pattern": "SelectionItem",
"controlType": "\u003celement-not-found\u003e",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
},
{
"interaction": "scroll-via-pattern",
"target": "winforms-host-providers",
"stack": "managed-System.Windows.Automation",
"automationId": "pnlScroll",
"pattern": "Scroll",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
},
{
"interaction": "scroll-via-pattern",
"target": "winforms-default",
"stack": "managed-System.Windows.Automation",
"automationId": "pnlScroll",
"pattern": "Scroll",
"controlType": "Pane",
"patternAcquired": false,
"supportedPatterns": [
],
"verdict": "pattern-unavailable",
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
},
{
"interaction": "scroll-via-pattern",
"target": "wpf",
"stack": "managed-System.Windows.Automation",
"automationId": "lstItems",
"pattern": "Scroll",
"controlType": "List",
"patternAcquired": true,
"makeScrollableMethod": "the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded",
"shrinkStepUsed": 240,
"verticallyScrollable": true,
"verticalPercentBefore": 0,
"verticalPercentAfter": 80,
"reReadMethod": "percent re-read from a freshly acquired ScrollPattern on a freshly found element",
"verdict": "ok"
}
]
}

View file

@ -0,0 +1,84 @@
{
"probe": "05-interactions",
"question": "is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it",
"stack": "managed-System.Windows.Automation",
"scope": "app/provider",
"whyTwoLookups": "GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.",
"comCounterpart": "captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.",
"p2o12Relevance": "2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.",
"rows": [
{
"target": "wpf-txtValue",
"stack": "managed-System.Windows.Automation",
"controlType": "Edit",
"className": "TextBox",
"supportedPatterns": [
"Scroll",
"SynchronizedInput",
"Text",
"Value"
],
"textPatternExposed": true,
"getText": {
"expectedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"observedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"exactMatch": true,
"note": "the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument"
},
"selection": {
"rangesBefore": 1,
"rangesAfter": 1,
"requestedUnits": 5,
"selectedShape": {
"utf16Units": 5,
"codepoints": 5,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "9d17cf8d7d1b14ceec751b8f91702aaea1d386a4ed188d5e279cbe02394b18ca"
},
"matchesSubstring": true
},
"caret": {
"method": "a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read",
"rangesAfter": 1,
"degenerate": true
},
"insert": {
"method": "TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above",
"verified": true
}
},
{
"target": "notepad-edit",
"stack": "managed-System.Windows.Automation",
"lookup": "FindFirst(Descendants, ControlType=Document)",
"resolved": false
},
{
"target": "notepad-edit",
"stack": "managed-System.Windows.Automation",
"lookup": "AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)",
"resolved": true,
"controlType": "Pane",
"className": "Edit",
"supportedPatterns": [
],
"tryGetCurrentPattern_Text": false,
"tryGetCurrentPattern_Value": false,
"tryGetCurrentPattern_Scroll": false
}
]
}

View file

@ -0,0 +1,84 @@
{
"probe": "05-interactions",
"question": "is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it",
"stack": "managed-System.Windows.Automation",
"scope": "app/provider",
"whyTwoLookups": "GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.",
"comCounterpart": "captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.",
"p2o12Relevance": "2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.",
"rows": [
{
"target": "wpf-txtValue",
"stack": "managed-System.Windows.Automation",
"controlType": "Edit",
"className": "TextBox",
"supportedPatterns": [
"Scroll",
"SynchronizedInput",
"Text",
"Value"
],
"textPatternExposed": true,
"getText": {
"expectedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"observedShape": {
"utf16Units": 29,
"codepoints": 28,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
},
"exactMatch": true,
"note": "the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument"
},
"selection": {
"rangesBefore": 1,
"rangesAfter": 1,
"requestedUnits": 5,
"selectedShape": {
"utf16Units": 5,
"codepoints": 5,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "9d17cf8d7d1b14ceec751b8f91702aaea1d386a4ed188d5e279cbe02394b18ca"
},
"matchesSubstring": true
},
"caret": {
"method": "a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read",
"rangesAfter": 1,
"degenerate": true
},
"insert": {
"method": "TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above",
"verified": true
}
},
{
"target": "notepad-edit",
"stack": "managed-System.Windows.Automation",
"lookup": "FindFirst(Descendants, ControlType=Document)",
"resolved": false
},
{
"target": "notepad-edit",
"stack": "managed-System.Windows.Automation",
"lookup": "AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)",
"resolved": true,
"controlType": "Pane",
"className": "Edit",
"supportedPatterns": [
],
"tryGetCurrentPattern_Text": false,
"tryGetCurrentPattern_Value": false,
"tryGetCurrentPattern_Scroll": false
}
]
}

View file

@ -0,0 +1,197 @@
{
"probe": "06-input-synthesis",
"question": "does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register",
"stack": "win32-SendInput",
"scope": "system",
"why": "2.8\u0027s type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement.",
"foregroundAcquisitionMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.",
"foregroundAcquired": true,
"injectionGate": "every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference",
"interference": null,
"typingRows": [
{
"payloadKind": "ascii",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 14,
"expectedShape": {
"utf16Units": 14,
"codepoints": 14,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
},
"observedShape": {
"utf16Units": 14,
"codepoints": 14,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "cjk",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 3,
"expectedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"observedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "astral-plane",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 4,
"expectedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"observedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "mixed",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 5,
"expectedShape": {
"utf16Units": 5,
"codepoints": 4,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
},
"observedShape": {
"utf16Units": 5,
"codepoints": 4,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
}
],
"chord": {
"chordSent": true,
"keys": "Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated",
"sourceTypedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"sourceObservedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"sourceRoundTrip": true,
"clipboardReadOk": true,
"clipboardObservedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"verifiedByShape": true,
"verificationRule": "the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.",
"verdict": "ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard"
},
"modifierStateAfterChord": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"modifierStateAfterChordVerdict": "no modifier left down by the chord"
}

View file

@ -0,0 +1,197 @@
{
"probe": "06-input-synthesis",
"question": "does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register",
"stack": "win32-SendInput",
"scope": "system",
"why": "2.8\u0027s type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement.",
"foregroundAcquisitionMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.",
"foregroundAcquired": true,
"injectionGate": "every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference",
"interference": null,
"typingRows": [
{
"payloadKind": "ascii",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 14,
"expectedShape": {
"utf16Units": 14,
"codepoints": 14,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
},
"observedShape": {
"utf16Units": 14,
"codepoints": 14,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "cjk",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 3,
"expectedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"observedShape": {
"utf16Units": 3,
"codepoints": 3,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "astral-plane",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 4,
"expectedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"observedShape": {
"utf16Units": 4,
"codepoints": 3,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
},
{
"payloadKind": "mixed",
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
"clearedBeforeTyping": true,
"utf16UnitsSent": 5,
"expectedShape": {
"utf16Units": 5,
"codepoints": 4,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
},
"observedShape": {
"utf16Units": 5,
"codepoints": 4,
"surrogatePairs": 1,
"replacementChars": 0,
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
},
"exactRoundTrip": true,
"surrogatePairSurvived": true,
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
"verdict": "ok"
}
],
"chord": {
"chordSent": true,
"keys": "Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated",
"sourceTypedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"sourceObservedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"sourceRoundTrip": true,
"clipboardReadOk": true,
"clipboardObservedShape": {
"utf16Units": 16,
"codepoints": 16,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
},
"verifiedByShape": true,
"verificationRule": "the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.",
"verdict": "ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard"
},
"modifierStateAfterChord": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"modifierStateAfterChordVerdict": "no modifier left down by the chord"
}

View file

@ -0,0 +1,70 @@
{
"probe": "06-input-synthesis",
"question": "do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target",
"stack": "win32-SendInput",
"scope": "system",
"coordinateModel": "MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.",
"interference": null,
"click": {
"targetControl": "btnAction (control id 1005)",
"targetRect": "340,235,104,26",
"requestedPoint": "392,248",
"cursorLandedPoint": "392,248",
"absoluteMoveExact": true,
"absoluteMoveNote": "the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.",
"statusBefore": "status:ready",
"statusAfter": "action:1",
"registered": true,
"reReadMethod": "WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked"
},
"wheel": {
"targetControl": "pnlScroll (control id 1011)",
"targetRect": "124,363,340,180",
"ticks": 3,
"deltaPerTick": -120,
"sinkBefore": "scroll:0",
"sinkAfter": "scroll:140",
"scrollPixelsBefore": 0,
"scrollPixelsAfter": 140,
"deltaScrollPixels": 140,
"registered": true,
"patternCounterpart": "the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.",
"measuredFactNaming": "the observed delta is named deltaScrollPixels, not \"y\" or \"height\", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away."
},
"drag": {
"targetControl": "tbSlider (control id 1008), Minimum 0 Maximum 100",
"targetRect": "124,307,320,45",
"method": "mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step",
"sliderValueBefore": 0,
"sliderValueAfter": 90,
"samples": [
{
"step": 1,
"sliderValue": 15
},
{
"step": 2,
"sliderValue": 30
},
{
"step": 3,
"sliderValue": 45
},
{
"step": 4,
"sliderValue": 60
},
{
"step": 5,
"sliderValue": 75
},
{
"step": 6,
"sliderValue": 90
}
],
"monotonicNonDecreasing": true,
"increased": true,
"verdict": "ok - monotonic increase under drag"
}
}

View file

@ -0,0 +1,70 @@
{
"probe": "06-input-synthesis",
"question": "do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target",
"stack": "win32-SendInput",
"scope": "system",
"coordinateModel": "MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.",
"interference": null,
"click": {
"targetControl": "btnAction (control id 1005)",
"targetRect": "340,235,104,26",
"requestedPoint": "392,248",
"cursorLandedPoint": "392,248",
"absoluteMoveExact": true,
"absoluteMoveNote": "the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.",
"statusBefore": "status:ready",
"statusAfter": "action:1",
"registered": true,
"reReadMethod": "WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked"
},
"wheel": {
"targetControl": "pnlScroll (control id 1011)",
"targetRect": "124,363,340,180",
"ticks": <duration>,
"deltaPerTick": -120,
"sinkBefore": "scroll:0",
"sinkAfter": "scroll:140",
"scrollPixelsBefore": 0,
"scrollPixelsAfter": 140,
"deltaScrollPixels": 140,
"registered": true,
"patternCounterpart": "the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.",
"measuredFactNaming": "the observed delta is named deltaScrollPixels, not \"y\" or \"height\", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away."
},
"drag": {
"targetControl": "tbSlider (control id 1008), Minimum 0 Maximum 100",
"targetRect": "124,307,320,45",
"method": "mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step",
"sliderValueBefore": 0,
"sliderValueAfter": 90,
"samples": [
{
"step": 1,
"sliderValue": 15
},
{
"step": 2,
"sliderValue": 30
},
{
"step": 3,
"sliderValue": 45
},
{
"step": 4,
"sliderValue": 60
},
{
"step": 5,
"sliderValue": 75
},
{
"step": 6,
"sliderValue": 90
}
],
"monotonicNonDecreasing": true,
"increased": true,
"verdict": "ok - monotonic increase under drag"
}
}

View file

@ -0,0 +1,77 @@
{
"probe": "06-input-synthesis",
"question": "does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium",
"stack": "win32-PostMessage",
"scope": "system",
"why": "Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.",
"foregroundIrrelevantNote": "PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.",
"rows": [
{
"target": "scratch WinForms Edit (control id 1003), same session, same integrity level as the probe",
"stack": "win32-PostMessage",
"integrityRelation": "High -\u003e High. 09-elevation-uipi measured the Medium -\u003e High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?",
"keyDownPostResult": "true",
"keyUpPostResult": "true",
"charPostResult": "true",
"resultFormat": "\"true\" on success, \"false:\u003cGetLastError\u003e\" on failure. The error code is only meaningful when the call failed, so it is only recorded then.",
"textBeforeShape": {
"utf16Units": 0,
"codepoints": 0,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"textAfterKeyDownShape": {
"utf16Units": 1,
"codepoints": 1,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
},
"textAfterKeyUpShape": {
"utf16Units": 1,
"codepoints": 1,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
},
"textAfterCharShape": {
"utf16Units": 2,
"codepoints": 2,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "219173f2f738427068d89f63e4ebe7b9d440ec37c08234356c276d6eafc77eca"
},
"keyDownRegistered": true,
"keyUpAddedAnything": false,
"charMessageRegistered": true,
"measuredResult": "the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.",
"interpretation": "this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread\u0027s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread\u0027s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.",
"invariantRelevance": "Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism."
},
{
"target": "chromium/electron (Obsidian, launched by this probe)",
"stack": "win32-PostMessage",
"obsidianVersion": "1.12.7",
"topLevelClassName": "Chrome_WidgetWin_1",
"childWindowClasses": "Chrome_RenderWidgetHostHWND",
"renderWidgetChildFound": true,
"topLevelKeyDownPostResult": "true",
"topLevelKeyUpPostResult": "true",
"renderWidgetKeyDownPostResult": "true",
"renderWidgetKeyUpPostResult": "true",
"renderWidgetCharPostResult": "true",
"observable": "a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.",
"controlPass": "the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.",
"timingStabilizationPasses": 2,
"nodesAtSettle": 15,
"nodesAfterIdleWait": 15,
"nodesAfterPostedKeys": 15,
"treeChurnWithoutInput": false,
"treeChangedAfterPostedKeys": false,
"keystrokeRegistered": false,
"verdict": "not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk",
"caveat": "an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such."
}
]
}

View file

@ -0,0 +1,77 @@
{
"probe": "06-input-synthesis",
"question": "does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium",
"stack": "win32-PostMessage",
"scope": "system",
"why": "Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.",
"foregroundIrrelevantNote": "PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.",
"rows": [
{
"target": "scratch WinForms Edit (control id 1003), same session, same integrity level as the probe",
"stack": "win32-PostMessage",
"integrityRelation": "High -\u003e High. 09-elevation-uipi measured the Medium -\u003e High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?",
"keyDownPostResult": "true",
"keyUpPostResult": "true",
"charPostResult": "true",
"resultFormat": "\"true\" on success, \"false:\u003cGetLastError\u003e\" on failure. The error code is only meaningful when the call failed, so it is only recorded then.",
"textBeforeShape": {
"utf16Units": 0,
"codepoints": 0,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"textAfterKeyDownShape": {
"utf16Units": 1,
"codepoints": 1,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
},
"textAfterKeyUpShape": {
"utf16Units": 1,
"codepoints": 1,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
},
"textAfterCharShape": {
"utf16Units": 2,
"codepoints": 2,
"surrogatePairs": 0,
"replacementChars": 0,
"sha256Utf16": "219173f2f738427068d89f63e4ebe7b9d440ec37c08234356c276d6eafc77eca"
},
"keyDownRegistered": true,
"keyUpAddedAnything": false,
"charMessageRegistered": true,
"measuredResult": "the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.",
"interpretation": "this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread\u0027s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread\u0027s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.",
"invariantRelevance": "Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism."
},
{
"target": "chromium/electron (Obsidian, launched by this probe)",
"stack": "win32-PostMessage",
"obsidianVersion": "1.12.7",
"topLevelClassName": "Chrome_WidgetWin_1",
"childWindowClasses": "Chrome_RenderWidgetHostHWND",
"renderWidgetChildFound": true,
"topLevelKeyDownPostResult": "true",
"topLevelKeyUpPostResult": "true",
"renderWidgetKeyDownPostResult": "true",
"renderWidgetKeyUpPostResult": "true",
"renderWidgetCharPostResult": "true",
"observable": "a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.",
"controlPass": "the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.",
"timingStabilizationPasses": <duration>,
"nodesAtSettle": 15,
"nodesAfterIdleWait": 15,
"nodesAfterPostedKeys": 15,
"treeChurnWithoutInput": false,
"treeChangedAfterPostedKeys": false,
"keystrokeRegistered": false,
"verdict": "not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk",
"caveat": "an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such."
}
]
}

View file

@ -0,0 +1,148 @@
{
"probe": "06-input-synthesis",
"question": "does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process",
"why": "KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.",
"modifierSweep": {
"policy": "conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.",
"stateBeforeSweep": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"keysSwept": [
],
"stateAfterSweep": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"allClear": true
},
"clipboard": {
"snapshotTaken": true,
"originalFormats": [
"text"
],
"originalTextPresent": true,
"restoredExactly": true,
"verificationMethod": "the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.",
"formatCaveat": "only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent."
},
"cursor": {
"snapshotTaken": true,
"offsetAfterRestore": "0,0",
"restoredExactly": true,
"recordingChoice": "the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while \"0,0\" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error."
},
"processes": {
"spawnedCount": 5,
"survivorCount": 0,
"confirmedGone": true,
"confirmationMethod": "Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown."
}
}

View file

@ -0,0 +1,148 @@
{
"probe": "06-input-synthesis",
"question": "does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process",
"why": "KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.",
"modifierSweep": {
"policy": "conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.",
"stateBeforeSweep": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"keysSwept": [
],
"stateAfterSweep": [
{
"key": "VK_SHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_CONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_MENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RWIN",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RSHIFT",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RCONTROL",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_LMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
},
{
"key": "VK_RMENU",
"asyncKeyIsDown": false,
"keyStateIsDown": false
}
],
"allClear": true
},
"clipboard": {
"snapshotTaken": true,
"originalFormats": [
"text"
],
"originalTextPresent": true,
"restoredExactly": true,
"verificationMethod": "the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.",
"formatCaveat": "only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent."
},
"cursor": {
"snapshotTaken": true,
"offsetAfterRestore": "0,0",
"restoredExactly": true,
"recordingChoice": "the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while \"0,0\" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error."
},
"processes": {
"spawnedCount": 5,
"survivorCount": 0,
"confirmedGone": true,
"confirmationMethod": "Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown."
}
}

View file

@ -0,0 +1,105 @@
{
"probe": "07-hittest",
"question": "do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets",
"stack": "managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint",
"scope": "system",
"safety": "both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.",
"occlusion": {
"question": "at a point covered by a second window, does ElementFromPoint return the occluder or the covered target",
"why": "2.6\u0027s occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess.",
"underWindowTitle": "AgentDesktop Scratch WinForms [u6-under]",
"overWindowTitle": "AgentDesktop Scratch WinForms [u6-over]",
"underWindowRect": "100,100,776,559",
"overWindowRect": "260,180,776,559",
"raiseMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).",
"overlapRect": "260,180,616,479",
"probePoint": "568,420",
"probePointInsideUnder": true,
"probePointInsideOver": true,
"elementFromPoint": {
"resolved": true,
"controlType": "Pane",
"automationId": "tbSlider",
"className": "WindowsForms10.msctls_trackbar32.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-over]",
"rect": "284,387,320,45"
},
"win32WindowFromPointRootTitle": "AgentDesktop Scratch WinForms [u6-over]",
"resolvedToOccluder": true,
"resolvedToOccluded": false,
"win32AgreesWithUia": true,
"titleEvidenceNote": "the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.",
"verdict": "ok - ElementFromPoint returned the occluder"
},
"zeroSize": {
"question": "what does a 0x0 control look like to a hit test, and is it reachable at all",
"controlId": 1007,
"automationId": "btnZeroSize",
"reachableByGetDlgItem": true,
"win32Rect": "248,271,0,0",
"win32ClassName": "WindowsForms10.BUTTON.app.0.34f5582_r8_ad1",
"win32IsWindowVisible": true,
"foundByFindFirstAutomationId": false,
"controlViewNodeCount": 24,
"rawViewNodeCount": 24,
"fromHandleResolved": true,
"fromHandleControlType": "Pane",
"fromHandleRect": "\u003cempty\u003e",
"fromHandleIsOffscreen": false,
"elementFromPointAtItsOrigin": {
"resolved": true,
"controlType": "Window",
"automationId": "frmScratchMain",
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
"rect": "100,100,776,559"
},
"priorEvidence": "U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.",
"consequence": "a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats \"not hit-testable\" as \"does not exist\" will be right about the click and wrong about the element."
},
"minimized": {
"question": "what does a minimized window report, and what does a hit test at its former coordinates return",
"priorEvidence": "U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.",
"pointProbed": "130,380",
"pointChoice": "a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself",
"topLevelRectBefore": "100,100,776,559",
"topLevelRectAfterMinimize": "\u003cempty\u003e",
"topLevelRectAfterRestore": "100,100,776,559",
"topLevelIsOffscreenBefore": false,
"topLevelIsOffscreenAfterMinimize": false,
"descendantAutomationId": "btnAction",
"descendantFoundAfterMinimize": true,
"descendantRectBefore": "340,235,104,26",
"descendantRectAfterMinimize": "-31768,-31896,104,26",
"descendantIsOffscreenBefore": false,
"descendantIsOffscreenAfterMinimize": false,
"win32IsIconic": true,
"elementFromPointBefore": {
"resolved": true,
"controlType": "Pane",
"automationId": "pnlScroll",
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
"rect": "124,363,340,180"
},
"elementFromPointAfterMinimize": {
"resolved": true,
"controlType": "Pane",
"automationId": "1",
"className": "SysListView32",
"topLevelWindow": "Program Manager",
"rect": "0,0,1639,732"
},
"elementFromPointAtDescendantAnchor": {
"resolved": true,
"controlType": "Pane",
"automationId": "",
"className": "#32769",
"topLevelWindow": "\u003cno-top-level-reached\u003e",
"rect": "0,0,1639,732"
},
"hitTestFreedTheCoordinates": true,
"gateConsequence": "ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.",
"rectRecordingNote": "every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record."
}
}

View file

@ -0,0 +1,105 @@
{
"probe": "07-hittest",
"question": "do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets",
"stack": "managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint",
"scope": "system",
"safety": "both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.",
"occlusion": {
"question": "at a point covered by a second window, does ElementFromPoint return the occluder or the covered target",
"why": "2.6\u0027s occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess.",
"underWindowTitle": "AgentDesktop Scratch WinForms [u6-under]",
"overWindowTitle": "AgentDesktop Scratch WinForms [u6-over]",
"underWindowRect": "100,100,776,559",
"overWindowRect": "260,180,776,559",
"raiseMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).",
"overlapRect": "260,180,616,479",
"probePoint": "568,420",
"probePointInsideUnder": true,
"probePointInsideOver": true,
"elementFromPoint": {
"resolved": true,
"controlType": "Pane",
"automationId": "tbSlider",
"className": "WindowsForms10.msctls_trackbar32.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-over]",
"rect": "284,387,320,45"
},
"win32WindowFromPointRootTitle": "AgentDesktop Scratch WinForms [u6-over]",
"resolvedToOccluder": true,
"resolvedToOccluded": false,
"win32AgreesWithUia": true,
"titleEvidenceNote": "the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.",
"verdict": "ok - ElementFromPoint returned the occluder"
},
"zeroSize": {
"question": "what does a 0x0 control look like to a hit test, and is it reachable at all",
"controlId": 1007,
"automationId": "btnZeroSize",
"reachableByGetDlgItem": true,
"win32Rect": "248,271,0,0",
"win32ClassName": "WindowsForms10.BUTTON.app.0.34f5582_r8_ad1",
"win32IsWindowVisible": true,
"foundByFindFirstAutomationId": false,
"controlViewNodeCount": 24,
"rawViewNodeCount": 24,
"fromHandleResolved": true,
"fromHandleControlType": "Pane",
"fromHandleRect": "\u003cempty\u003e",
"fromHandleIsOffscreen": false,
"elementFromPointAtItsOrigin": {
"resolved": true,
"controlType": "Window",
"automationId": "frmScratchMain",
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
"rect": "100,100,776,559"
},
"priorEvidence": "U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.",
"consequence": "a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats \"not hit-testable\" as \"does not exist\" will be right about the click and wrong about the element."
},
"minimized": {
"question": "what does a minimized window report, and what does a hit test at its former coordinates return",
"priorEvidence": "U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.",
"pointProbed": "130,380",
"pointChoice": "a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself",
"topLevelRectBefore": "100,100,776,559",
"topLevelRectAfterMinimize": "\u003cempty\u003e",
"topLevelRectAfterRestore": "100,100,776,559",
"topLevelIsOffscreenBefore": false,
"topLevelIsOffscreenAfterMinimize": false,
"descendantAutomationId": "btnAction",
"descendantFoundAfterMinimize": true,
"descendantRectBefore": "340,235,104,26",
"descendantRectAfterMinimize": "-31768,-31896,104,26",
"descendantIsOffscreenBefore": false,
"descendantIsOffscreenAfterMinimize": false,
"win32IsIconic": true,
"elementFromPointBefore": {
"resolved": true,
"controlType": "Pane",
"automationId": "pnlScroll",
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
"rect": "124,363,340,180"
},
"elementFromPointAfterMinimize": {
"resolved": true,
"controlType": "Pane",
"automationId": "1",
"className": "SysListView32",
"topLevelWindow": "Program Manager",
"rect": "0,0,1639,732"
},
"elementFromPointAtDescendantAnchor": {
"resolved": true,
"controlType": "Pane",
"automationId": "",
"className": "#32769",
"topLevelWindow": "\u003cno-top-level-reached\u003e",
"rect": "0,0,1639,732"
},
"hitTestFreedTheCoordinates": true,
"gateConsequence": "ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.",
"rectRecordingNote": "every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record."
}
}