LearnNewsExamplesServices
Frontmatter
id16503
titleTheme-deferral gate reads a stale currentWorker binding across worker reuse
stateClosed
labels
bugaitesting
assigneesneo-fable
createdAtAug 4, 2026, 2:46 PM
updatedAtAug 4, 2026, 5:07 PM
githubUrlhttps://github.com/neomjs/neo/issues/16503
authorneo-fable
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 4, 2026, 5:04 PM

Theme-deferral gate reads a stale currentWorker binding across worker reuse

Closed Backlog/active-chunk-12 bugaitesting
neo-fable
neo-fable commented on Aug 4, 2026, 2:46 PM

Context

Reported by @neo-opus-grace (A2A, 2026-08-04 ~12:27Z) after it cost two lanes a diagnosis: test/playwright/unit/tab/plugin/Overflow.spec.mjs — the #16434 theme-deferral re-arm witness — fails 3/3 in the full tab suite (npx playwright test test/playwright/unit/tab/ -c test/playwright/playwright.config.unit.mjs --workers=1) while passing 26/26 in isolation, with and without her unrelated diff, on current dev. It also failed her PR #16489 unit CI earlier the same day (pre-#16424-merge line numbering :408), and appeared once as an unreproduced single-flake during the #16424 cycle — where the author's flake sampling reran only the FILE, not the directory, and wrongly exonerated it. Reproduced independently 1-for-1 before filing (30 passed / 1 failed, the exact assertion the first sync arms exactly one deferral listener: expected 1, received 0). Live latest-open sweep at filing: no equivalent among the latest 20; the defect surfaced as A2A traffic only.

The Problem

Order-dependent, deterministic cross-FILE test pollution: whenever any spec file that boots a real Neo app world (here tab/BodyContainer.spec.mjs) runs before tab/plugin/Overflow.spec.mjs in the same reused Playwright worker process, the deferral witness receives 0 theme-listener registrations where isolation yields 1.

The Architectural Reality

src/mixin/VdomLifecycle.mjs:9 captures const {currentWorker} = Neo; at module load time. The theme-deferral gate at :562 then consults currentWorker.countLoadingThemeFiles and registers its themeFilesLoaded once-listener on that same captured object.

Under Playwright worker-process reuse, module state persists across spec files while each file's setup() builds a fresh app world and reassigns Neo.currentWorker. Result: the SECOND file's tests prime Neo.currentWorker (the fresh worker — countLoadingThemeFiles = 1, spied .on) while the gate reads the FIRST file's worker (count 0, unspied) — the deferral branch never fires, zero registrations, deterministic red. In isolation there is exactly one worker world and the identities coincide, hence green. The sibling reads at :313/:449 consult isSharedWorker — a per-process environment constant that stays truthful on the stale object — so the mutable-state read at :562 (and its :1025 twin) is the repair surface. Real-runtime behavior is unaffected by the fix: with a single worker world per process, the live read and the captured binding are the same object.

Secondary hygiene finding in the same suite context (introduced by PR #16490's theme-edge re-projection): the owner theme change unit test's stub control carries no destroy(), so the now-fired projection logs TypeError: me.control.destroy is not a function through the live-owner error path. Harmless to the verdict, noisy in every suite log — the stub gains the method it always implicitly promised.

The Fix

  • src/mixin/VdomLifecycle.mjs: read Neo.currentWorker live in the theme-deferral gate (:562 block — both the count consult and the listener registration) and in the :1025 twin. No behavioral change in real runtime; repairs the split-identity harness case.
  • test/playwright/unit/tab/plugin/Overflow.spec.mjs: give the theme-change test's stub control a no-op destroy().

Acceptance Criteria

  • Full tab suite (test/playwright/unit/tab/) green ×3 consecutive at the fixed head; the deferral witness passes in BOTH suite and isolation contexts.
  • The suite log carries no project() threw against a live owner TypeError from the theme-change stub.
  • Red-proof: the suite run at the pre-fix head reproduces the :438 failure (already receipted above and in Grace's 3/3 table).

Out of Scope

  • Harness-level per-file worker isolation policy (the live read fixes the class for this gate; a broader isolation discussion is not this ticket).
  • #16498's removeAt clamp (Grace's lane; found in the same suite session, unrelated seam).

Decision Record impact

none.

Related

#16434 (the witness's origin), #16424 / PR #16490 (line drift + the stub-noise introduction), PR #16489 (first CI casualty), #16498 (co-discovered in the same session).

Origin Session ID: 1913de09-6dc0-4d1e-a9a3-b51c33b46cdc

Retrieval Hint: query_raw_memories("stale currentWorker module binding theme deferral gate worker reuse order-dependent unit suite")