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
145 changes: 138 additions & 7 deletions Tools/windows/uia-live-gate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,15 @@ function Ensure-ShellForeground(
# silently-passing check. That is exactly the transient class the original comment
# described. Unlike the ENA case, this can't make an assertion vacuous: it always
# throws (never returns a masking @()) unless it genuinely recovers within budget.
function Test-RetryableUiaError([System.Management.Automation.ErrorRecord] $errorRecord) {
$exception = $errorRecord.Exception
$inner = $exception.InnerException
return ($exception -is [System.Runtime.InteropServices.COMException]) -or
($inner -is [System.Runtime.InteropServices.COMException]) -or
($exception -is [System.Windows.Automation.ElementNotAvailableException]) -or
($inner -is [System.Windows.Automation.ElementNotAvailableException])
}

function Get-DirectChildren(
[System.Windows.Automation.AutomationElement] $element,
[System.Windows.Automation.TreeWalker] $walker
Expand Down Expand Up @@ -841,10 +850,7 @@ function Get-DirectChildren(
# exception types get a bounded, wall-clock-limited chance to resolve
# before that unconditional throw.
$inner = $_.Exception.InnerException
$isRetryable = ($_.Exception -is [System.Runtime.InteropServices.COMException]) -or
($inner -is [System.Runtime.InteropServices.COMException]) -or
($_.Exception -is [System.Windows.Automation.ElementNotAvailableException]) -or
($inner -is [System.Windows.Automation.ElementNotAvailableException])
$isRetryable = Test-RetryableUiaError $_
$hresult = if ($inner) { $inner.HResult } else { $_.Exception.HResult }
$innerType = if ($inner) { $inner.GetType().FullName } else { "" }
$attempt++
Expand Down Expand Up @@ -981,16 +987,21 @@ function Wait-ForGraphChildren(
[int] $delayMs = 100
) {
$observed = @()
$children = @()
$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)
$children = @(Get-DirectChildren $liveGraph $walker)
$observed = @($children | Where-Object $filter)
if (& $until $observed) { break }
} else {
$children = @()
$observed = @()
}
Start-Sleep -Milliseconds $delayMs
}
return [pscustomobject]@{ Graph = $liveGraph; Items = $observed }
return [pscustomobject]@{ Graph = $liveGraph; Items = $observed; Children = $children }
}

