Skip to content

Re-resolve the graph and poll preconditions across canvas assertions - #444

Merged
coneilen merged 1 commit into
mainfrom
gate-eliminate-sleep-then-read
Sep 24, 2026
Merged

coneilen merged 1 commit into
mainfrom
gate-eliminate-sleep-then-read

Conversation

@coneilen

Copy link
Copy Markdown
Collaborator

Problem

#443 fixed the workspace chrome mount loop, and the gate then advanced past that block — and failed further downstream at line 1739:

switching to the mounted background tab changed terminal tab identity

This is the same defect, in its fourth location. Rather than fix a fifth site later, this change removes the pattern everywhere it remains.

The shape is:

$someControl.Invoke()
Start-Sleep -Milliseconds 150
$items = @(Get-DirectChildren $graph $rawWalker | Where-Object { ... })
Require (<assertion about $items>) "..."

Both halves are unsound:

  1. The parent is stale. The fragment provider recreates the graph and its children when the canvas re-lays out, which is exactly what the preceding Invoke() triggers. Get-DirectChildren on a dead parent returns nothing for as long as it is asked, so no sleep length can outlast it.
  2. The read is a single sample. One read after a fixed delay captures one arbitrary moment, so a fast runner passes and a loaded one fails.

In the tab-identity case these combine into a particularly quiet failure: the read returns an empty list, $workspaceTabs[0] is $null, and $null.Current.AutomationId yields $null without throwing — so the identity comparison fails with no indication that nothing was ever read.

This also explains why #440, #441, #442 and #443 each appeared to fix this region and then failed somewhere adjacent. There was never more than one defect; each fix corrected the site that happened to be reached first, and the next run surfaced the next occurrence.

Fix

Wait-ForGraphChildren re-resolves the graph parent on every attempt and polls until the caller's precondition holds, replacing all seven remaining sites.

The important constraint: it waits on preconditions only, never on an assertion. For the tab-identity checks it waits for two tabs to exist, then asserts their ids are unchanged — so a genuine identity regression still fails exactly as before. On timeout it returns the last observed state so each Require reports the real condition with its original message, rather than masking a failure as a timeout.

A latent vacuous pass, fixed

$initialWorkspaceTabId was captured from a tab list that had not necessarily remounted. When that read came back empty the baseline id was $null, and if the later read was also empty the assertion compared $null -eq $null and passed — reporting success while verifying nothing. This adds a Require that the initial tab exists before its id is captured, which is the one new assertion in this change.

Evidence

RED: CI run 35958493192 -> FAIL switching to the mounted background tab changed terminal tab identity at uia-live-gate.ps1:447, downstream of #443's fix and caused by the same stale-parent read.

GREEN: the windows-shell live gate now advances past the terminal tab identity block -> see the windows-shell check on this PR.

REGRESSION: Tools\windows\Tests\WindowsShell.Tests.ps1 -ZigExecutable $env:GRAPHCODE_ZIG0152 -> All 266 tests passed, Windows shell scaffold contract: PASS (40 source files executed).

Verification

  • Parses clean.
  • Require predicates compared against main with Compare-Object: zero lost, 379 -> 380, the single addition being the vacuous-pass guard described above. Every existing assertion and failure message is preserved verbatim.
  • This check earned its keep: an intermediate revision of this change silently dropped Require ($workspaceTabs.Count -ge 2) "New Tab did not expose a mounted background tab", and the comparison caught it before it was committed.
  • Zero remaining instances of fixed-sleep-followed-by-cached-$graph-read in the file.
  • No C# touched, so the shared Add-Type block and its declaration counts are unaffected.

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 <coneilen@microsoft.com>
@coneilen
coneilen merged commit 138dc3b into main Sep 24, 2026
10 checks passed
@coneilen
coneilen deleted the gate-eliminate-sleep-then-read branch September 24, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant