LearnNewsExamplesServices
Frontmatter
id17369
titlePlaywright fixture loads the whole Brain for six Neural Link symbols
stateClosed
labels
bugaitestingperformance
assigneesneo-opus-ada
createdAtAug 19, 2026, 5:44 AM
updatedAtAug 21, 2026, 11:42 AM
githubUrlhttps://github.com/neomjs/neo/issues/17369
authorneo-opus-ada
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 19, 2026, 2:03 PM

Playwright fixture loads the whole Brain for six Neural Link symbols

Closed Backlog/active-chunk-17 bugaitestingperformance
neo-opus-ada
neo-opus-ada commented on Aug 19, 2026, 5:44 AM

Context

Observed in a downstream consumer project that pins neo as a git dependency and runs whitebox E2E through our Playwright fixture. Its CI began failing with every test passing and then the process aborting:

Statement::~Statement() [node_modules/better-sqlite3/build/Release/better_sqlite3.node]
Aborted (core dumped)

17 passed immediately above, then exit code 1. Three open merge requests in that project were red on this and nothing else. It is intermittent — the same dependency tree went green on other runs the same day — which is the signature of a teardown race rather than a test failure.

That consumer does not use SQLite. It has zero first-party importers of better-sqlite3; the package appears exactly once in its repository, as a devDependencies line.

Observation vs inference. Observed: the abort trace, the zero-importer count, the two experiments in The Fix below. Inferred: the trigger described in The Architectural Reality, which is a code-read and is not fully pinned — see the open question there.

The Problem

test/playwright/fixtures.mjs needs six Neural Link symbols. It obtains them from ai/services.mjs, which is a barrel over the entire Brain — knowledge-base, memory-core, ingestion, graph. Importing six Neural Link services therefore pulls memory-core and the graph layer into the test process of every consumer that uses our fixture.

The cost is not abstract:

  1. A native binding is loaded that the consumer never uses, and its destructor runs against a torn-down environment at process exit. That abort is what reds the pipeline, after every test has already passed — so the failure points at nothing the author changed.
  2. The dependency becomes non-removable downstream. A consumer that correctly identifies better-sqlite3 as unused cannot drop it, because our fixture demands it.
  3. Install weight. Consumers carry native SQLite (and the graph surface behind it) purely to run browser tests.

The Neural Link services themselves are clean. Their imports are src/core/Base.mjs, their own config/logger, RuntimeFreshnessService, their siblings, and node builtins plus ws/fs-extra. Nothing in the Neural Link surface needs the Brain. The coupling is entirely an artifact of the barrel.

The Architectural Reality

  • test/playwright/fixtures.mjs:3-11 — the six symbols are destructured from '../../ai/services.mjs'.

  • ai/services.mjs:16-18, 203-209 — the barrel re-exports NeuralLink_* alongside every other service family.

  • ai/services/memory-core/GraphService.mjs:6statically imports ../../../ai/graph/storage/SQLite.mjs, so the barrel reaches the graph storage module at load.

  • ai/graph/storage/SQLite.mjs:49 — loads the native binding through a dynamic import, carrying this comment:

    // Use dynamic imports to prevent native Node module evaluation crashes inside browser/test runtimes

    That mitigation already exists and already names this exact hazard. The barrel path defeats it in practice.

  • ai/services/neural-link/ — the six services live in their own modules and can be imported directly.

Open question, deliberately not asserted: a dynamic import should not fire at module load, yet removing the package makes the fixture fail to load with ERR_MODULE_NOT_FOUND … imported from ai/graph/storage/SQLite.mjs. Something on the barrel path reaches the dynamic import during initialization. Identifying that caller is part of the work, and it may reveal a second defect worth its own ticket — an eagerly-opened graph connection would be a problem beyond the test harness.

The Fix

Import the Neural Link services directly in the fixture instead of through the barrel:

import Neo                          from '../../src/Neo.mjs';
import * as core                    from '../../src/core/_export.mjs';
import NeuralLink_ConnectionService from '../../ai/services/neural-link/ConnectionService.mjs';
// …InstanceService, ComponentService, DataService, DockService, RuntimeService, InteractionService

The two core imports are load-bearing and are the non-obvious half. The barrel was also bootstrapping the Neo global as an undeclared side effect; direct imports alone fail with ReferenceError: Neo is not defined at src/core/Compare.mjs:166, thrown far from the import that caused it. ai/mcp/server/neural-link/run-bridge.mjs:2-3 already does exactly this pair, so the shape has precedent.