# A modal teardown (e.g. dismissing the update-offer dialog via SendCommand)
Expand Down Expand Up @@ -1055,6 +1066,7 @@ function Wait-ForRootReconnect(
break
}
} catch {
if (-not (Test-RetryableUiaError $_)) { throw }
$lastException = $_
Write-Host "UIA_ROOT_RECONNECT_RETRY attempt=$attempt type=$($_.Exception.GetType().FullName) message=$($_.Exception.Message)"
}
Expand Down Expand Up @@ -2292,6 +2304,7 @@ try {
$workspaceCards[0].GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Current.IsSelected) `
"loop invocation did not transition to the selected workspace loop"
$workspaceToolbar = $null
$workspaceLoopBar = $null
$workspaceShowGraph = $null
$workspaceTabs = @()
$workspaceControls = @()
Expand All @@ -2314,6 +2327,10 @@ try {
$workspaceToolbar = @($workspaceChildren | Where-Object {
$_.Current.AutomationId -match '^workspace-toolbar-' -and $_.Current.Name -eq "UIA project"
}) | Select-Object -First 1
$workspaceLoopBar = @($workspaceChildren | Where-Object {
$_.Current.AutomationId -match '^workspace-loop-bar-' -and
$_.Current.Name -eq "Selected loop workspace"
}) | Select-Object -First 1
$workspaceShowGraph = @($workspaceChildren | Where-Object {
$_.Current.AutomationId -match '^workspace-show-graph-' -and $_.Current.Name -eq "Show in Graph"
}) | Select-Object -First 1
Expand All @@ -2336,7 +2353,8 @@ try {
$workspaceUsage = @($workspaceChildren | Where-Object {
$_.Current.AutomationId -match '^workspace-detail-usage-' -and $_.Current.Name -match 'tokens$'
}) | Select-Object -First 1
if (($null -ne $workspaceToolbar) -and ($null -ne $workspaceShowGraph) -and
if (($null -ne $workspaceToolbar) -and ($null -ne $workspaceLoopBar) -and
($null -ne $workspaceShowGraph) -and
($workspaceTabs.Count -ge 1) -and ($workspaceControls.Count -eq 3) -and
($null -ne $workspacePanelToggle) -and ($null -ne $workspaceSparkline) -and
($null -ne $workspaceStart) -and ($null -ne $workspaceUsage)) {
Expand All @@ -2346,6 +2364,8 @@ try {
}
Require ($null -ne $workspaceToolbar) `
"workspace chrome omitted the toolbar identity child"
Require ($null -ne $workspaceLoopBar) `
"workspace chrome omitted the selected-loop identity child"
Require ($null -ne $workspaceShowGraph) `
"workspace chrome omitted the Show in Graph child"
Require ($workspaceControls.Count -eq 3) `
Expand All @@ -2356,6 +2376,12 @@ try {
Require ($null -ne $workspaceSparkline) "workspace right panel omitted metric sparkline child"
Require ($null -ne $workspaceStart) "workspace right panel omitted start-time child"
Require ($null -ne $workspaceUsage) "workspace right panel omitted token-usage child"
$expectedShowGraphCardId = $workspaceCards[0].Current.AutomationId
$expectedShowGraphLoopRowId = $activeLoopRow.Current.AutomationId
$expectedWorkspaceToolbarId = $workspaceToolbar.Current.AutomationId
$expectedWorkspaceLoopBarId = $workspaceLoopBar.Current.AutomationId
$expectedWorkspaceShowGraphId = $workspaceShowGraph.Current.AutomationId

# The four detail children can be present in the UIA tree before the loop panel has
# laid them out, so existence (which the mount loop above waits for) does not imply
# non-empty bounds. They are also served by a custom fragment provider that destroys
Expand Down Expand Up @@ -2480,6 +2506,111 @@ try {
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"

# Resolve the current workspace generation after the tab round trip, then invoke
# Show in Graph once. Poll only for the positive destination precondition: the
# exact selected card must exist. Workspace-chrome absence is asserted separately
# from the same successful generation, so an unavailable tree cannot pass as empty.
$showGraphActionProbe = Wait-ForGraphChildren $root $rawWalker `
{ $_.Current.AutomationId -in @($expectedWorkspaceLoopBarId, $expectedWorkspaceShowGraphId) } `
{ param($items)
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedWorkspaceLoopBarId }).Count -eq 1) -and
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedWorkspaceShowGraphId }).Count -eq 1) }
$graph = $showGraphActionProbe.Graph
$workspaceShowGraph = @($showGraphActionProbe.Items | Where-Object {
$_.Current.AutomationId -eq $expectedWorkspaceShowGraphId -and
$_.Current.Name -eq "Show in Graph"
})
Require ($workspaceShowGraph.Count -eq 1) `
"workspace round trip did not retain the exact Show in Graph action identity"
$workspaceShowGraph[0].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke()

$root = Wait-ForRootReconnect $process @($rawWalker)
$showGraphProbe = Wait-ForGraphChildren $root $rawWalker `
{ $_.Current.AutomationId -eq $expectedShowGraphCardId } `
{ param($items) $items.Count -eq 1 } `
-maxAttempts 100
$graph = $showGraphProbe.Graph
Require ($null -ne $graph) "Show in Graph did not expose the project graph"
$showGraphChildren = @($showGraphProbe.Children)
$showGraphCards = @($showGraphChildren | Where-Object {
$_.Current.AutomationId -eq $expectedShowGraphCardId -and
$_.Current.Name -eq "UIA loop A"
})
Require ($showGraphCards.Count -eq 1) `
"Show in Graph did not expose exactly one card with the selected loop's stable identity"
$remainingWorkspaceChrome = @($showGraphChildren | Where-Object {
$_.Current.AutomationId -match '^workspace-(toolbar|show-graph|loop-bar|tab|new-tab|split-right|split-down)-'
})
Require ($remainingWorkspaceChrome.Count -eq 0) `
"Show in Graph retained workspace chrome: $(@($remainingWorkspaceChrome | ForEach-Object { $_.Current.AutomationId }) -join '|')"
Require ($showGraphCards[0].GetCurrentPattern(
[System.Windows.Automation.SelectionItemPattern]::Pattern
).Current.IsSelected) `
"Show in Graph did not retain selection of the exact UIA loop A card"
$process.Refresh()
Require (-not $process.HasExited) "Show in Graph terminated the shell"

