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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Tools/windows/Invoke-UiCaptureWorker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2114,6 +2114,48 @@ function Invoke-CapMatchedScenes {
bounds=@($mark[0].Current.BoundingRectangle.Left,$mark[0].Current.BoundingRectangle.Top,
$mark[0].Current.BoundingRectangle.Right,$mark[0].Current.BoundingRectangle.Bottom)}})
}
if ($scene -cmatch '^s-main-cycle-') {
$actionIdentity="attention-action:$($fixture.graphs[0].project.path):$($state[0].nodeId)"
$reasonIdentity="cycle-reason:$($fixture.graphs[0].project.path):$($state[0].nodeId)"
$actionId=Get-CapMatchedAutomationId $actionIdentity
$reasonId=Get-CapMatchedAutomationId $reasonIdentity
$graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph'
$children=@(Get-CapDirectChildren -Parent $graph)
$inspect=@($children | Where-Object {$_.Current.AutomationId -ceq $actionId -and
$_.Current.Name -ceq 'Inspect'})
$reason=@($children | Where-Object {$_.Current.AutomationId -ceq $reasonId -and
$_.Current.Name -ceq 'cycle · no entry point'})
if ($inspect.Count -ne 1 -or $reason.Count -ne 1) {
throw "Matched cycle card omitted its separate Inspect action or footer reason for $scene."
}
if ($inspect[0].Current.ProcessId -ne $app.Run.Identity.Pid -or
$reason[0].Current.ProcessId -ne $app.Run.Identity.Pid -or
$inspect[0].Current.ControlType -ne [Windows.Automation.ControlType]::Button -or
$reason[0].Current.ControlType -ne [Windows.Automation.ControlType]::Text) {
throw "Matched cycle card exposed the wrong UIA ownership or control types for $scene."
}
$null=$inspect[0].GetCurrentPattern([Windows.Automation.InvokePattern]::Pattern)
$reasonPattern=$null
if ($reason[0].TryGetCurrentPattern([Windows.Automation.InvokePattern]::Pattern,[ref]$reasonPattern)) {
throw "Matched cycle footer reason incorrectly exposed Invoke for $scene."
}
$inspectBounds=$inspect[0].Current.BoundingRectangle
$reasonBounds=$reason[0].Current.BoundingRectangle
if ($inspectBounds.Width -le 0 -or $inspectBounds.Height -le 0 -or
$reasonBounds.Width -le 0 -or $reasonBounds.Height -le 0 -or
$inspectBounds.Bottom -gt $reasonBounds.Top) {
throw "Matched cycle card did not keep Inspect above the footer reason for $scene."
}
$app.SObservation | Add-Member -NotePropertyName cycleHierarchy -NotePropertyValue `
([pscustomobject]@{inspect=[pscustomobject]@{name='Inspect';automationId=$actionId
controlType='Button';invokable=$true;bounds=@($inspectBounds.Left,$inspectBounds.Top,
$inspectBounds.Right,$inspectBounds.Bottom)}
reason=[pscustomobject]@{name='cycle · no entry point';automationId=$reasonId
controlType='Text';invokable=$false;bounds=@($reasonBounds.Left,$reasonBounds.Top,
$reasonBounds.Right,$reasonBounds.Bottom)}
selected=[bool]$state[0].selected;cardIdentity=$card[0].AutomationId})
}
Save-CapSceneImage -AppRun $app -SceneName $scene -ZoomEvidence $zoom -Phase $Phase
if ($null -ne $recovery) {
$null=Invoke-CapSceneAction -AppRun $app -Action 'wire-node' -SubjectId $recovery.wireIdentity
Expand Down
60 changes: 60 additions & 0 deletions Tools/windows/Tests/AccessibilityProvider.Native.Tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,65 @@ static void nativeUnwiredRecoveryChecks(IRawElementProviderSimple *provider, HWN
if (projects) projects->Release();
}

static void nativeCycleHierarchyChecks(IRawElementProviderSimple *provider, HWND hwnd,
IRawElementProviderFragment *root) {
const char *identities[] = {
"attention-action:C:\\fixture:main",
"cycle-reason:C:\\fixture:main",
};
const char *names[] = {"Inspect", "cycle \xC2\xB7 no entry point"};
const int parents[] = {4, 4};
const int none[] = {0, 0};
const int actions[] = {1, 0};
const int bounds[] = {
412, 151, 478, 168,
261, 175, 478, 188,
};
auto update = reinterpret_cast<FullUpdate>(&gc_uia_update);
check(update(provider, "Cycle hierarchy", identities, names, parents, none, none, actions, bounds,
2, 0, 1, 0, 0, 0, 0) == S_OK,
"cycle hierarchy publication succeeds");

auto *projects = child(root, NavigateDirection_FirstChild);
auto *loops = child(projects, NavigateDirection_NextSibling);
auto *worktrees = child(loops, NavigateDirection_NextSibling);
auto *graph = child(worktrees, NavigateDirection_NextSibling);
auto *inspect = child(graph, NavigateDirection_FirstChild);
auto *reason = child(inspect, NavigateDirection_NextSibling);
check(stringProperty(inspect, UIA_AutomationIdPropertyId).rfind(L"attention-action-", 0) == 0,
"Inspect keeps its stable action automation id");
check(stringProperty(reason, UIA_AutomationIdPropertyId).rfind(L"cycle-reason-", 0) == 0,
"cycle reason has a stable metadata automation id");
checkName(inspect, L"Inspect");
checkName(reason, L"cycle \u00b7 no entry point");
check(invokable(inspect), "Inspect remains invokable");
check(!invokable(reason), "cycle reason is metadata, not an action");

IRawElementProviderSimple *reason_simple = nullptr;
check(reason && reason->QueryInterface(IID_PPV_ARGS(&reason_simple)) == S_OK && reason_simple,
"cycle reason exposes native UIA properties");
if (reason_simple) {
VARIANT control_type;
VariantInit(&control_type);
check(reason_simple->GetPropertyValue(UIA_ControlTypePropertyId, &control_type) == S_OK &&
control_type.vt == VT_I4 && control_type.lVal == UIA_TextControlTypeId,
"cycle reason is exposed as Text");
VariantClear(&control_type);
reason_simple->Release();
}
POINT origin{0, 0};
ClientToScreen(hwnd, &origin);
checkRect(inspect, origin, 412, 151, 66, 17, "Inspect retains its body action bounds");
checkRect(reason, origin, 261, 175, 217, 13, "cycle reason occupies the footer bounds");

if (reason) reason->Release();
if (inspect) inspect->Release();
if (graph) graph->Release();
if (worktrees) worktrees->Release();
if (loops) loops->Release();
if (projects) projects->Release();
}

static void nativeQuickChatRestartChecks(IRawElementProviderSimple *provider, HWND hwnd,
IRawElementProviderFragment *root) {
check(!IsWindowVisible(hwnd), "Quick Chat restart is tested against a hidden provider window");
Expand Down Expand Up @@ -744,6 +803,7 @@ static void nativeCommandAndScrollChecks() {
nativeHeaderOverflowChecks(provider, capture, root);
nativeBannerInvokeChecks(provider, capture, root);
nativeUnwiredRecoveryChecks(provider, capture, root);
nativeCycleHierarchyChecks(provider, capture, root);
nativeQuickChatRestartChecks(provider, capture, root);
nativeQuickChatWorkspaceBarChecks(provider, root);
nativeScrollChecks(provider, capture, root);
Expand Down
4 changes: 2 additions & 2 deletions Tools/windows/Tests/AccessibilityProvider.Native.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ exit /b %test_exit%
$setupPathLength = $setupPath[0].Substring(5).Length
Write-Host "[native-uia] post-vcvars PATH length=$setupPathLength (cmd limit=8191)"
if ($setupPathLength -ge 8191) { throw "Native accessibility provider post-vcvars PATH exceeds cmd limit: $setupPathLength" }
if (@($nativeOutput | Where-Object { $_ -eq "Native accessibility provider: 240 checks, 0 failures" }).Count -ne 1) {
throw "Native accessibility provider test-exe did not report exactly one 240-check, zero-failure summary"
if (@($nativeOutput | Where-Object { $_ -eq "Native accessibility provider: 259 checks, 0 failures" }).Count -ne 1) {
throw "Native accessibility provider test-exe did not report exactly one 259-check, zero-failure summary"
}
} finally {
$env:TEMP = $savedTemp
Expand Down
22 changes: 21 additions & 1 deletion Tools/windows/Tests/UiCaptureDriver.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,11 @@ function Invoke-CapMatchedContracts {
$proof=Assert-CapMatchedFixture -Fixture $fixture -ProjectRoot 'C:\fixture'
Assert-CapTest ($proof.states.Count -eq 6 -and $proof.nodes -eq 8 -and $proof.macSource -ceq
'2ccf303a04f995184aae1d4b738a9eca0f71b79e') 'Matched fixture typed inventory differs.'
$workerSource=Get-Content -LiteralPath $workerPath -Raw
Assert-CapTest ($workerSource.Contains("cycle-reason:") -and
$workerSource.Contains("Matched cycle card did not keep Inspect above the footer reason") -and
$workerSource.Contains("controlType='Text';invokable=`$false")) `
'Matched cycle scenes do not require separate body action and footer-reason UIA evidence.'
$wire=Invoke-CapPeerWireCase -Peer B -Case clean -Source (Get-CapBPeerSource) `
-FixturePath (Join-Path $driverDirectory 'fixtures\matched-v12.json') -CaseRoot (Join-Path $scratch 'matched-fixture-wire')
return @{fixture=$proof;wire=$wire;qualifiedStates=$proof.states.Count;nodes=$proof.nodes
Expand Down Expand Up @@ -3349,7 +3354,7 @@ function Invoke-CapMatchedTabContracts {
$terminalSource -cmatch '(?s)pub fn canCloseTab\(self: \*const Workspace\) bool \{\s*return self\.layout\.tabs\.items\.len > 1;\s*\}' -and
$accessibilitySource.Contains('sidebar_scroll: bool = false,') -and
$providerSource -cmatch '(?s)bool supportsScrollItem\(\) const \{.*?if \(isRowKey\(id_\)\) return state_->rows\.at\(id_\)\.scrolls;' -and
$providerSource -cmatch '(?s)if \(isRowKey\(id_\)\) \{.*?bool_value = sidebar_error_footer.*?: true;') `
$providerSource -cmatch '(?s)if \(isRowKey\(id_\)\) \{.*?bool_value = static_text.*?: true;') `
'Product tab close source contract no longer matches the capture interpretation.'
foreach ($vector in @(
@('workspace-tab:0','workspace-tab-2015407614065646652'),
Expand Down Expand Up @@ -4047,10 +4052,18 @@ function Invoke-CapMatchedSceneRoutingContract {
}
function New-CapMockElement([string]$Id,[string]$Name,[int]$ProcessId,[double[]]$Bounds) {
$current=[pscustomobject]@{AutomationId=$Id;Name=$Name;ProcessId=$ProcessId
ControlType=$(if ($Id -clike 'cycle-reason-*') {[Windows.Automation.ControlType]::Text}
else {[Windows.Automation.ControlType]::Button})
BoundingRectangle=[pscustomobject]@{Left=$Bounds[0];Top=$Bounds[1];Right=$Bounds[2];Bottom=$Bounds[3]
Width=($Bounds[2]-$Bounds[0]);Height=($Bounds[3]-$Bounds[1])}}
$element=[pscustomobject]@{Current=$current}
$element | Add-Member -MemberType ScriptMethod -Name GetCurrentPattern -Value {param($Pattern) [pscustomobject]@{}}
$element | Add-Member -MemberType ScriptMethod -Name TryGetCurrentPattern -Value {
param($Pattern,[ref]$Value)
if ($this.Current.AutomationId -clike 'cycle-reason-*') {return $false}
$Value.Value=[pscustomobject]@{}
return $true
}
return $element
}
function Find-CapAutomationElement {
Expand All @@ -4073,6 +4086,13 @@ function Invoke-CapMatchedSceneRoutingContract {
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("mark-entry:"+$fixture.graphs[0].project.path+":"+$nodeId)) `
'Mark as entry' 4242 @(340,131,426,151)))
}
if ($scene -cmatch '^s-main-cycle-') {
$nodeId=@($fixture.states | Where-Object scene -CEQ $scene)[0].nodeId
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("attention-action:"+$fixture.graphs[0].project.path+":"+$nodeId)) `
'Inspect' 4242 @(412,151,478,168)))
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("cycle-reason:"+$fixture.graphs[0].project.path+":"+$nodeId)) `
'cycle · no entry point' 4242 @(261,175,478,188)))
}
return $rows.ToArray()
}
function Send-CapMatchedEscape {param($AppRun) $model.wireArmed=$false;return $true}
Expand Down
6 changes: 6 additions & 0 deletions Tools/windows/Tests/ValidationRunner.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3683,6 +3683,12 @@ try {
if ($uiaErrors.Count -ne 0) {
throw "RED: UIA live gate source does not parse"
}
if ($uiaLiveGateSource -notmatch '\[switch\]\$CycleHierarchyOnly' -or
$uiaLiveGateSource -notmatch 'PostFixtureMutation\(\$shellWindow, 23\)' -or
$uiaLiveGateSource -notmatch 'UIA_CYCLE_HIERARCHY_EVIDENCE=' -or
$uiaLiveGateSource -notmatch 'UIA_CYCLE_HIERARCHY_SHA256=') {
throw "RED: UIA live gate does not exercise and hash both exact cycle-card hierarchy scenes"
}
$chromeClassifier = $uiaAst.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
Expand Down
2 changes: 1 addition & 1 deletion Tools/windows/UI-CAPTURE-SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ observed UI state, not a `selected` property invented on the daemon's node.
| Scene names | State and comparison boundary |
| --- | --- |
| `s-goal-unwired-selected`, `s-goal-unwired-unselected` | Goal / `goalBased` / `awaitingInput`, no inbound or outbound edge; M6 unwired NEEDS YOU context. Both selection variants are recorded with owned, visible `Wire it up` and `Mark as entry` UIA controls. Each scene captures the unchanged unwired state first, then invokes the exact stable control IDs: Wire enters the existing edge-drag route and is cancelled with Escape; Mark changes the card to entry and removes both unwired controls while preserving the same card identity. These observations prove the live Windows routes, not paired macOS visual equivalence by themselves. |
| `s-main-cycle-selected`, `s-main-cycle-unselected` | Main / `sketch` / `blocked`, two unfired blocking handoff edges form a closed cycle with no external entry; M1 Main-cycle context. |
| `s-main-cycle-selected`, `s-main-cycle-unselected` | Main / `sketch` / `blocked`, two unfired blocking handoff edges form a closed cycle with no external entry; M1 Main-cycle context. Both scenes require a separate owned `Inspect` Button in the body and non-invokable `cycle · no entry point` Text in the metadata footer, with positive bounds and the body action ending above the footer reason. The selected scene also retains the same stable card identity and observed selection after the normal card-activation/show-graph route. |
| `s-main-stopped-reclaim` | Actual Main / `sketch` / `stopped` ENTRY, null resolution, separate worktree binding; M4 Main STOPPED entry context. `GraphStore.swift` clears resolution outside succeeded/failed. |
| `s-goal-done-reclaim` | Actual Goal / `goalBased` / `succeeded` interior, predicate resolution and separate worktree binding; controls/context only against M4, **not** an equivalent Main outcome. |
| `s-tabs-goal-needs-you` | One Goal tab, agent asks state, selected focused pane. |
Expand Down
Loading
Loading