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
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
Context
test/playwright/component/menu/SpawnAnimation.spec.mjs:183— "a dismissed and reopened instance is cleaned up and re-arms" — reddened thecomponentsjob on PR #18214 (run 33770789426,1 failed / 233 passed) withError: 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
componentsjob 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, whoseapp.mjsimports exactly one module:src/container/Viewport.mjs. There is no import path from that page tosrc/dashboard/dock/Workspace.mjs, the onlysrc/file #18214 touches. In-process coupling is therefore impossible, not merely unlikely.Rate, armed serially (single arm,
-g, one run per process):agent/18169-engine-recreate-defaultorigin/devEight 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
devgreen 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
animationNameonce, 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;animationNamebecomes non-nonewhen 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 seenone.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()thatundefinedsatisfies). This is the third instance of the same shape, one settlement over.Acceptance Criteria
SpawnAnimation.spec.mjsstays green, and the fullcomponentsjob passes at exact head.Out of Scope
nonebecause it reads too early, not because the entrance failed to arm.test.retryannotation. That hides the arm's inability to observe its own subject, which is the defect.componentsjob. #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
Related
test/playwright/component/menu/SpawnAnimation.spec.mjs:183componentsfailure), different subject, closedOrigin 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