# Cards are intentionally non-invokable. Return through the supported sidebar
# row, asserting its exact stable identity before invoking it, then require the
# reopened workspace to expose the same project and selected-loop identities.
$showGraphSidebar = Find-FragmentById $root "loops" $rawWalker
Require ($null -ne $showGraphSidebar) "Show in Graph result omitted the sidebar loop list"
$showGraphLoopRows = @(Get-DirectChildren $showGraphSidebar $rawWalker | Where-Object {
$_.Current.AutomationId -eq $expectedShowGraphLoopRowId -and
$_.Current.Name -eq "UIA loop A"
})
Require ($showGraphLoopRows.Count -eq 1) `
"Show in Graph result omitted the selected loop's exact sidebar identity"
$showGraphLoopRows[0].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke()

$root = Wait-ForRootReconnect $process @($rawWalker)
$roundTripProbe = Wait-ForGraphChildren $root $rawWalker `
{ $_.Current.AutomationId -in @(
$expectedShowGraphCardId,
$expectedWorkspaceToolbarId,
$expectedWorkspaceLoopBarId,
$expectedWorkspaceShowGraphId
) } `
{ param($items)
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedShowGraphCardId }).Count -eq 1) -and
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedWorkspaceToolbarId }).Count -eq 1) -and
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedWorkspaceLoopBarId }).Count -eq 1) -and
(@($items | Where-Object { $_.Current.AutomationId -eq $expectedWorkspaceShowGraphId }).Count -eq 1) }
$graph = $roundTripProbe.Graph
$roundTripSelectedCard = @($roundTripProbe.Items | Where-Object {
$_.Current.AutomationId -eq $expectedShowGraphCardId -and $_.Current.Name -eq "UIA loop A"
})
$roundTripToolbar = @($roundTripProbe.Items | Where-Object {
$_.Current.AutomationId -eq $expectedWorkspaceToolbarId -and $_.Current.Name -eq "UIA project"
})
$roundTripLoopBar = @($roundTripProbe.Items | Where-Object {
$_.Current.AutomationId -eq $expectedWorkspaceLoopBarId -and
$_.Current.Name -eq "Selected loop workspace"
})
$roundTripShowGraph = @($roundTripProbe.Items | Where-Object {
$_.Current.AutomationId -eq $expectedWorkspaceShowGraphId -and
$_.Current.Name -eq "Show in Graph"
})
Require (($roundTripSelectedCard.Count -eq 1) -and
$roundTripSelectedCard[0].GetCurrentPattern(
[System.Windows.Automation.SelectionItemPattern]::Pattern
).Current.IsSelected -and
($roundTripToolbar.Count -eq 1) -and
($roundTripLoopBar.Count -eq 1) -and
($roundTripShowGraph.Count -eq 1)) `
"Show in Graph sidebar return did not reopen the same project and selected-loop workspace"

# Rebind all provider references used below from the returned generation.
$projects = Find-FragmentById $root "projects" $rawWalker
$loops = Find-FragmentById $root "loops" $rawWalker
$overviewDestination = Find-FragmentById $root "overview-destination" $rawWalker
Require (($null -ne $projects) -and ($null -ne $loops) -and
($null -ne $overviewDestination)) `
"Show in Graph round trip did not restore downstream navigation providers"
$surfaceActionPatterns["overview-destination"] = $overviewDestination.GetCurrentPattern(
[System.Windows.Automation.InvokePattern]::Pattern
)
$surfaceActionPatterns["overview-destination"].Invoke()
Start-Sleep -Milliseconds 150
Require ([GraphCodeUiaGateState]::PostTaggedExitCollision($process.MainWindowHandle)) `
Expand Down
2 changes: 1 addition & 1 deletion investigation/ui-parity-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Statuses:
| Pane headers | agent/shell identity, backend/shell detail, focused state | Product-owned pane headers distinguish agent and shell panes, label the zmx session detail, add truthful backend: agent/backend: shell detail, and retain the explicit focused-pane accent. Focused rendering unit coverage plus the live `windows-shell` CI run (real workspace/terminal panes, run https://github.com/scgopi/GraphCode/actions/runs/35415967793) provide the side-by-side live evidence that was previously blocked | Validated |
| Mounted background tabs | Switching preserves live terminal surfaces | Workspace implementation tests still cover the topology, and the live UIA gate now creates a second mounted tab, switches between the original and background tab, and asserts both tab automation identities survive the round trip without shell exit/reconnection | Partial |
| Right loop panel | Minimap, upstream/downstream, fired conditions, metric sparkline, branch/start/usage footer | The full workspace right rail includes the selected-loop map, upstream/downstream cards, fired-edge coloring, edge conditions, branch/worktree identity, metric/goal detail, model tier, a metric-history sparkline from decoded samples, start-time/usage/backend footer text, a collapse/expand control that no longer reserves rail width while hidden, and dedicated UIA children for sparkline/start/usage/toggle (`workspace-detail-sparkline-*`, start, usage, and toggle automation IDs). The live UIA gate asserts those children and toggles collapse/expand, and that gate passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated |
| Show in Graph | Visible loop-bar and menu action | The restored Loop menu and native loop bar expose Show in Graph, and focused hit testing covers the visible action. The live gate asserts that a `workspace-show-graph-*` child named `Show in Graph` is present in the real shell build's workspace chrome (`uia-live-gate.ps1:1156` discovery, `:1188` `Require`), and that assertion passes on `windows-shell`. **Presence is all that is proven.** The gate never retrieves an `InvokePattern` for this element and never invokes it, so "invokable" is not established; there is also no return-to-graph-card walkthrough in the gate (no `graph-card` assertion exists anywhere in `uia-live-gate.ps1`), and the earlier claim of one was inaccurate. Invoking Show in Graph and observing the resulting navigation back to the graph still needs live evidence | Partial |
| Show in Graph | Visible loop-bar and menu action | The live gate invokes the real `workspace-show-graph-*` loop-bar action through `InvokePattern`, requires the exact selected `UIA loop A` `canvas-card-*` identity and selected state with strict workspace-chrome absence, then returns through the exact supported `loop-row-*` identity and requires the same project toolbar, selected-loop bar, and Show in Graph identities. A focused isolated pair at `.graphcode-evidence/show-in-graph-20260924-151455` used source gate SHA-256 `AE4A2C71...`, focused harness `78E6821E...`, and shell `8E0AA9E7...`: GREEN invoked the action and completed the exact round trip (stdout `E94DCA9D...`); RED disabled only that invocation and failed the unchanged chrome-absence assertion (stderr `3198914D...`). This confirms the route without a product fix; graph cards remain intentionally non-invokable. The focused early return did not exercise later downstream-provider rebinding. A full integrated gate pass and live native Loop-menu invocation remain unverified. | Partial |

**Live-gate infrastructure fix (this session):** the `windows-shell` CI job's `uia-live-gate.ps1` step was, until now, never actually exercising any of the workspace chrome above: `App.init()` unconditionally skipped `Workspace.init()` under `GRAPHCODE_UIA_GATE=1` regardless of whether a real `zmx` executable was supplied (a pre-existing guard predating this workstream), so every "Partial" row above had never been run against a real workspace at all. Fixed in `App.zig` to build the real workspace under the gate whenever `GRAPHCODE_ZMX` is present. That surfaced a second, genuine regression: the newly-real terminal surface competed for native Win32 keyboard focus with the rest of the UI after navigating away from the workspace (`App.openGlobalOverview()` and friends). Root-caused to `Workspace.poll()` (driven by the main window's 100ms `WM_TIMER`) unconditionally draining terminal output and calling `winghostty_surface_notify_accessibility_text()` regardless of workspace visibility, which kept re-asserting UI Automation focus on the terminal no matter what Win32-level focus fixes were made. Fixed by adding `Workspace.collapse()`/`Workspace.collapsed`, skipping `resize()`/`syncTopology()`'s pane refocus and terminal-output polling entirely while the workspace is hidden, plus a `WM_ACTIVATE` handler that reasserts the app's own focus policy after `DefWindowProc`'s default child-focus restoration on window reactivation. All of this is now covered by the passing `windows-shell` CI job (commits `a31813b`..`cba010f`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793). Note: the separate `windows-spikes`/`windows-hardening` jobs (`validate.ps1 -Task all`) run the identical gate script but under much heavier CI load and still intermittently hit this same assertion's 15-second retry window; this has been confirmed as pre-existing, cross-branch flakiness unrelated to this workstream (an unrelated sibling branch, `coneilen-microsoft-repository-settings-parity`, shows both a pass and an unrelated failure on the same job across consecutive runs), not a regression introduced here.

Expand Down
Loading