Removing the hidden process.cwd() fallback from spawnBridge (#16429, shipped in PR #16983, merged 2026-08-12) taught one entrypoint to supply --cwd: the Neural Link MCP server. The Playwright fixture is a second entrypoint, and it was never taught.
Surfaced 2026-08-19 by a downstream consumer's CI on a neo pin bump: 17 of 17 whitebox e2e specs failed in ~300ms each, all with the same refusal, none reaching a page.
Error: ConnectionService.spawnBridge: `cwd` is unresolved. It is supplied by the Neural
Link MCP entrypoint (`--cwd`) and must be assigned before a spawn. Refusing to substitute
process.cwd() — on a GUI-launched server that is `/`, and the Bridge cannot start there.
I own a share of this: PR #17384 (merged 2026-08-19, #17369) rewrote this exact fixture's Neural Link imports, for a downstream-consumer reason, and did not supply the cwd.
The Problem
test/playwright/fixtures.mjs calls manageConnection({action: 'start'}). That reaches ensureBridgeAndConnect(), which attempts connectToBridge() first and calls spawnBridge() only when no Bridge is already listening. spawnBridge() now throws unless ConnectionService.cwd is assigned, and nothing on the fixture path assigns it.
Why it survived a week unseen, and why no maintainer can reproduce it: every seat runs a Neural Link Bridge on port 8081 (configBase.mjs:54) because the MCP server starts one. connectToBridge() therefore succeeds locally, spawnBridge() is never reached, and the refusal never fires. It fires only where nothing is listening — a clean CI container.
And neo has no e2e job. No workflow in .github/workflows/ references test-e2e or playwright.config.e2e.mjs (40 lint/guard workflows, zero e2e). So the only observer of this code path in existence is a downstream adopter's pipeline.
Blast radius: 100 of neo's 111 e2e spec files import this fixture, including all 40 under test/playwright/e2e/agentos.
The contract this breaks is documented and still taught.learn/guides/testing/WhiteboxE2E.md:37 — "Instead of manually establishing WebSocket connections, the Neo.mjs team provides a powerful Playwright fixture called neuralLink." Neither that guide nor .agents/skills/whitebox-e2e/ ever tells an author to start a Bridge first, because not managing the connection is the fixture's entire value proposition.
The Architectural Reality
ai/services/neural-link/ConnectionService.mjs:829 — the if (!this.cwd) refusal, immediately before the spawn.
ai/mcp/server/neural-link/Server.mjs:185 — ConnectionService.cwd = this.bridgeCwd. The only production assignment in the tree; every other assignment is a unit-test stub.
ai/services/neural-link/ConnectionService.mjs — ensureBridgeAndConnect(): connect-then-spawn, which is what makes the failure invisible wherever a Bridge is already up.
test/playwright/fixtures.mjs — calls manageConnection({action: 'start'}) with cwd unassigned.
process.cwd() is the correct value at this call site specifically, and for a structural reason rather than a convenient one: Playwright resolves its config and testDir relative to the invocation directory, so a fixture only ever runs from the consuming project's root. That is exactly the property a GUI-launched MCP server lacks, which is why the refusal is right to keep rejecting the general case.
CORRECTED 2026-08-20 — this paragraph was false, and it was the prescription. Falsified in review of PR #17405 by @neo-gpt-emmy, then reproduced independently on a two-arm probe. Playwright resolves testDir from the config file and never calls chdir, which is precisely why the invocation directory is unconstrained: npx playwright test -c /abs/config.mjs run from anywhere is a valid, green run whose worker cwd is that anywhere.
arm
invoked from
worker cwd
package.json there
run
A
project root
project root
yes
passed
B
/private/tmp, absolute -c
/private/tmp
no
passed
The consequence is worse than an inaccuracy: process.cwd() hands spawnBridge a plausible wrong value where it previously had none, which is the exact class its refusal was hardened against (#16429). Measured on the real suite, same port, same invocation — process.cwd() from a foreign cwd gives BRIDGE_EXIT_254 then ECONNREFUSED, four specs red. A named refusal became a distant symptom.
The Fix
In test/playwright/fixtures.mjs, assign the cwd before the connect call — derived from the fixture's own location and validated, never read from the ambient process:
findBridgeScriptRoot walks up from a caller-supplied directory and returns the first ancestor whose package.jsondeclares the script, so a returned path is one where npm run is known to resolve rather than one that looks like a root. It takes both the start directory and the script name as required arguments — a default for either would be a hidden default of the kind this replaces. BRIDGE_NPM_SCRIPT is exported from ConnectionService so the fixture validates against the same name spawnBridge will run, rather than a copy free to drift from it.
??= rather than = so an entrypoint that already supplied one is never clobbered. null (no ancestor declares it) is a real result: the cwd stays unassigned and the spawn refuses by name, which is strictly better than starting npm somewhere it cannot resolve.
The change is inert wherever a Bridge is already listening — that path never reads cwd — so its only behavioural effect is on the path that currently throws unconditionally.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
test/playwright/fixtures.mjs
ConnectionService.cwd contract (Server.mjs:185)
fixture supplies a cwd derived from its own module location and confirmed to declare BRIDGE_NPM_SCRIPT
??= yields to an already-assigned value; unresolved → stays null so the spawn refuses by name
learn/guides/testing/WhiteboxE2E.md
17/17 downstream e2e failures at the refusal
test/playwright/findBridgeScriptRoot.mjs
the package.json that declares the script
nearest declaring ancestor wins; a manifest without the script is not an answer
returns null, never a guess
jsdoc + spec
7 unit arms, 3 killed by a mutant that drops the validation
AC-1 — red-proof, and it must be the real path: with no listener on port 8081, a whitebox e2e spec reaches its page. Asserted against the same spec failing at the refusal before the change. A unit stub cannot serve as this control — PR #16983 shipped 343 lines of green unit spec around this guard while the fixture path was broken.
AC-2 — no regression with a Bridge already running: the whitebox e2e suite behaves identically when 8081 is occupied, since that path never reads cwd.
AC-3: an already-assigned cwd is not overwritten.
AC-4:learn/guides/testing/WhiteboxE2E.md states how the fixture derives the Bridge cwd, so the documented contract matches the code — and does not repeat the falsified process.cwd() claim.
AC-6 — portability, added 2026-08-20: the supplied directory is derived from the fixture's own location and confirmed to declare the script, so a run invoked from a foreign cwd with an absolute -c reaches its page. Red arm required: the same invocation on process.cwd() must fail. Added because AC-1 as originally written is satisfiable by a value that only works when the invocation happens to start at the root — the ledger's own prescription passed AC-1 and failed this.
AC-5 — post-merge, flagged: the downstream consumer's e2e passes on a pin carrying this fix.
Observer named (2026-08-20). Left as an obligation with no owner, this is a deferral nobody is watching. It is observed by the downstream consumer's pin-bump pipeline, which is already open and already waiting on this merge — the pin bump is the check, and it fails loudly there if this is wrong. It is therefore tracked on the consumer side and is not work this ticket's PR owes; it stays listed here so the trail records who watches it, not to gate the close.
Out of Scope
#16992's half — keeping the MCP server alive when the Bridge cannot spawn. Separate concern, already owns its ACs, and its AC-1 presumes --cwdis supplied.
Changing the refusal. It is correct; this supplies the input it asks for.
Adding an e2e job to neo CI. Real, and the reason this aged a week unseen, but it is a CI-capacity decision with its own cost profile. Named here so the next reader sees it was considered, not missed.
Avoided Traps
Fixing it downstream. The consumer could assign ConnectionService.cwd itself, but that is a workaround in every adopter for a defect in the shared fixture — and it would re-add a workaround to the very PR whose purpose was removing them.
Reading a green unit suite as coverage. The guard's unit specs assign cwd = '/real-seat' by hand, so they exercise the branch and never the caller. They were green throughout.
Trusting a local run. A maintainer seat always has a Bridge up, so a local e2e pass is not evidence about the spawn path. The control must remove the listener.
Related
#16429 / PR #16983 — removed the fallback and taught only the MCP entrypoint.
#16992 — the survivability half split out of #16429.
#17369 / PR #17384 — rewrote this fixture's imports without supplying the cwd.
Context
Removing the hidden
process.cwd()fallback fromspawnBridge(#16429, shipped in PR #16983, merged 2026-08-12) taught one entrypoint to supply--cwd: the Neural Link MCP server. The Playwright fixture is a second entrypoint, and it was never taught.Surfaced 2026-08-19 by a downstream consumer's CI on a neo pin bump: 17 of 17 whitebox e2e specs failed in ~300ms each, all with the same refusal, none reaching a page.
I own a share of this: PR #17384 (merged 2026-08-19, #17369) rewrote this exact fixture's Neural Link imports, for a downstream-consumer reason, and did not supply the cwd.
The Problem
test/playwright/fixtures.mjscallsmanageConnection({action: 'start'}). That reachesensureBridgeAndConnect(), which attemptsconnectToBridge()first and callsspawnBridge()only when no Bridge is already listening.spawnBridge()now throws unlessConnectionService.cwdis assigned, and nothing on the fixture path assigns it.Why it survived a week unseen, and why no maintainer can reproduce it: every seat runs a Neural Link Bridge on port 8081 (
configBase.mjs:54) because the MCP server starts one.connectToBridge()therefore succeeds locally,spawnBridge()is never reached, and the refusal never fires. It fires only where nothing is listening — a clean CI container.And neo has no e2e job. No workflow in
.github/workflows/referencestest-e2eorplaywright.config.e2e.mjs(40 lint/guard workflows, zero e2e). So the only observer of this code path in existence is a downstream adopter's pipeline.Blast radius: 100 of neo's 111 e2e spec files import this fixture, including all 40 under
test/playwright/e2e/agentos.The contract this breaks is documented and still taught.
learn/guides/testing/WhiteboxE2E.md:37— "Instead of manually establishing WebSocket connections, the Neo.mjs team provides a powerful Playwright fixture calledneuralLink." Neither that guide nor.agents/skills/whitebox-e2e/ever tells an author to start a Bridge first, because not managing the connection is the fixture's entire value proposition.The Architectural Reality
ai/services/neural-link/ConnectionService.mjs:829— theif (!this.cwd)refusal, immediately before the spawn.ai/mcp/server/neural-link/Server.mjs:185—ConnectionService.cwd = this.bridgeCwd. The only production assignment in the tree; every other assignment is a unit-test stub.ai/services/neural-link/ConnectionService.mjs—ensureBridgeAndConnect(): connect-then-spawn, which is what makes the failure invisible wherever a Bridge is already up.test/playwright/fixtures.mjs— callsmanageConnection({action: 'start'})withcwdunassigned.The Fix
In
test/playwright/fixtures.mjs, assign the cwd before the connect call — derived from the fixture's own location and validated, never read from the ambient process:const bridgeScriptRoot = findBridgeScriptRoot(path.dirname(fileURLToPath(import.meta.url)), BRIDGE_NPM_SCRIPT); // ... NeuralLink_ConnectionService.cwd ??= bridgeScriptRoot;findBridgeScriptRootwalks up from a caller-supplied directory and returns the first ancestor whosepackage.jsondeclares the script, so a returned path is one wherenpm runis known to resolve rather than one that looks like a root. It takes both the start directory and the script name as required arguments — a default for either would be a hidden default of the kind this replaces.BRIDGE_NPM_SCRIPTis exported fromConnectionServiceso the fixture validates against the same namespawnBridgewill run, rather than a copy free to drift from it.??=rather than=so an entrypoint that already supplied one is never clobbered.null(no ancestor declares it) is a real result: the cwd stays unassigned and the spawn refuses by name, which is strictly better than startingnpmsomewhere it cannot resolve.The change is inert wherever a Bridge is already listening — that path never reads
cwd— so its only behavioural effect is on the path that currently throws unconditionally.Contract Ledger Matrix
test/playwright/fixtures.mjsConnectionService.cwdcontract (Server.mjs:185)BRIDGE_NPM_SCRIPT??=yields to an already-assigned value; unresolved → staysnullso the spawn refuses by namelearn/guides/testing/WhiteboxE2E.mdtest/playwright/findBridgeScriptRoot.mjspackage.jsonthat declares the scriptnull, never a guessspawnBridgerefusalprocess.cwd() === '/'Decision Record impact
none.Acceptance Criteria
cwd.cwdis not overwritten.learn/guides/testing/WhiteboxE2E.mdstates how the fixture derives the Bridge cwd, so the documented contract matches the code — and does not repeat the falsifiedprocess.cwd()claim.-creaches its page. Red arm required: the same invocation onprocess.cwd()must fail. Added because AC-1 as originally written is satisfiable by a value that only works when the invocation happens to start at the root — the ledger's own prescription passed AC-1 and failed this.Out of Scope
--cwdis supplied.Avoided Traps
ConnectionService.cwditself, but that is a workaround in every adopter for a defect in the shared fixture — and it would re-add a workaround to the very PR whose purpose was removing them.cwd = '/real-seat'by hand, so they exercise the branch and never the caller. They were green throughout.Related
Origin Session ID: 20a0add9-b999-41e5-b90e-c37913987602
Retrieval Hint:
query_raw_memories("Playwright fixture second entrypoint spawnBridge cwd unresolved connect-then-spawn 8081 listener")