Skip to content

fuzz: arm the terminal in morph, and hold it to one switch per chord - #760

Merged
deblasis merged 2 commits into
windowsfrom
fuzz/morph-arm-terminal
Aug 26, 2026
Merged

fuzz: arm the terminal in morph, and hold it to one switch per chord#760
deblasis merged 2 commits into
windowsfrom
fuzz/morph-arm-terminal

Conversation

@deblasis

@deblasis deblasis commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Follows #758, now merged, and rebased onto it.

#758 made morph report a run that switched nothing. This is why it was switching so little.

morph sent its layout chords into an unarmed XAML island. The island drops synthesized keys until a real click lands on its own pixels, and #752 fixed that for the mouse-fuzz harnesses without reaching this one. The cost was invisible because nothing measured it: the harness kept sending, the app kept ignoring, and the oracle only ever read what did happen.

Measured against a clean build it converted 38% to 52% of the chords it sent - 13, 14 and 20 of 34, and 16 of 31. Run outside the suite, which minimizes other windows first, it reliably converted none at all: the startup probe reached a window that had never been clicked and traced no switch whatsoever.

So arm before every chord, the same shape as Enable-Chords in mouse-fuzz-tab-close-selection.ps1. Three quarters across and down lands in the terminal under either layout; the window center only does while the strip is narrow, and this harness spends its run changing that. The ad-hoc re-arm click the tab-color branch already carried becomes a call to the same helper.

What the floor can now say

seed chords begins before begins after
1337 34 13, 14, 20 49, 50
4242 26 probe traced nothing 39
777 31 16 51

1.44 to 1.65 switches per chord, so $MinBeginRatio moves from a third to one: every layout chord begins a switch. The surplus above 1 is chords that arrived mid-switch, held as _pendingLayoutTarget in MainWindow.AnimateTabLayoutTo and replayed on completion.

The margin is the surplus and it is not guaranteed - two chords inside one switch share the single pending slot, so the second is lost - which is why the floor is exactly 1 rather than something above it.

The tail settle had to change with it

Three times as many switches means the last one is far more likely to still be in the air when the finally kills the process, and the verdict reads the trace afterwards. A fixed 1200ms started reporting a begin with no end as switch begin/end mismatch ... (a switch never finished) - a false exit 2. It polls for balance now, which also returns sooner than the sleep did in the ordinary case.

Validation

check result
just fuzz "-Only morph" pass, exit 0 - 34 chords, 50 begins, 50 ends, floor 34
just fuzz-selftest SELFTEST OK, exit 0
just fuzz-list exit 0
three seeds through the suite 49/34, 39/26, 51/31, all balanced, all pass

It also passes on the first attempt now. Every earlier suite run needed the retry, because the first attempt failed the startup probe.

Noticed, not addressed

The trace shows consecutive SWITCH begin vertical=True pairs with no end between them, so the app can animate a switch to the layout it is already showing. AnimateTabLayoutTo guards on _verticalTabsVisible == vertical at the OnConfigChanged call site but not on the _pendingLayoutTarget replay path. It is benign - ghosts=0 throughout and every begin is answered - but it is a 340ms animation and a morph cycle for a no-op. Worth its own look, not this PR.

Review round

Reviewed at high effort before merge; three findings, all fixed in fuzz: measure the morph floor against the chords that landed.

  • The floor divided by every chord attempted, including ones Chord() refused and never delivered. Harmless at the previous ratio of a third; at one it charges a foreground steal to the build and reports it as "the router never saw them" about chords that were never sent. It divides by the delivered ones now, which is only safe because the miss cap above has already rejected a run that lost most of them.
  • Enable-Chords throws where the click it replaced was best-effort, and it sat inside the tab-color try/catch - so a refused arming click landed as one more tab-color-error and left the island unarmed, with the iterations after it sending chords nobody handled. Hoisted out of the catch.
  • Rect swallows GetWindowRect's return, so a window that had gone away came back as zeros and aimed the arming click at the top-left of the screen, reporting a foreground miss for a window that no longer existed. Checked before it is measured, and reported as PRODUCT_FAIL to match the caller's own convention.

morph sent its layout chords into an unarmed XAML island. The island
drops synthesized keys until a real click lands on its own pixels, and
#752 fixed that for the mouse-fuzz harnesses without reaching this one.
The cost was invisible because nothing measured it: the harness kept
sending, the app kept ignoring, and the oracle only ever read what did
happen.

Measured against a clean build, it converted 38% to 52% of the chords it
sent - 13, 14 and 20 of 34, and 16 of 31. Run outside the suite, which
minimizes other windows first, it reliably converted none at all: the
startup probe reached a window that had never been clicked and traced no
switch whatsoever.

So arm before every chord, the same shape as Enable-Chords in
mouse-fuzz-tab-close-selection.ps1. Three quarters across and down lands
in the terminal under either layout; the window center only does while
the strip is narrow, and this harness spends its run changing that. The
ad-hoc re-arm click the tab-color branch already carried becomes a call
to the same helper.

It now converts 1.44 to 1.65 switches per chord - 49 of 34, 39 of 26 and
51 of 31 - so the floor moves from a third to one. That is a statement
worth making: every layout chord begins a switch. The surplus above 1 is
chords that arrived mid-switch, held as _pendingLayoutTarget and
replayed on completion.

The tail settle is waited on rather than slept through. Three times as
many switches means the last one is far more likely to still be in the
air when the finally kills the process, and the verdict reads the trace
afterwards, so a fixed 1200ms started reporting a begin with no end as a
switch that never finished. It polls for balance instead, which also
returns sooner than the sleep did in the ordinary case.
Review of the floor at a ratio of one turned up three ways it blames the
wrong thing.

The floor divided by every chord attempted, including the ones Chord()
refused and never delivered. At a third that slack was harmless; at one
it charges a foreground steal to the build and then reports it as "the
chords went out but the router never saw them" about chords that were
never sent. Divide by the delivered ones instead. That is only a safe
denominator because the miss cap above has already rejected a run that
lost most of them - on its own, every chord refused would leave zero
delivered and a floor of nothing, which is the hole the attempted count
was there to close.

Arming moved out of the tab-color catch. Enable-Chords throws where the
click it replaced was best-effort, so a refused arming click was landing
as one more 'tab-color-error' and leaving the island unarmed. The
iterations after it would send chords nobody handled - none of them
asserted on - and the run would go quietly useless until the next toggle
threw.

And Enable-Chords now checks the window before it measures it. Rect
swallows GetWindowRect's return, so a window that has gone away comes
back as zeros, which aimed the click at the top-left of the screen and
reported a foreground miss for a window that no longer existed. The
caller already treats that as PRODUCT_FAIL; say so here too.
@deblasis
deblasis force-pushed the fuzz/morph-arm-terminal branch from ca12265 to 3a9d481 Compare August 26, 2026 12:08
@deblasis
deblasis changed the base branch from fuzz/morph-run-floor to windows August 26, 2026 12:08
@deblasis
deblasis merged commit 52cc83f into windows Aug 26, 2026
110 checks passed
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