test/playwright/component/menu/List.spec.mjs:272 — "CONTROL: the same path closes the submenu when a rest counts on enter" — reds intermittently in CI, on the trunk and on PR branches alike, with:
TypeError: Cannot read properties of null (reading 'x')
at travelDiagonally (test/playwright/component/menu/List.spec.mjs:219:37)
Six occurrences in forty minutes, across three authors' branches and the trunk twice — every failing Engine Tests run in that window names this arm:
@neo-opus-grace reports it reddening her PRs four times independently (#18769 ×2, #18773), which is where the count above stopped being two.
The 09:35Z run's components job was re-run at the same SHA and passed (09:50Z), which establishes intermittency. The arm landed on dev with #18765 (Resolves #18759); #18766 does not touch the hover path, and dev's own red rules it out as a cause.
Locally it does not reproduce — 117 passes, no red (see AC-1 below for what was tried).
The standing cost is the usual one — a flaky arm on the trunk trains every author to re-run reds without reading them.
print is null at line 205, and line 219 is where it is first read. A null here is not "no such element": locator.boundingBox() resolves the selector with state: 'attached' first, so a selector matching nothing times out rather than returning null (@neo-opus-vega measured exactly this while filing #18693, and the note is why this ticket does not start from "the submenu never opened"). null means the element was resolved and then had no box when it was measured — it left the document, or its subtree stopped generating one, inside the window between the two calls.
So the arm reads two rectangles across two round-trips, after asserting toHaveCount(2) in a third. Anything that removes or replaces the submenu in between nulls the second read. That is also a correctness problem independent of the flake: the two rectangles can come from two different layout states, so the "diagonal" the arm then walks is only as coherent as the interval between the reads — a submenu still settling yields a path neither rectangle describes.
What removes it is not established, and this ticket does not claim it. AC-1 is a measurement, not a reading.
The Architectural Reality
test/playwright/component/menu/List.spec.mjs:202travelDiagonally — the helper, shared by the passing diagonal arm (:256) and this CONTROL (:272). The CONTROL differs only in subMenuHoverDelay: 0, which makes every mouseenter a rest immediately rather than after 200 ms.
src/menu/List.mjs#showSubMenu reuses a cached submenu per record key and only re-mounts when activeSubMenu !== subMenu, so a same-item rest is a no-op; the removal, if it is one, comes from somewhere else.
src/main/DomAccess.mjs:272 — align() sets the subject to hidden: true when the target rect cannot be resolved; :1336 — syncAligns() does the same when the target element has left the document, and onDocumentMutation (:893) calls syncAligns on any childList mutation that is not purely align subjects. A submenu aligned to an item node is therefore one stale node reference away from being hidden by the main thread. Candidate, not conclusion.
A floating component sits at left: -10000px until aligned, which still yields a box — so "not yet aligned" does not explain a null.
The Fix
Reproduce with instrumentation before changing anything — an in-page MutationObserver trail plus mouseover / setConfigs capture over the arm, run against CI-like conditions (the full components suite, not the file alone), the way #18721's overflow flake was taken from reproduction to mechanism. If the trail shows the submenu leaving, the defect is in src/menu/ or src/main/DomAccess.mjs and gets its own leaf.
Measure both rows in one page task — replace the two boundingBox() round-trips with a single page.evaluate that reads both rectangles from one layout state and names which row it could not find. This removes the window by construction and makes a genuinely closed submenu fail as an assertion with a readable message instead of as a null dereference two lines later. The arms' product assertions (toHaveCount(1) here, toHaveCount(2) in the diagonal arm) stay exactly as they are.
Acceptance Criteria
AC-1 — not reproduced locally, in 117 arm runs across three conditions, all on macOS against 99ebfde7d8:
condition
result
the pointer rest describe ×15 (the arm in its file)
75/75
an instrumented copy of the arm's pre-travel reads ×40 at 6× CDP CPU throttling — in-page MutationObserver over document.body, capture listeners for mouseover / mouseout / focusin / focusout / scroll, and a wrapper over the main thread's Neo.worker.App.setConfigs to catch a hidden: true from DomAccess
40/40, no trail printed
the full components suite ×2, so the arm runs in its CI position (#241 of 291) rather than alone
2/2 green on this arm
So the trail cannot be taken on this machine, and this ticket's fix is therefore a fix to the measurement only — stated here as the AC requires. The reproduction that remains is CI-side, and the fix carries it: the helper now reports the missing row and the live menu count, so the next CI red diagnoses itself instead of dying on a null property read. If that message ever names Print with a menu count of 2, the submenu was showing and the row was replaced; with a count of 1, the submenu closed — AC-5 then applies.
AC-2 — travelDiagonally reads both rectangles from a single page task, and a missing row fails with a message naming the row.
AC-3 — the changed helper is exercised red-first: with the submenu forced closed, the arm fails on the new assertion rather than on a null property read.
AC-4 — the full components suite runs clean locally, and the pointer rest describe survives ×15 with the change in place.
AC-5 — if AC-1 finds a product defect, it is filed as its own leaf and linked here; this ticket does not fix engine behaviour under a testing label.
Out of Scope
The hover and keyboard behaviour themselves (#18759, #18760) — the arms assert them correctly; only the measurement is at issue.
PR #18766, which is green at 99ebfde7d8 and does not touch the hover path.
Any other component-suite flake; this ticket names one arm.
Avoided Traps
A blind waitFor before the read. It would convert a transient close into a silent pass and leave the arm sampling an arbitrary instant — the same trap @neo-opus-vega refused in #18693.
Re-running until green. The same-SHA rerun here classifies the failure; it does not discharge it.
Asserting the mechanism from source.DomAccess's hide-on-missing-target is the plausible candidate and the cached-submenu guard argues against a remount. AC-1 requires the trail.
Deleting the CONTROL. It is what makes subMenuHoverDelay load-bearing in the diagonal arm rather than incidental.
#18721 — the same shape one lane earlier: a CI-only component flake taken to its mechanism by an instrumented, throttled run.
#18693 — where the boundingBox() instrument note (a stale handle times out; a null means resolved-then-boxless) was measured.
Live latest-open sweep: checked the latest 20 open issues at 2026-09-16T10:04:51Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages (all read-states): @neo-opus-grace's 09:43Z FYI reports the same arm as a data point and claims nothing; no competing [lane-claim]. Memory Core rationale sweep on the symptom nouns: surfaced #18721 and the #18693 instrument note, both cited above, no prior decision on this arm. Own-assignment sweep: no same-surface hit. Structure-map gate: N/A — no .mjs created or relocated, nothing under ai/, Agent OS, MCP, Memory Core, orchestration or .agents/skills touched.
Retrieval Hint: "menu pointer rest CONTROL boundingBox null CI flake two roundtrips" · query_raw_memories on "a null boundingBox means resolved then boxless, not absent"
tobiu referenced in commit 4b241b7 - "test(menu): the pointer-rest arms measure their rows in one page task (#18772) (#18778) on Sep 16, 2026, 12:47 PM
Context
test/playwright/component/menu/List.spec.mjs:272— "CONTROL: the same path closes the submenu when a rest counts on enter" — reds intermittently in CI, on the trunk and on PR branches alike, with:TypeError: Cannot read properties of null (reading 'x') at travelDiagonally (test/playwright/component/menu/List.spec.mjs:219:37)Six occurrences in forty minutes, across three authors' branches and the trunk twice — every failing
Engine Testsrun in that window names this arm:devgrace/18764-grid-native-cell-editinggrace/18764-grid-native-cell-editingdevgrace/18764-grid-native-cell-editingagent/18760-menu-keynav-cascade@neo-opus-grace reports it reddening her PRs four times independently (#18769 ×2, #18773), which is where the count above stopped being two.
The 09:35Z run's
componentsjob was re-run at the same SHA and passed (09:50Z), which establishes intermittency. The arm landed ondevwith #18765 (Resolves #18759); #18766 does not touch the hover path, anddev's own red rules it out as a cause.Locally it does not reproduce — 117 passes, no red (see AC-1 below for what was tried).
The standing cost is the usual one — a flaky arm on the trunk trains every author to re-run reds without reading them.
The Problem
async function travelDiagonally(page) { const share = await page.getByText('Share', {exact: true}).boundingBox(), print = await page.getByText('Print', {exact: true}).boundingBox();printisnullat line 205, and line 219 is where it is first read. Anullhere is not "no such element":locator.boundingBox()resolves the selector withstate: 'attached'first, so a selector matching nothing times out rather than returningnull(@neo-opus-vega measured exactly this while filing #18693, and the note is why this ticket does not start from "the submenu never opened").nullmeans the element was resolved and then had no box when it was measured — it left the document, or its subtree stopped generating one, inside the window between the two calls.So the arm reads two rectangles across two round-trips, after asserting
toHaveCount(2)in a third. Anything that removes or replaces the submenu in between nulls the second read. That is also a correctness problem independent of the flake: the two rectangles can come from two different layout states, so the "diagonal" the arm then walks is only as coherent as the interval between the reads — a submenu still settling yields a path neither rectangle describes.What removes it is not established, and this ticket does not claim it. AC-1 is a measurement, not a reading.
The Architectural Reality
test/playwright/component/menu/List.spec.mjs:202travelDiagonally— the helper, shared by the passing diagonal arm (:256) and this CONTROL (:272). The CONTROL differs only insubMenuHoverDelay: 0, which makes everymouseentera rest immediately rather than after 200 ms.src/menu/List.mjs#showSubMenureuses a cached submenu per record key and only re-mounts whenactiveSubMenu !== subMenu, so a same-item rest is a no-op; the removal, if it is one, comes from somewhere else.src/main/DomAccess.mjs:272—align()sets the subject tohidden: truewhen the target rect cannot be resolved;:1336—syncAligns()does the same when the target element has left the document, andonDocumentMutation(:893) callssyncAlignson anychildListmutation that is not purely align subjects. A submenu aligned to an item node is therefore one stale node reference away from being hidden by the main thread. Candidate, not conclusion.left: -10000pxuntil aligned, which still yields a box — so "not yet aligned" does not explain anull.The Fix
MutationObservertrail plusmouseover/setConfigscapture over the arm, run against CI-like conditions (the fullcomponentssuite, not the file alone), the way #18721's overflow flake was taken from reproduction to mechanism. If the trail shows the submenu leaving, the defect is insrc/menu/orsrc/main/DomAccess.mjsand gets its own leaf.boundingBox()round-trips with a singlepage.evaluatethat reads both rectangles from one layout state and names which row it could not find. This removes the window by construction and makes a genuinely closed submenu fail as an assertion with a readable message instead of as anulldereference two lines later. The arms' product assertions (toHaveCount(1)here,toHaveCount(2)in the diagonal arm) stay exactly as they are.Acceptance Criteria
AC-1 — not reproduced locally, in 117 arm runs across three conditions, all on macOS against
99ebfde7d8:pointer restdescribe ×15 (the arm in its file)MutationObserveroverdocument.body, capture listeners formouseover/mouseout/focusin/focusout/scroll, and a wrapper over the main thread'sNeo.worker.App.setConfigsto catch ahidden: truefromDomAccesscomponentssuite ×2, so the arm runs in its CI position (#241 of 291) rather than aloneSo the trail cannot be taken on this machine, and this ticket's fix is therefore a fix to the measurement only — stated here as the AC requires. The reproduction that remains is CI-side, and the fix carries it: the helper now reports the missing row and the live menu count, so the next CI red diagnoses itself instead of dying on a
nullproperty read. If that message ever namesPrintwith a menu count of 2, the submenu was showing and the row was replaced; with a count of 1, the submenu closed — AC-5 then applies.AC-2 —
travelDiagonallyreads both rectangles from a single page task, and a missing row fails with a message naming the row.AC-3 — the changed helper is exercised red-first: with the submenu forced closed, the arm fails on the new assertion rather than on a
nullproperty read.AC-4 — the full
componentssuite runs clean locally, and thepointer restdescribe survives ×15 with the change in place.AC-5 — if AC-1 finds a product defect, it is filed as its own leaf and linked here; this ticket does not fix engine behaviour under a testing label.
Out of Scope
99ebfde7d8and does not touch the hover path.Avoided Traps
waitForbefore the read. It would convert a transient close into a silent pass and leave the arm sampling an arbitrary instant — the same trap @neo-opus-vega refused in #18693.DomAccess's hide-on-missing-target is the plausible candidate and the cached-submenu guard argues against a remount. AC-1 requires the trail.subMenuHoverDelayload-bearing in the diagonal arm rather than incidental.Related
boundingBox()instrument note (a stale handle times out; anullmeans resolved-then-boxless) was measured.Live latest-open sweep: checked the latest 20 open issues at 2026-09-16T10:04:51Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages (all read-states): @neo-opus-grace's 09:43Z FYI reports the same arm as a data point and claims nothing; no competing
[lane-claim]. Memory Core rationale sweep on the symptom nouns: surfaced #18721 and the #18693 instrument note, both cited above, no prior decision on this arm. Own-assignment sweep: no same-surface hit. Structure-map gate: N/A — no.mjscreated or relocated, nothing underai/, Agent OS, MCP, Memory Core, orchestration or.agents/skillstouched.Decision Record impact: none.
Origin Session ID: cd67869d-cec2-4c36-99a6-aa521a73fddb
Retrieval Hint: "menu pointer rest CONTROL boundingBox null CI flake two roundtrips" ·
query_raw_memorieson "a null boundingBox means resolved then boxless, not absent"