LearnNewsExamplesServices
Frontmatter
id19713
titleconnectToApp's identity wait returns at once and binds the wrong app
stateClosed
labels
bugaitesting
assigneesneo-opus-grace
createdAtOct 13, 2026, 4:04 PM
updatedAtOct 13, 2026, 5:42 PM
githubUrlhttps://github.com/neomjs/neo/issues/19713
authorneo-opus-grace
commentsCount2
parentIssue19655
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 13, 2026, 5:28 PM

connectToApp's identity wait returns at once and binds the wrong app

Closed Backlog/active-chunk-8 bugaitesting
neo-opus-grace
neo-opus-grace commented on Oct 13, 2026, 4:04 PM

Context

During #19671's run, @neo-gpt's fresh page called neuralLink.connectToApp('Neo.examples.grid.bigData') and bound his older same-name session c763996a, which had columnMenu: true. The test's setProperties wrote to that older app. His trace: the identity step "returned a handle immediately and jsonValue null during bootstrap, then appName selected oldest." Captured as a defect-note on 2026-10-13; promoted here by triage: every Brain-gated diagnosis reads through this fixture, the tier now blocks every Engine PR (#19705), and its retry bridge ends on 2026-10-25.

The Problem

connectToApp asks the page for its own App Worker id with page.waitForFunction(async () => …). Playwright treats the async predicate's Promise as truthy, so the wait resolves on its first evaluation and never polls. A probe on the pinned Playwright 1.63.0, with a page that sets the value after 1500 ms:

Predicate Result
async () => window.id ?? null null after 9 ms
() => window.id ?? null "ready" after 1492 ms

During bootstrap the remote is not registered yet, so the first read is null. The fixture then binds by appName (all 139 connectToApp calls pass one). ConnectionService.waitForSession returns the first sessionData entry with that appName, which is the oldest.

Observed: locally, with another same-name app open, the test drives the other app. Inference, not yet observed in CI: a same-name session still registered from an earlier test would bind the same way. FiveBeat scene 2 (morph) needed the retry in both #19705's run and the post-merge dev run 38403855169. Its first-attempt error was "no worker error reached the page console, and the page threw nothing", which is what a test driving another page's worker would see.

Two specs already work around it locally: WorkstationTopologyGroupsNL.spec.mjs:99 (readWorkerId, expect.poll + page.evaluate), and #19711 adds another.

The Architectural Reality

  • test/playwright/fixtures.mjs:287-330 connectToApp: the async waitForFunction at :301-309, and the appName fallback at :319-326.
  • Brain ai/services/neural-link/ConnectionService.mjs:1035 waitForSession: id === target || appName match, first in Map insertion order.
  • Design authority: the fixture's own comments. "The identity path is what makes same-name topologies safe", and "Poll with a bounded wait instead; only genuinely old builds (no getWorkerId remote) fall through to appName." The code contradicts both. It was introduced by #12897 (c115c846c4).
  • The argument fallback has two faces. On an open page without Neo it never ran: its "2 s" Neo wait has passed {timeout: 2000} as the predicate's arg since #8851, so it waits until the test timeout. On a closed page the read throws at once and the fallback runs. WorkstationTopologyGroupsNL :290 and :333 rely on that: they close the fixture's context and pass an exact worker id read from a page in a fresh context.

The Fix

Engine-only, in connectToApp:

  1. Read the id with a Node-side poll of page.evaluate through expect.poll, the shape readWorkerId already proves, bounded and returning as soon as the remote answers.
  2. An exact App Worker id passed as the argument binds as given. A name goes through the page's own id and is never bound by name; when no id arrives within the bound, connectToApp throws an error naming it. The unbounded Neo wait is deleted.

Decision Record impact: none.

Contract Ledger

Every row is in test/playwright/fixtures.mjs at #19715's head b0a0edc2fc. There is no Brain change.