Both halves verified empirically against a consumer's suite:

experiment result
remove better-sqlite3, keep the barrel import ERR_MODULE_NOT_FOUND from ai/graph/storage/SQLite.mjs — fixture will not load
remove better-sqlite3, use direct imports + core bootstrap 17/17 passed

The absent dependency is the assertion: the suite passing without it installed is what proves the barrel was the only thing requiring it.

Acceptance Criteria

  • test/playwright/fixtures.mjs imports its Neural Link services from ai/services/neural-link/* rather than from ai/services.mjs.
  • The Neo global bootstrap is imported explicitly, and a comment records why — the next reader must not "simplify" it away, because its absence fails as a ReferenceError in an unrelated file.
  • A consumer running the fixture no longer resolves better-sqlite3. Verifiable without a consumer: no module under ai/graph/** or ai/services/memory-core/** is reachable from the fixture's import graph.
  • Neo's own Playwright suites stay green — the fixture is shared, so this is a change to every suite that uses it.
  • The caller that reaches SQLite.mjs:49's dynamic import during initialization is identified and named in the PR, or a follow-up ticket is filed if it is a separate defect.

Out of Scope

  • Restructuring ai/services.mjs itself. The barrel is legitimate for callers that genuinely want the whole Brain; the defect is that a test fixture uses it for six symbols. Narrowing the consumer is the proportionate fix.
  • The MCP bridge's own import graph — run-bridge.mjs is already lean and unaffected.
  • Whether chromadb can also be dropped by consumers. It is reached by three real importers under ai/services/**, so it is a genuine dependency of the barrel and a separate question once the fixture stops pulling that surface in.
  • Changing the dynamic-import mitigation at SQLite.mjs:49. It is correct; this ticket stops the path that renders it moot.

Avoided Traps

  • Bumping better-sqlite3 to make the abort go away. The teardown crash is a real symptom, and a version bump might well silence it — but it would leave every consumer loading a native database binding to run browser tests. Treating the symptom here also costs the install-weight and non-removability wins.
  • Telling the consumer to drop the dependency. Attempted first, and it fails: the fixture will not load without it. A consumer cannot fix this from its own repository, which is precisely what makes it ours.
  • Assuming a lazy import is inert. SQLite.mjs:49 is a deliberate dynamic import with a comment explaining that it exists to avoid native-module crashes in test runtimes — and the crash happened anyway. A mitigation is only as good as the paths that respect it; the presence of the guard is not evidence the hazard is handled.
  • Scoping a dependency search to the directory you believe owns it. While diagnosing this, a search of ai/services/** for better-sqlite3 returned zero — with a positive control (chromadb, three hits) that passed. The control validated the matcher, not the path scope; the real importer was one directory over in ai/graph/. For "what does X transitively need", scope to the whole package.

Related

#135-class install-weight concerns in consuming projects · #17357 (container startup facts) is unrelated but touches the same ai/ tree · the RuntimeFreshnessService dependency of the Neural Link services is unaffected by this change.

Decision Record impact: none — this narrows one import site; it does not alter the service topology or any ADR-governed boundary.

Origin Session ID: 1baae1f2-97e4-418c-9119-c3112763f552

Handoff Retrieval Hints

  • query_raw_memories: "playwright fixture barrel import better-sqlite3 teardown abort"
  • query_raw_memories: "Neo is not defined Compare.mjs direct service import bootstrap"
  • Anchors: test/playwright/fixtures.mjs:3-11 · ai/services/memory-core/GraphService.mjs:6 · ai/graph/storage/SQLite.mjs:49 · ai/mcp/server/neural-link/run-bridge.mjs:2-3

Gate records — live latest-open sweep: latest 20 open issues checked 2026-08-18, no equivalent found. A2A in-flight sweep: latest 30 messages, all read-states; no overlapping [lane-claim]. Agent OS Structure Map gate: run; owning folders ai/services/neural-link and ai/graph/storage both exist as siblings — no new file, no placement decision.

tobiu referenced in commit 1e04a3a - "fix(test): import Neural Link services directly, not through the Brain barrel (#17369) (#17384) on Aug 19, 2026, 2:03 PM
tobiu closed this issue on Aug 19, 2026, 2:03 PM