Skip to content

Windows: inspect native context menus in the UIA live gate - #422

Merged
coneilen merged 2 commits into
mainfrom
coneilen-microsoft-context-menu-uia-automation
Sep 23, 2026
Merged

coneilen merged 2 commits into
mainfrom
coneilen-microsoft-context-menu-uia-automation

Conversation

@coneilen

@coneilen coneilen commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #418.

Tools/windows/uia-live-gate.ps1 had no way to open, read, or dismiss a native Win32
popup menu, so every context-menu behavior in the Windows shell was unverifiable live —
including the grayed Move Project... (unavailable: daemon support required) item that
GraphContextMenu.show() appends with MF_GRAYED.

What this adds

  • Gate-only fixture message MainWindow.wm_uia_context_menu (WM_APP + 44). App.showUiaContextMenu
    calls the same GraphContextMenu.show() the mouse path calls, with the same target data;
    only hit-test routing is bypassed. TrackPopupMenu blocks, but its own modal loop keeps
    pumping, so the gate posts the request and inspects while the shell thread is blocked.
  • Anti-hang watchdog. A timer armed before show() fires inside the menu's modal loop and
    calls EndMenu(), so an abandoned popup can never block the shell for the life of the process.
  • Gate popup capability: FindPopupMenuWindow (class #32768, scoped to the shell pid),
    MN_GETHMENU + GetMenuItemCount/GetMenuStringW/GetMenuState, PostContextMenu, and
    DismissPopupMenu; plus Wait-ForPopupMenu / Get-PopupMenuItems / Close-PopupMenu with
    bounded waits, focus diagnostics, and hard failure — no skip path.
  • Live assertions: the project menu's ordered items; Move is command 5149 with its exact
    unavailable text and a disabled state; a remote project's menu omits Move, Recycle Bin, and
    Explorer; the popup dismisses and the shell's UIA tree still answers afterwards.
  • Missing coverage fixed: GraphContextMenu.zig and MainWindow.zig unit tests were not
    executed by any harness. PR Make unavailable project relocation explicit on Windows #408's Move Project test has never actually run in CI. They now run
    in WindowsShell.Tests.ps1.

Honest scoping of the evidence

A popup menu surfaces in the UIA tree only as an empty Pane with zero MenuItem children —
I verified this three times with a two-process spike before building anything. Item identity, text,
and the MF_GRAYED state are therefore read through MN_GETHMENU against the HMENU the shell
itself handed to TrackPopupMenu. That is live evidence of what the shell renders, but it is not
UIA-tree evidence, and it is labelled that way in the gate comments, the structural tests, and the
parity ledger.

investigation/ui-parity-matrix.md: the "Project context menu" row was Validated on a stale
claim that Windows keeps Move as an Explorer /select handoff — rewritten to the actual
grayed-unavailable behavior with live evidence. "Loop context menu" and "Canvas context menu" stay
Partial, now stating exactly what would close them.

Verification

Ran locally against the real built shell (graphcode-windows.exe, pinned Zig 0.15.2, sha256
matching bootstrap.ps1) with a focused probe driving the new hook:

MENU target=1 (local project) items=12
  [8] id=5149 enabled=False sep=False state=0x1 'Move Project... (unavailable: daemon support required)'
MENU target=2 (remote project) items=10   # no 5149, no 5151, no 5144
DISMISSED closed=True ... SHELL_TREE_RESPONSIVE true
WATCHDOG_CLOSED=True

Also run locally: pinned-Zig zig build, zig test src\GraphContextMenu.zig (69 pass),
zig test src\MainWindow.zig (6 pass), ValidationRunner.Tests.ps1 (PASS), and
Parser::ParseFile on every modified PowerShell file.

Not run locally: the full uia-live-gate.ps1 end-to-end. It stops early here on a pre-existing,
unrelated workspace-toolbar assertion because the pinned zmx provider is unavailable on this
machine (its pinned commit does not build against the locally installed Zig 0.16.0). CI is the
authoritative run for the new live assertions.

RED: gate had zero popup-menu capability -> new assertion "project context menu never opened a native popup window" fails on main because PostContextMenu, FindPopupMenuWindow, and the shell hook do not exist
GREEN: wm_uia_context_menu opens the real TrackPopupMenu and MN_GETHMENU reads it -> live probe returned id=5149 state=0x1 disabled with the exact unavailable text, and the remote menu omitted 5149/5151/5144
REGRESSION: ValidationRunner structural tests plus newly wired zig tests -> deleting the popup helpers, the disabled-Move assertion, the EndMenu watchdog, or the GraphContextMenu/MainWindow test wiring now fails validation even on a runner with no live desktop

CI evidence (authoritative)

The windows-shell job is green and the live gate now publishes what the native popup
actually contained, from run on 48cfa6e:

contextMenuItemCount          : 12
contextMenuMoveProjectText    : Move Project... (unavailable: daemon support required)
contextMenuMoveProjectEnabled : False
contextMenuMoveProjectState   : 0x1
contextMenuDismissed          : True
remoteContextMenuItemCount    : 10

The job log also shows the previously-unrun unit tests executing:
8/69 GraphContextMenu.test.the real Move Project menu item is disabled with its explicit reason inline...OK.
So the live-gate blocker noted above is resolved by CI: the assertions did run, on a real
desktop, against the real TrackPopupMenu.

coneilen added a commit that referenced this pull request Sep 23, 2026
…vocation

Moves the structural guard added for #424 to the end of
WindowsShell.Tests.ps1, after all zig test invocations, instead of right
after Resolve-TestZig. Placed first, the guard's expected failure (reserving
GraphContextMenu.zig/MainWindow.zig for #418/#422) short-circuited the whole
script in CI before any of the newly-wired tests ever ran on the actual
runner, leaving only local verification as evidence. Placed last, CI now
executes and reports every invocation for real before the guard's contract
check runs, while the guard still fails the job overall until #418/#422
lands.

RED: with the guard first, CI failed at the guard on the first push and
never exercised a single newly-wired zig test -> no real CI signal existed
for the wiring itself, only local runs.
GREEN: relocated the guard after every Invoke-Native/Invoke-NativeQuarantined
call and reran the full harness locally with pinned Zig 0.15.2 -> all 37
invocations execute (90/93 Sidebar.zig, 228/231 App.zig, everything else
100%), quarantines are tolerated correctly, and the guard still throws last
naming exactly GraphContextMenu.zig and MainWindow.zig.
REGRESSION: reran the same real-mutation guard check (removing a wired file
name) after relocating the block -> guard still throws the identical
not-wired message immediately, confirming the guard's detection logic is
unchanged, only its position in the script moved.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen and others added 2 commits September 22, 2026 19:23
The live gate had no way to open, read, or dismiss a native Win32 popup
menu, so every context-menu behavior in the Windows shell -- including
the grayed Move Project item that GraphContextMenu.show() appends with
MF_GRAYED -- was unverifiable live.

Add a gate-only fixture message (MainWindow.wm_uia_context_menu) that
asks the shell to open a real context menu through the same
GraphContextMenu.show() the mouse path calls, a watchdog timer that
calls EndMenu() so an abandoned popup can never block the message loop,
and gate-side popup discovery, introspection, and dismissal.

A popup surfaces in the UIA tree only as an empty Pane with no MenuItem
children, so the menu is read through MN_GETHMENU and the Win32 menu API
against the HMENU the shell handed to TrackPopupMenu. That is live
evidence of what the shell renders, not UIA-tree evidence, and is
labelled as such in the gate, the tests, and the parity ledger.

The gate now asserts the live project menu's ordered items, that Move is
command 5149 with its exact unavailable text and a disabled state, that a
remote project's menu omits Move/Recycle Bin/Explorer, and that the popup
dismisses without wedging the shell. GraphContextMenu.zig and
MainWindow.zig unit tests were never executed by any harness; they now
run in the Windows shell validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
The context-menu assertions were silent on success, so a passing CI run
left no positive record of what the native popup actually contained. Emit
the observed item count, the Move Project item's exact text, enabled flag
and raw MF_ state bits, and the dismissal result into the gate's summary
JSON so the live evidence is auditable from the job log.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
@coneilen
coneilen force-pushed the coneilen-microsoft-context-menu-uia-automation branch from 48cfa6e to 846b663 Compare September 23, 2026 02:23
coneilen added a commit that referenced this pull request Sep 23, 2026
Cherry-picked #420's NativeForms/UpdateOfferPresentation harness wiring
(commit 775d77b) onto this branch: my branch point (68eabe5) predated that
merge, so the reentrancy-guard Assert-Contract, the source-list entry, and
the zig test invocation for UpdateOfferPresentation.zig were all absent
here even though they exist on main. Restored via cherry-pick rather than
a full rebase, per instruction to hold on rebasing until #422 lands.

That cherry-pick alone was not sufficient: this PR's own anti-drift guard
maintains a second, independent file list (\) that #420
never touched (the guard did not exist on main). Newly restoring the
UpdateOfferPresentation.zig invocation without adding it to that list
would have made the guard itself flag it as unwired.

RED: after cherry-picking 775d77b, the guard's \ array
still lacked "UpdateOfferPresentation.zig" -> a manual simulation of the
guard's detection logic reported it as missing alongside the two entries
correctly reserved for #422.
GREEN: added "UpdateOfferPresentation.zig" to \ next to
"NativeForms.zig" -> the same simulation now reports exactly and only
GraphContextMenu.zig and MainWindow.zig as missing, matching the #422
reservation.
REGRESSION: reran the full harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 37 invocations execute
(including the restored NativeForms 96/96 and UpdateOfferPresentation
1/1), the same 3 pre-existing Sidebar.zig failures are tolerated by name,
and the guard still throws on exactly GraphContextMenu.zig, MainWindow.zig.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coneilen
coneilen merged commit 06e092e into main Sep 23, 2026
10 checks passed
coneilen added a commit that referenced this pull request Sep 23, 2026
…nti-drift guard

Fixes #424. WindowsShell.Tests.ps1 hand-maintained a list of zig test
invocations that missed 17 files (Accessibility.zig was fixed separately by
#421; GraphContextMenu.zig/MainWindow.zig are reserved for in-flight #418/
#422). Wires all 17 remaining orphaned files with per-file link flags
verified against pinned Zig 0.15.2, and adds a structural guard that
enumerates graphcode-windows\src\*.zig, detects files containing a 	est "
block, and throws if any is missing from the wired-file list -- so this
drift cannot recur silently.

First-run triage:
- WorktreeDialog.zig: one test used a stale fixture (dirty instead of
  locked) to exercise armConfirmation()'s fail-closed path; sweepSelectable()
  intentionally permits dirty rows. Fixed the test fixture, not the code.
- App.zig: one test's App struct literal predated three fields
  (sidebar_state, declared_entry_ids, kept_worktree_paths) added since it
  last compiled. Fixed the test to match App.init()'s initialization.
- Sidebar.zig: 3 real, pre-existing test failures traced to one root cause
  (layoutFor()/projectSectionHeight() count a recent_projects entry that is
  also the open project, but appendRows() correctly excludes it from
  rendered rows, desyncing row/scroll y-math). Per explicit instruction not
  to modify Sidebar.zig source, these are quarantined at the harness level
  with an explicit reason string, not fixed or deleted. Reported as a real
  product bug for separate follow-up.
- App.zig transitively reruns the same 3 Sidebar failures (it imports
  Sidebar.zig); quarantined identically with a note explaining why.

RED: WorktreeDialog and App.zig tests failed on first run -> both had stale
fixtures, not product bugs; fixed the tests to match current code.
GREEN: all 17 newly-wired files now execute; 90/93 Sidebar.zig and 228/231
App.zig tests pass -> the 3 residual failures are quarantined with cause.
REGRESSION: full harness run end-to-end with pinned Zig 0.15.2 exits 0 and
the anti-drift guard fails loudly on a real mutation removing a wired file
-> guard verified functional, not merely asserted.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…vocation

Moves the structural guard added for #424 to the end of
WindowsShell.Tests.ps1, after all zig test invocations, instead of right
after Resolve-TestZig. Placed first, the guard's expected failure (reserving
GraphContextMenu.zig/MainWindow.zig for #418/#422) short-circuited the whole
script in CI before any of the newly-wired tests ever ran on the actual
runner, leaving only local verification as evidence. Placed last, CI now
executes and reports every invocation for real before the guard's contract
check runs, while the guard still fails the job overall until #418/#422
lands.

RED: with the guard first, CI failed at the guard on the first push and
never exercised a single newly-wired zig test -> no real CI signal existed
for the wiring itself, only local runs.
GREEN: relocated the guard after every Invoke-Native/Invoke-NativeQuarantined
call and reran the full harness locally with pinned Zig 0.15.2 -> all 37
invocations execute (90/93 Sidebar.zig, 228/231 App.zig, everything else
100%), quarantines are tolerated correctly, and the guard still throws last
naming exactly GraphContextMenu.zig and MainWindow.zig.
REGRESSION: reran the same real-mutation guard check (removing a wired file
name) after relocating the block -> guard still throws the identical
not-wired message immediately, confirming the guard's detection logic is
unchanged, only its position in the script moved.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
Cherry-picked #420's NativeForms/UpdateOfferPresentation harness wiring
(commit 775d77b) onto this branch: my branch point (68eabe5) predated that
merge, so the reentrancy-guard Assert-Contract, the source-list entry, and
the zig test invocation for UpdateOfferPresentation.zig were all absent
here even though they exist on main. Restored via cherry-pick rather than
a full rebase, per instruction to hold on rebasing until #422 lands.

That cherry-pick alone was not sufficient: this PR's own anti-drift guard
maintains a second, independent file list (\) that #420
never touched (the guard did not exist on main). Newly restoring the
UpdateOfferPresentation.zig invocation without adding it to that list
would have made the guard itself flag it as unwired.

RED: after cherry-picking 775d77b, the guard's \ array
still lacked "UpdateOfferPresentation.zig" -> a manual simulation of the
guard's detection logic reported it as missing alongside the two entries
correctly reserved for #422.
GREEN: added "UpdateOfferPresentation.zig" to \ next to
"NativeForms.zig" -> the same simulation now reports exactly and only
GraphContextMenu.zig and MainWindow.zig as missing, matching the #422
reservation.
REGRESSION: reran the full harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 37 invocations execute
(including the restored NativeForms 96/96 and UpdateOfferPresentation
1/1), the same 3 pre-existing Sidebar.zig failures are tolerated by name,
and the guard still throws on exactly GraphContextMenu.zig, MainWindow.zig.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…422; correct Sidebar quarantine attribution

Rebased onto origin/main after #422 (issue #418) merged as 06e092e, which
wired GraphContextMenu.zig and MainWindow.zig into the harness's zig test
invocations and source-list array. #422 never touched this PR's own
\ guard array, because that array did not exist on main
when #422 was authored -- it is this PR's own addition. Added both
filenames to the list and updated the guard's explanatory comment, which
was otherwise now stale (still described the two files as reserved and
not-yet-landed).

Also corrected the Sidebar.zig quarantine reason: an independent review
determined that only 2 of the 3 quarantined failures are the real
layoutFor()/appendRows() product bug (#428, fix in flight as #430); the
third (sidebar scroll clamps overflow, shrink, and resize, expected 334
found 410) is a separate, stale test expectation -- the 76px delta is the
Activity block height that contentBottom/paint() correctly account for
and the test's oracle omitted. Left it quarantined (not fixed) since this
PR must not modify Sidebar.zig source, but the reason string now
attributes each failure accurately instead of lumping all three under one
root cause.

RED: mechanically diffed this branch's wired-file set against origin/main
after rebasing -> main had 22 entries (20 plus #422's 2), this branch
still reported only 38 in its own \ guard list, and running
the guard's detection logic directly showed it still flagging
GraphContextMenu.zig and MainWindow.zig as unwired despite their zig test
invocations now existing in the script.
GREEN: added both names to \ and reran the same detection
logic -> zero missing files reported.
REGRESSION: ran the complete harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 39 invocations execute
including the newly-landed GraphContextMenu.zig (8/8) and MainWindow.zig
(6/6, appearing twice via App.zig's transitive import), the same 3
Sidebar.zig failures are tolerated by name with the corrected attribution
text rendering intact, and the full script now exits 0 -- the guard no
longer fires at all.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…nti-drift guard

Fixes #424. WindowsShell.Tests.ps1 hand-maintained a list of zig test
invocations that missed 17 files (Accessibility.zig was fixed separately by
#421; GraphContextMenu.zig/MainWindow.zig are reserved for in-flight #418/
#422). Wires all 17 remaining orphaned files with per-file link flags
verified against pinned Zig 0.15.2, and adds a structural guard that
enumerates graphcode-windows\src\*.zig, detects files containing a 	est "
block, and throws if any is missing from the wired-file list -- so this
drift cannot recur silently.

First-run triage:
- WorktreeDialog.zig: one test used a stale fixture (dirty instead of
  locked) to exercise armConfirmation()'s fail-closed path; sweepSelectable()
  intentionally permits dirty rows. Fixed the test fixture, not the code.
- App.zig: one test's App struct literal predated three fields
  (sidebar_state, declared_entry_ids, kept_worktree_paths) added since it
  last compiled. Fixed the test to match App.init()'s initialization.
- Sidebar.zig: 3 real, pre-existing test failures traced to one root cause
  (layoutFor()/projectSectionHeight() count a recent_projects entry that is
  also the open project, but appendRows() correctly excludes it from
  rendered rows, desyncing row/scroll y-math). Per explicit instruction not
  to modify Sidebar.zig source, these are quarantined at the harness level
  with an explicit reason string, not fixed or deleted. Reported as a real
  product bug for separate follow-up.
- App.zig transitively reruns the same 3 Sidebar failures (it imports
  Sidebar.zig); quarantined identically with a note explaining why.

RED: WorktreeDialog and App.zig tests failed on first run -> both had stale
fixtures, not product bugs; fixed the tests to match current code.
GREEN: all 17 newly-wired files now execute; 90/93 Sidebar.zig and 228/231
App.zig tests pass -> the 3 residual failures are quarantined with cause.
REGRESSION: full harness run end-to-end with pinned Zig 0.15.2 exits 0 and
the anti-drift guard fails loudly on a real mutation removing a wired file
-> guard verified functional, not merely asserted.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…vocation

Moves the structural guard added for #424 to the end of
WindowsShell.Tests.ps1, after all zig test invocations, instead of right
after Resolve-TestZig. Placed first, the guard's expected failure (reserving
GraphContextMenu.zig/MainWindow.zig for #418/#422) short-circuited the whole
script in CI before any of the newly-wired tests ever ran on the actual
runner, leaving only local verification as evidence. Placed last, CI now
executes and reports every invocation for real before the guard's contract
check runs, while the guard still fails the job overall until #418/#422
lands.

RED: with the guard first, CI failed at the guard on the first push and
never exercised a single newly-wired zig test -> no real CI signal existed
for the wiring itself, only local runs.
GREEN: relocated the guard after every Invoke-Native/Invoke-NativeQuarantined
call and reran the full harness locally with pinned Zig 0.15.2 -> all 37
invocations execute (90/93 Sidebar.zig, 228/231 App.zig, everything else
100%), quarantines are tolerated correctly, and the guard still throws last
naming exactly GraphContextMenu.zig and MainWindow.zig.
REGRESSION: reran the same real-mutation guard check (removing a wired file
name) after relocating the block -> guard still throws the identical
not-wired message immediately, confirming the guard's detection logic is
unchanged, only its position in the script moved.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
Cherry-picked #420's NativeForms/UpdateOfferPresentation harness wiring
(commit 775d77b) onto this branch: my branch point (68eabe5) predated that
merge, so the reentrancy-guard Assert-Contract, the source-list entry, and
the zig test invocation for UpdateOfferPresentation.zig were all absent
here even though they exist on main. Restored via cherry-pick rather than
a full rebase, per instruction to hold on rebasing until #422 lands.

That cherry-pick alone was not sufficient: this PR's own anti-drift guard
maintains a second, independent file list (\) that #420
never touched (the guard did not exist on main). Newly restoring the
UpdateOfferPresentation.zig invocation without adding it to that list
would have made the guard itself flag it as unwired.

RED: after cherry-picking 775d77b, the guard's \ array
still lacked "UpdateOfferPresentation.zig" -> a manual simulation of the
guard's detection logic reported it as missing alongside the two entries
correctly reserved for #422.
GREEN: added "UpdateOfferPresentation.zig" to \ next to
"NativeForms.zig" -> the same simulation now reports exactly and only
GraphContextMenu.zig and MainWindow.zig as missing, matching the #422
reservation.
REGRESSION: reran the full harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 37 invocations execute
(including the restored NativeForms 96/96 and UpdateOfferPresentation
1/1), the same 3 pre-existing Sidebar.zig failures are tolerated by name,
and the guard still throws on exactly GraphContextMenu.zig, MainWindow.zig.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…422; correct Sidebar quarantine attribution

Rebased onto origin/main after #422 (issue #418) merged as 06e092e, which
wired GraphContextMenu.zig and MainWindow.zig into the harness's zig test
invocations and source-list array. #422 never touched this PR's own
\ guard array, because that array did not exist on main
when #422 was authored -- it is this PR's own addition. Added both
filenames to the list and updated the guard's explanatory comment, which
was otherwise now stale (still described the two files as reserved and
not-yet-landed).

Also corrected the Sidebar.zig quarantine reason: an independent review
determined that only 2 of the 3 quarantined failures are the real
layoutFor()/appendRows() product bug (#428, fix in flight as #430); the
third (sidebar scroll clamps overflow, shrink, and resize, expected 334
found 410) is a separate, stale test expectation -- the 76px delta is the
Activity block height that contentBottom/paint() correctly account for
and the test's oracle omitted. Left it quarantined (not fixed) since this
PR must not modify Sidebar.zig source, but the reason string now
attributes each failure accurately instead of lumping all three under one
root cause.

RED: mechanically diffed this branch's wired-file set against origin/main
after rebasing -> main had 22 entries (20 plus #422's 2), this branch
still reported only 38 in its own \ guard list, and running
the guard's detection logic directly showed it still flagging
GraphContextMenu.zig and MainWindow.zig as unwired despite their zig test
invocations now existing in the script.
GREEN: added both names to \ and reran the same detection
logic -> zero missing files reported.
REGRESSION: ran the complete harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 39 invocations execute
including the newly-landed GraphContextMenu.zig (8/8) and MainWindow.zig
(6/6, appearing twice via App.zig's transitive import), the same 3
Sidebar.zig failures are tolerated by name with the corrected attribution
text rendering intact, and the full script now exits 0 -- the guard no
longer fires at all.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
coneilen added a commit that referenced this pull request Sep 23, 2026
…-drift guard (#426)

* Wire 17 orphaned Zig test files into WindowsShell.Tests.ps1 and add anti-drift guard

Fixes #424. WindowsShell.Tests.ps1 hand-maintained a list of zig test
invocations that missed 17 files (Accessibility.zig was fixed separately by
#421; GraphContextMenu.zig/MainWindow.zig are reserved for in-flight #418/
#422). Wires all 17 remaining orphaned files with per-file link flags
verified against pinned Zig 0.15.2, and adds a structural guard that
enumerates graphcode-windows\src\*.zig, detects files containing a 	est "
block, and throws if any is missing from the wired-file list -- so this
drift cannot recur silently.

First-run triage:
- WorktreeDialog.zig: one test used a stale fixture (dirty instead of
  locked) to exercise armConfirmation()'s fail-closed path; sweepSelectable()
  intentionally permits dirty rows. Fixed the test fixture, not the code.
- App.zig: one test's App struct literal predated three fields
  (sidebar_state, declared_entry_ids, kept_worktree_paths) added since it
  last compiled. Fixed the test to match App.init()'s initialization.
- Sidebar.zig: 3 real, pre-existing test failures traced to one root cause
  (layoutFor()/projectSectionHeight() count a recent_projects entry that is
  also the open project, but appendRows() correctly excludes it from
  rendered rows, desyncing row/scroll y-math). Per explicit instruction not
  to modify Sidebar.zig source, these are quarantined at the harness level
  with an explicit reason string, not fixed or deleted. Reported as a real
  product bug for separate follow-up.
- App.zig transitively reruns the same 3 Sidebar failures (it imports
  Sidebar.zig); quarantined identically with a note explaining why.

RED: WorktreeDialog and App.zig tests failed on first run -> both had stale
fixtures, not product bugs; fixed the tests to match current code.
GREEN: all 17 newly-wired files now execute; 90/93 Sidebar.zig and 228/231
App.zig tests pass -> the 3 residual failures are quarantined with cause.
REGRESSION: full harness run end-to-end with pinned Zig 0.15.2 exits 0 and
the anti-drift guard fails loudly on a real mutation removing a wired file
-> guard verified functional, not merely asserted.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Run the anti-drift guard last so CI still validates every zig test invocation

Moves the structural guard added for #424 to the end of
WindowsShell.Tests.ps1, after all zig test invocations, instead of right
after Resolve-TestZig. Placed first, the guard's expected failure (reserving
GraphContextMenu.zig/MainWindow.zig for #418/#422) short-circuited the whole
script in CI before any of the newly-wired tests ever ran on the actual
runner, leaving only local verification as evidence. Placed last, CI now
executes and reports every invocation for real before the guard's contract
check runs, while the guard still fails the job overall until #418/#422
lands.

RED: with the guard first, CI failed at the guard on the first push and
never exercised a single newly-wired zig test -> no real CI signal existed
for the wiring itself, only local runs.
GREEN: relocated the guard after every Invoke-Native/Invoke-NativeQuarantined
call and reran the full harness locally with pinned Zig 0.15.2 -> all 37
invocations execute (90/93 Sidebar.zig, 228/231 App.zig, everything else
100%), quarantines are tolerated correctly, and the guard still throws last
naming exactly GraphContextMenu.zig and MainWindow.zig.
REGRESSION: reran the same real-mutation guard check (removing a wired file
name) after relocating the block -> guard still throws the identical
not-wired message immediately, confirming the guard's detection logic is
unchanged, only its position in the script moved.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Point quarantine reason at issue #428 instead of #424

The three Sidebar.zig quarantine entries pointed at #424 (this PR's own
tracking issue) with the phrase "first-run finding" as a stand-in for a
real bug report, since no dedicated issue existed yet for the Sidebar
layout bug when it was first quarantined. The coordinating session filed
#428 with the full root-cause writeup (layoutFor()/projectSectionHeight()
vs appendRows()'s isProjectOpen skip, the 76px delta, and the hit-testing
failure). Repointing the quarantine reason string at #428 so it resolves
to the actual bug report instead of this wiring PR.

RED: quarantine reason string referenced #424, which is this very PR and
not a bug report -> anyone reading the quarantine message would have to
guess where the real Sidebar.zig fix should land.
GREEN: repointed both Sidebar.zig and App.zig quarantine reason strings to
#428 and reran the full harness locally with pinned Zig 0.15.2 -> same
90/93 Sidebar.zig and 228/231 App.zig pass counts, guard still fails last on
exactly GraphContextMenu.zig and MainWindow.zig, exit code unchanged.
REGRESSION: confirmed Invoke-NativeQuarantined's known-failure matching is
by test name string only, independent of the reason text -> the wording
change cannot affect which failures are tolerated.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add UpdateOfferPresentation.zig to the guard's wired-file list

Cherry-picked #420's NativeForms/UpdateOfferPresentation harness wiring
(commit 775d77b) onto this branch: my branch point (68eabe5) predated that
merge, so the reentrancy-guard Assert-Contract, the source-list entry, and
the zig test invocation for UpdateOfferPresentation.zig were all absent
here even though they exist on main. Restored via cherry-pick rather than
a full rebase, per instruction to hold on rebasing until #422 lands.

That cherry-pick alone was not sufficient: this PR's own anti-drift guard
maintains a second, independent file list (\) that #420
never touched (the guard did not exist on main). Newly restoring the
UpdateOfferPresentation.zig invocation without adding it to that list
would have made the guard itself flag it as unwired.

RED: after cherry-picking 775d77b, the guard's \ array
still lacked "UpdateOfferPresentation.zig" -> a manual simulation of the
guard's detection logic reported it as missing alongside the two entries
correctly reserved for #422.
GREEN: added "UpdateOfferPresentation.zig" to \ next to
"NativeForms.zig" -> the same simulation now reports exactly and only
GraphContextMenu.zig and MainWindow.zig as missing, matching the #422
reservation.
REGRESSION: reran the full harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 37 invocations execute
(including the restored NativeForms 96/96 and UpdateOfferPresentation
1/1), the same 3 pre-existing Sidebar.zig failures are tolerated by name,
and the guard still throws on exactly GraphContextMenu.zig, MainWindow.zig.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Wire GraphContextMenu.zig/MainWindow.zig into the guard's list after #422; correct Sidebar quarantine attribution

Rebased onto origin/main after #422 (issue #418) merged as 06e092e, which
wired GraphContextMenu.zig and MainWindow.zig into the harness's zig test
invocations and source-list array. #422 never touched this PR's own
\ guard array, because that array did not exist on main
when #422 was authored -- it is this PR's own addition. Added both
filenames to the list and updated the guard's explanatory comment, which
was otherwise now stale (still described the two files as reserved and
not-yet-landed).

Also corrected the Sidebar.zig quarantine reason: an independent review
determined that only 2 of the 3 quarantined failures are the real
layoutFor()/appendRows() product bug (#428, fix in flight as #430); the
third (sidebar scroll clamps overflow, shrink, and resize, expected 334
found 410) is a separate, stale test expectation -- the 76px delta is the
Activity block height that contentBottom/paint() correctly account for
and the test's oracle omitted. Left it quarantined (not fixed) since this
PR must not modify Sidebar.zig source, but the reason string now
attributes each failure accurately instead of lumping all three under one
root cause.

RED: mechanically diffed this branch's wired-file set against origin/main
after rebasing -> main had 22 entries (20 plus #422's 2), this branch
still reported only 38 in its own \ guard list, and running
the guard's detection logic directly showed it still flagging
GraphContextMenu.zig and MainWindow.zig as unwired despite their zig test
invocations now existing in the script.
GREEN: added both names to \ and reran the same detection
logic -> zero missing files reported.
REGRESSION: ran the complete harness end-to-end locally with pinned Zig
0.15.2 and GRAPHCODE_WINGHOSTTY_ROOT set -> all 39 invocations execute
including the newly-landed GraphContextMenu.zig (8/8) and MainWindow.zig
(6/6, appearing twice via App.zig's transitive import), the same 3
Sidebar.zig failures are tolerated by name with the corrected attribution
text rendering intact, and the full script now exits 0 -- the guard no
longer fires at all.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Remove Sidebar/App.zig quarantine now that #430 fixed the underlying bug

PR #430 (merged as ece5935) fixed the real Sidebar.zig layout/row-count
desync structurally via a single shared predicate,
projectIsVisibleInSection, so all three previously-quarantined tests
now pass unconditionally. Landing the quarantine would have shipped a
misleading tolerate-list for an already-fixed bug, so remove it
entirely instead:

- Delete Invoke-NativeQuarantined and its Get-FailingZigTestNames
  helper (Sidebar.zig/App.zig were their only consumers).
- Delete the sidebarLayoutOpenProjectKnownFailures/-Reason variables.
- Convert both call sites to plain Invoke-Native.

Verified end-to-end locally with pinned Zig 0.15.2: exit 0, all 93
Sidebar.zig tests and the full 232-test App.zig suite pass
unconditionally, zero quarantine output. Re-ran the guard's
set-difference check against origin/main (ece5935): empty, with
UpdateOfferPresentation.zig and the NativeForms Assert-Contract block
both still intact.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

Windows UIA gate cannot open or inspect native context menus (TrackPopupMenu)

1 participant