From 268acc767b82b564e3f401e2f3d5bca432ae1793 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Wed, 23 Sep 2026 22:30:10 -0700 Subject: [PATCH] Re-resolve the graph and poll preconditions across canvas assertions The live gate repeatedly invoked a control, slept a fixed interval, then read children from a \ captured before the invocation. Both halves are unsound: the provider recreates the graph and its children on re-layout, so a captured parent can be dead - and Get-DirectChildren on a dead parent yields nothing for as long as it is asked - while a single post-sleep read samples one arbitrary moment. Add Wait-ForGraphChildren, which re-resolves the parent every attempt and polls until the caller's precondition holds, and apply it to the seven remaining sites. It waits only on preconditions, never on an assertion, and on timeout returns what it last observed so each Require still reports the real state with its original message. Also require the initial terminal tab before capturing it as the identity baseline. Reading that id from a tab list that had not remounted yielded \, which the later comparison could match against another \ and pass vacuously. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/uia-live-gate.ps1 | 163 ++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 58 deletions(-) diff --git a/Tools/windows/uia-live-gate.ps1 b/Tools/windows/uia-live-gate.ps1 index 9cda0f52..20914ad5 100644 --- a/Tools/windows/uia-live-gate.ps1 +++ b/Tools/windows/uia-live-gate.ps1 @@ -756,6 +756,39 @@ function Find-FragmentById( return $null } +# Reads children of the live "graph" fragment, re-resolving the parent on every attempt +# and polling until $until is satisfied by the filtered set. +# +# Both halves matter. The fragment provider recreates the graph and its children when the +# canvas re-lays out, so a parent captured before an Invoke can be dead by the time it is +# read - and Get-DirectChildren on a dead parent returns nothing for as long as it is +# asked, which no fixed sleep can outlast. Reading once after Start-Sleep also samples a +# single arbitrary moment, so a slow runner fails while a fast one passes. +# +# This waits on the caller's precondition only. It never waits on the assertion itself: +# on timeout it returns whatever it last observed so the caller's Require reports the real +# state with its original message. +function Wait-ForGraphChildren( + [System.Windows.Automation.AutomationElement] $root, + [System.Windows.Automation.TreeWalker] $walker, + [scriptblock] $filter, + [scriptblock] $until, + [int] $maxAttempts = 60, + [int] $delayMs = 100 +) { + $observed = @() + $liveGraph = $null + for ($attempt = 0; $attempt -lt $maxAttempts; $attempt++) { + $liveGraph = Find-FragmentById $root "graph" $walker + if ($null -ne $liveGraph) { + $observed = @(Get-DirectChildren $liveGraph $walker | Where-Object $filter) + if (& $until $observed) { break } + } + Start-Sleep -Milliseconds $delayMs + } + return [pscustomobject]@{ Graph = $liveGraph; Items = $observed } +} + function Assert-FragmentLinks( [System.Windows.Automation.AutomationElement] $parent, [System.Windows.Automation.TreeWalker] $walker, @@ -1430,8 +1463,13 @@ try { $null = Assert-FragmentLinks $graph $rawWalker $graphChildIds "RawView Graph" $null = Assert-FragmentLinks $graph $controlWalker $graphChildIds "ControlView Graph" $projectCards[1].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - Start-Sleep -Milliseconds 150 - $compositeChildren = @(Get-DirectChildren $graph $rawWalker | Where-Object { $_.Current.AutomationId -match '^canvas-card-' }) + $compositeProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^canvas-card-' } ` + { param($items) + (@($items | Where-Object { $_.Current.Name -match '^UIA nested ' }).Count -eq 2) -and + (@($items | Where-Object { $_.Current.Name -eq "Back to UIA project" }).Count -eq 1) } + $graph = $compositeProbe.Graph + $compositeChildren = @($compositeProbe.Items) $nestedCards = @($compositeChildren | Where-Object { $_.Current.Name -match '^UIA nested ' }) Require (($nestedCards.Count -eq 2) -and ((@($nestedCards | ForEach-Object { $_.Current.Name }) -join "|") -eq "UIA nested A|UIA nested B")) ` @@ -1442,10 +1480,11 @@ try { Require (($compositeBack[0].Current.BoundingRectangle.Width -gt 0) -and ($compositeBack[0].Current.BoundingRectangle.Height -gt 0)) "Composite Back breadcrumb has empty bounds" $compositeBack[0].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - Start-Sleep -Milliseconds 150 - $restoredProjectCards = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA loop ' - }) + $restoredProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA loop ' } ` + { param($items) $items.Count -eq 2 } + $graph = $restoredProbe.Graph + $restoredProjectCards = @($restoredProbe.Items) Require (($restoredProjectCards.Count -eq 2) -and ((@($restoredProjectCards | ForEach-Object { $_.Current.Name }) -join "|") -eq "UIA loop A|UIA loop B")) ` "Composite Back did not restore the parent project canvas" @@ -1459,17 +1498,19 @@ try { [System.Windows.Automation.InvokePattern]::Pattern) } $surfaceActionPatterns["overview-destination"].Invoke() - Start-Sleep -Milliseconds 150 - $overviewCards = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA loop ' - }) + $overviewProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA loop ' } ` + { param($items) $items.Count -eq 2 } + $graph = $overviewProbe.Graph + $overviewCards = @($overviewProbe.Items) Require (($overviewCards.Count -eq 2) -and ((@($overviewCards | ForEach-Object { $_.Current.Name }) -join "|") -eq "UIA loop A|UIA loop B")) "Overview did not expose synchronized cards" $surfaceActionPatterns["quick-chats-destination"].Invoke() - Start-Sleep -Milliseconds 150 - $quickChatCards = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA chat ' - }) + $quickChatProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -match '^UIA chat ' } ` + { param($items) $items.Count -eq 2 } + $graph = $quickChatProbe.Graph + $quickChatCards = @($quickChatProbe.Items) Require (($quickChatCards.Count -eq 2) -and ((@($quickChatCards | ForEach-Object { $_.Current.Name }) -join "|") -eq "UIA chat A|UIA chat B")) "Quick Chats did not expose synchronized cards: $(@($quickChatCards | ForEach-Object { $_.Current.Name }) -join '|')" $surfaceActionPatterns["canvas-primary-action"].Invoke() @@ -1691,57 +1732,60 @@ try { # observed taking longer than 150ms on a loaded CI runner. The assertion below is # unchanged -- this waits for exactly the control it already requires, and still fails # if that control never appears. - $workspacePanelToggle = $null - for ($attempt = 0; $attempt -lt 30; $attempt++) { - Start-Sleep -Milliseconds 100 - $workspaceChildren = @(Get-DirectChildren $graph $rawWalker) - $workspacePanelToggle = @($workspaceChildren | Where-Object { - $_.Current.AutomationId -match '^workspace-toggle-panel-' -and $_.Current.Name -eq "Expand loop panel" - }) | Select-Object -First 1 - if ($null -ne $workspacePanelToggle) { break } - } + $panelProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^workspace-toggle-panel-' -and $_.Current.Name -eq "Expand loop panel" } ` + { param($items) $items.Count -ge 1 } 30 + $graph = $panelProbe.Graph + $workspacePanelToggle = @($panelProbe.Items) | Select-Object -First 1 Require ($null -ne $workspacePanelToggle) "workspace right panel did not expose expand control after collapse" $workspacePanelToggle.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() # Same treatment for the expand repaint: wait for New Tab, which the assertion at the end - # of this block requires, instead of assuming a fixed 150ms is enough. - $newTab = $null - for ($attempt = 0; $attempt -lt 30; $attempt++) { - Start-Sleep -Milliseconds 100 - $workspaceChildren = @(Get-DirectChildren $graph $rawWalker) - $newTab = @($workspaceChildren | Where-Object { - $_.Current.AutomationId -match '^workspace-new-tab-' -and $_.Current.Name -eq "New Tab" - }) | Select-Object -First 1 - if ($null -ne $newTab) { break } - } - $workspaceTabs = @($workspaceChildren | Where-Object { + # of this block requires, instead of assuming a fixed 150ms is enough. Also wait for the + # first tab, whose id is captured below as the identity baseline: reading it from a tab + # list that has not remounted yet yields $null, which would later be compared against + # another $null and pass vacuously. + $newTabProbe = Wait-ForGraphChildren $root $rawWalker ` + { ($_.Current.AutomationId -match '^workspace-new-tab-' -and $_.Current.Name -eq "New Tab") -or + ($_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$') } ` + { param($items) + (@($items | Where-Object { $_.Current.AutomationId -match '^workspace-new-tab-' }).Count -ge 1) -and + (@($items | Where-Object { $_.Current.AutomationId -match '^workspace-tab-' }).Count -ge 1) } 30 + $graph = $newTabProbe.Graph + $newTab = @($newTabProbe.Items | Where-Object { + $_.Current.AutomationId -match '^workspace-new-tab-' -and $_.Current.Name -eq "New Tab" + }) | Select-Object -First 1 + $workspaceTabs = @($newTabProbe.Items | Where-Object { $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' }) + Require ($workspaceTabs.Count -ge 1) "workspace omitted its initial terminal tab before the mounted-tab preservation check" $initialWorkspaceTabId = $workspaceTabs[0].Current.AutomationId Require ($null -ne $newTab) "workspace omitted New Tab before mounted-tab preservation check" $newTab.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - for ($attempt = 0; $attempt -lt 60; $attempt++) { - Start-Sleep -Milliseconds 100 - $workspaceChildren = @(Get-DirectChildren $graph $rawWalker) - $workspaceTabs = @($workspaceChildren | Where-Object { - $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' - }) - if ($workspaceTabs.Count -ge 2) { break } - } + $mountedProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' } ` + { param($items) $items.Count -ge 2 } + $graph = $mountedProbe.Graph + $workspaceTabs = @($mountedProbe.Items) Require ($workspaceTabs.Count -ge 2) "New Tab did not expose a mounted background tab" $newWorkspaceTabId = $workspaceTabs[1].Current.AutomationId $workspaceTabs[0].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - Start-Sleep -Milliseconds 150 - $workspaceTabs = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' - }) + # Wait for both tabs to be present again after the switch remounts them, then assert + # identity. The wait covers only the precondition (two tabs exist); it deliberately + # does not wait on the ids matching, so a genuine identity change still fails here. + $switchProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' } ` + { param($items) $items.Count -ge 2 } + $graph = $switchProbe.Graph + $workspaceTabs = @($switchProbe.Items) Require (($workspaceTabs[0].Current.AutomationId -eq $initialWorkspaceTabId) -and ($workspaceTabs[1].Current.AutomationId -eq $newWorkspaceTabId)) ` "switching to the mounted background tab changed terminal tab identity" $workspaceTabs[1].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - Start-Sleep -Milliseconds 150 - $workspaceTabs = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' - }) + $switchBackProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^workspace-tab-' -and $_.Current.Name -match 'tab$' } ` + { param($items) $items.Count -ge 2 } + $graph = $switchBackProbe.Graph + $workspaceTabs = @($switchBackProbe.Items) Require (($workspaceTabs[0].Current.AutomationId -eq $initialWorkspaceTabId) -and ($workspaceTabs[1].Current.AutomationId -eq $newWorkspaceTabId)) ` "switching back from the mounted background tab changed terminal tab identity" @@ -2952,16 +2996,19 @@ try { $activityNavigationRow = @($filteredActivityRows | Where-Object { $_.Current.Name -eq 'Activity C' }) | Select-Object -First 1 Require ($null -ne $activityNavigationRow) "activity strip omitted the Activity C card after filtering" $activityNavigationRow.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() - Start-Sleep -Milliseconds 250 - $workspaceLoopBar = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^workspace-loop-bar-' - }) | Select-Object -First 1 + $loopBarProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^workspace-loop-bar-' } ` + { param($items) $items.Count -ge 1 } + $graph = $loopBarProbe.Graph + $workspaceLoopBar = @($loopBarProbe.Items) | Select-Object -First 1 Require ($null -ne $workspaceLoopBar) "activity navigation did not open a workspace" - $selectedWorkspaceCard = @(Get-DirectChildren $graph $rawWalker | Where-Object { - $_.Current.AutomationId -match '^canvas-card-' -and + $selectedProbe = Wait-ForGraphChildren $root $rawWalker ` + { $_.Current.AutomationId -match '^canvas-card-' -and $_.Current.Name -eq 'Activity C' -and - $_.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Current.IsSelected - }) | Select-Object -First 1 + $_.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Current.IsSelected } ` + { param($items) $items.Count -ge 1 } + $graph = $selectedProbe.Graph + $selectedWorkspaceCard = @($selectedProbe.Items) | Select-Object -First 1 Require ($null -ne $selectedWorkspaceCard) "activity navigation did not select the targeted loop" if ($SidebarParityOnly) { return }