Target surface Source of authority Proposed behavior Fallback Docs Evidence
neuralLink.connectToApp(target), target a name (139 of 139 call sites pass one) this ticket; the method's own JSDoc, "the identity path is what makes same-name topologies safe" binds the page's own App Worker id, read by readWorkerId, through NeuralLink_ConnectionService.waitForSession(id, 30000)'s exact-id match; never binds by name no id within identityTimeout: throws neuralLink.connectToApp('<target>'): the page's App Worker reported no id within <n> ms… JSDoc Fixture.spec.mjs: the competitor test (the older session never binds) and the refusal test
connectToApp(target), target an App Worker id (UUID, matched by WORKER_ID) this ticket; it keeps the exact-id behavior that WorkstationTopologyGroupsNL :183 and :583 rely on binds that id as given, without reading the page, so it works after the fixture's page has closed an id that never connects: waitForSession rejects after 30 s (unchanged) JSDoc Fixture.spec.mjs closed-page arm; TopologyGroups :290 and :333
connectToApp(), target omitted this ticket same as a name: the page's own id (WORKER_ID.test(undefined) is false) no id: throws, naming undefined. The old appPath inference is gone; no call site omits the target JSDoc the name-path tests
options.identityTimeout (new) this ticket ms bound for the id poll; default 30000, the bound waitForSession already uses — JSDoc the refusal test passes 1000
readWorkerId(page, timeout), exported (new) this ticket; it lifts WorkstationTopologyGroupsNL.spec.mjs:99's proven helper polls Neo.worker.App.getWorkerId() through expect.poll over page.evaluate until a non-empty id, unwrapping a SharedWorker {data} envelope resolves null on timeout or a closed page; never throws JSDoc Fixture.spec.mjs reads both pages with it
Brain ConnectionService.waitForSession — unchanged — — the diff touches only test/playwright/

Acceptance Criteria

  • A test/playwright/e2e/neural-link/Fixture.spec.mjs case connects an older same-name page first, then calls connectToApp on a freshly navigated page and asserts that the bound session is that page's own worker id. It is red on the merge-base and green on the head.
  • A page whose App Worker reports no id within identityTimeout makes connectToApp throw a message naming the appName; nothing binds by appName.
  • The PR's full Brain-gated run is green, and its receipt's retry list is compared with run 38403855169's.
  • Post-merge: the next dev runs' retry lists show whether FiveBeat scene 2 still needs the retry. The answer goes on #19655.

Out of Scope

  • Making Brain waitForSession refuse an ambiguous appName. That is a Brain contract change, for a defect the Engine fixture can close by itself.
  • Deleting the spec-local readWorkerId copies. Their owners can drop them after this lands.
  • The other quarantined intermittents.

Avoided Traps

  • A longer waitForFunction timeout: the predicate never polls, so the timeout is never reached.
  • A sync predicate that caches the remote's Promise on window: it writes test state into the app's global.
  • Keeping a name fallback when the page reports no id: it keeps the oldest-session guess alive. An exact id needs no fallback: it is bound as given.

Related

#19655 · #19657 · #19705 · #12897 · #14859 · #19671 · #19711 · #17844 · #8851

Live latest-open sweep: the latest 20 open issues at 2026-10-13T14:02:50Z, plus gh issue list --search connectToApp, found no equivalent. A2A claim sweep at 14:03Z: no claim on this scope. Memory Core sweep: #12897's identity-bind design (June) and #14859's envelope fix (July). Neither recorded that the poll does not poll. Own open assignments: #14800 and #15000, which do not overlap.

Origin Session ID: 747f2b2a-718e-4049-adc4-c31c72be4160 Retrieval Hint: "connectToApp identity wait async waitForFunction never polls same-name oldest session"

tobiu referenced in commit 1984e6f - "feat(test): connectToApp polls the page's App Worker id and never binds by appName (#19713) (#19715) on Oct 13, 2026, 5:28 PM
tobiu closed this issue on Oct 13, 2026, 5:28 PM
tobiu referenced in commit d52f0e3 - "test(e2e): ReclaimEmbodiment :148 returns to the Brain quarantine (#19721) (#19722) on Oct 13, 2026, 8:24 PM