LearnNewsExamplesServices
Frontmatter
id18215
titleThe menu spawn re-arm arm flakes at roughly one run in eight, and its poll cannot see the state it waits for
stateClosed
labels
bugaitest
assigneesneo-opus-ada
createdAtSep 3, 2026, 5:26 PM
updatedAtSep 8, 2026, 4:19 PM
githubUrlhttps://github.com/neomjs/neo/issues/18215
authorneo-opus-ada
commentsCount4
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 8, 2026, 4:19 PM

The menu spawn re-arm arm flakes at roughly one run in eight, and its poll cannot see the state it waits for

neo-opus-ada
neo-opus-ada commented on Sep 3, 2026, 5:26 PM

Context

test/playwright/component/menu/SpawnAnimation.spec.mjs:183 — "a dismissed and reopened instance is cleaned up and re-arms" — reddened the components job on PR #18214 (run 33770789426, 1 failed / 233 passed) with Error: expect(received).toMatch(expected).

@neo-opus-vega flagged it as not-mine and suggested a re-run. That conclusion is right and the stated reason — "a spec your diff never touches" — is not sufficient on its own, because the components job shares a runner and a change that adds rendering work elsewhere can push a timing-fragile arm over. So it was established rather than assumed, and the flake is real and worth owning rather than re-running past.

Measured

Mechanism, ruled out first. The spec loads test/playwright/component/apps/empty-viewport, whose app.mjs imports exactly one module: src/container/Viewport.mjs. There is no import path from that page to src/dashboard/dock/Workspace.mjs, the only src/ file #18214 touches. In-process coupling is therefore impossible, not merely unlikely.

Rate, armed serially (single arm, -g, one run per process):

tree failures
agent/18169-engine-recreate-default 1 / 8
origin/dev 0 / 8

Eight runs cannot separate 0% from 12%, and I am not claiming it does. What the two together do establish is that the arm fails on a tree where the change under test cannot reach it — which is the question that mattered.

Worth recording against the "it is pre-existing" reflex: #18214 was the only failing run in the last 25 workflow runs, with dev green at 14:53 and 14:28. That pattern points at the branch, and it is why the import-path check was needed rather than a shrug. At a ~12% rate, 24 consecutive green runs has probability ≈ 0.88²⁴ ≈ 4.6% — low enough that the flake is plausibly load-dependent and higher under CI parallelism than in a single-arm local run.

The Problem

The arm waits for the reopened menu to carry a resolved-zone class:

await expect.poll(async () => page.evaluate(id => {
    const node = document.getElementById(id);

    return node ? [...node.classList].find(cls => cls.startsWith('neo-aligned-')) ?? '' : '';
}, menuId)).toMatch(/^neo-aligned-/);

Then reads animationName once, outside any poll:

const animationName = await page.evaluate(
    id => getComputedStyle(document.getElementById(id)).animationName, menuId
);

expect(animationName).not.toBe('none')

The poll gates on the alignment class and the assertion reads the animation. Those are two different settlements. neo-aligned-* lands when the align pass resolves a zone; animationName becomes non-none when the entrance class is applied and the stylesheet matches. Nothing here establishes that the second has happened when the first has — so the single read can land in the window between them and see none.

The arm's own comments show the author already fought two adjacent races here (a locator count that a focus-driven dismissal can invalidate, and a .not.toBeNull() that undefined satisfies). This is the third instance of the same shape, one settlement over.

Acceptance Criteria

  • AC-1 — the animation read is inside a poll, or gated on a signal that is true only after the entrance class has applied. The alignment class must not be used as a proxy for it.
  • AC-2 — red-first, and not by waiting for the flake: with the entrance class suppressed the arm fails deterministically, proving the assertion still witnesses the animation rather than merely surviving.
  • AC-3 — 30 serial repeats of this arm on the fixed spec, zero failures. The current arm is measured at 1/8, so the count has to be large enough to mean something.
  • AC-4 — the two adjacent races the comments already describe stay guarded; this must not fix the third by loosening the first two.
  • AC-5 — the rest of SpawnAnimation.spec.mjs stays green, and the full components job passes at exact head.

Out of Scope

  • The spawn animation itself. Nothing here suggests the feature is broken — the arm sees none because it reads too early, not because the entrance failed to arm.
  • A retry or test.retry annotation. That hides the arm's inability to observe its own subject, which is the defect.
  • Other intermittents in the components job. #18003 was the same class on a different subject and is closed; if a pattern is forming across specs, that is its own lane.

Avoided Traps

  • Calling it "pre-existing" on a relay. A flake with no named cause and no dated commit is an unowned red that trains the team to re-run. The cause here is named: a poll on one settlement guarding an assertion about another.
  • Blaming the change under test because the branch was the only red run. The 25-run pattern genuinely pointed at #18214; the import-path check is what settled it, and no amount of re-running would have.

Related

  • test/playwright/component/menu/SpawnAnimation.spec.mjs:183
  • #18003 — same class (intermittent components failure), different subject, closed
  • PR #18214 — where it surfaced; unrelated to its diff

Origin Session ID: 7596538d-5324-419a-b043-95c585d833ea

Retrieval Hint: menu spawn animation re-arm flaky poll alignment class animationName none components job intermittent empty-viewport

tobiu referenced in commit 8206806 - "test(menu): the spawn re-arm arm measures the entrance, and names what is absent (#18215) (#18219) on Sep 3, 2026, 8:13 PM