LearnNewsExamplesServices
Frontmatter
id12047
titleinitServerConfigs: preserve operator edits when materializing stale server config imports
stateClosed
labels
bugaitestingarchitecture
assigneesneo-gpt
createdAtMay 26, 2026, 10:14 PM
updatedAtMay 27, 2026, 2:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/12047
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[ ] 11976 Tests import config.mjs (operator-overlay) instead of config.template.mjs (canonical defaults) — drift risk across ~78 imports
closedAtMay 27, 2026, 2:18 PM

initServerConfigs: preserve operator edits when materializing stale server config imports

Closed v13.0.0/archive-v13-0-0-chunk-14 bugaitestingarchitecture
neo-opus-ada
neo-opus-ada commented on May 26, 2026, 10:14 PM

Context

Surfaced during cycle-1 review of PR #12044 (export-drift guard for TenantRepoSyncService.resolveIngestionService). Any unit test that calls a function which transitively await import('ai/services.mjs') in an operator-overlay context (where ai/config.mjs has already registered Neo.ai.Config extending the template) trips the Neo.ai.Config namespace-collision guard at src/Neo.mjs:820:

Error: Namespace collision in unitTestMode for Neo.ai.Config
   at src/Neo.mjs:821
   at ai/config.template.mjs:578

PR #12044 worked around this with a per-test Neo.config.unitTestMode = false flip wrapped in try/finally. The flip is correct for the local case (the guard's non-strict branch is exactly designed for "different versions of Neo.mjs" / "different environments in parallel" — the same semantic that operator-overlay + template represents), but the workaround being needed at all is a substrate friction signal.

The Problem

unitTestMode strict behavior treats namespace presence as the collision criterion:

// src/Neo.mjs:819-825
if (ns) {
    if (Neo.config.unitTestMode) {
        throw new Error('Namespace collision in unitTestMode for ' + proto.constructor.config.className)
    }
    return ns
}

For operator-overlay-extending-template patterns (e.g., Neo.ai.Config registered first by ai/config.mjs, then ai/config.template.mjs:578 re-attempts registration via a transitive import chain), the existing namespace is the same class — strict-mode treats it as a collision, but it's structurally a no-op re-registration.

The current strict-mode guard can't distinguish:

  • Real collision: two different classes claiming the same className (bug — should error).
  • Self-equal re-registration: same class registered twice through different import chains (operator-overlay + template; no-op intended).

The Architectural Reality

  • src/Neo.mjs:811-825setupClass namespace-collision guard. Comment at lines 811-818 explicitly names "different versions of Neo.mjs" and "different environments in parallel" as cases the non-strict branch was written for.
  • ai/config.mjs (gitignored operator overlay, generated from ai/config.template.mjs) — both register Neo.ai.Config.
  • Any test that transitively triggers a re-import of either fails-loud under unitTestMode: true.

The Fix (design discussion needed before mechanical PR)

Three options, listed in increasing intrusiveness:

Option A — Class-identity-aware guard

Change the collision criterion from "namespace exists" to "namespace exists AND points to a different class identity":

if (ns) {
    if (Neo.config.unitTestMode && ns !== proto.constructor) {
        // existing namespace points to a DIFFERENT class — real collision
        throw new Error('Namespace collision in unitTestMode for ' + ...);
    }
    return ns
}

Tradeoff: preserves strict-mode's intent (catch real collisions) while allowing self-equal re-registration. Backward-compat for existing strict-mode tests (real collisions still throw). Risk: subtle class-identity comparison semantics under module re-loads (Playwright workers, hot-reload, etc.).

Option B — Test infra: don't auto-load operator overlay in test mode

Have the test runner (or setup.mjs) detect unitTestMode: true and bypass the operator overlay loading entirely — tests always run against canonical template. Pure-template test environment.

Tradeoff: keeps strict-mode semantics intact. But blocks tests that need to exercise the overlay path (e.g., tests verifying operator-specific config behavior). Some tests would need a "load overlay" opt-in.

Option C — Document the per-test flip pattern as canonical

Treat the try/finally flip in PR #12044 as the documented pattern for tests that need to exercise transitive services.mjs imports. Add a helper in setup.mjs (e.g., withoutUnitTestModeStrictness(fn)) so callers don't reinvent the boilerplate.

Tradeoff: lowest blast radius. But "test-mode-bypass-for-testing" is a structural smell — the substrate isn't being fixed, just made livable.

Decision Record impact

challenges the implicit unitTestMode strictness contract — depending on which option lands, may amend or supersede the guard semantics. ADR successor-risk audit needed for substrate-level changes (Option A).

Acceptance Criteria

  • Design decision recorded (A / B / C / hybrid).
  • Implementation matches the recorded design.
  • PR #12044's per-test flip is either removed (Option A makes it unnecessary) or kept with a comment pointing at the chosen design (Option B/C).
  • Test that asserts the new collision semantics: pass for same-class re-registration; fail for genuine class-identity mismatch.
  • Cloud-deployment guide note explaining the canonical pattern for tests that touch the services.mjs ↔ config-overlay boundary.

Out of Scope

  • Refactoring all existing strict-mode tests — wait for design decision before bulk-touching.
  • Operator-overlay design (whether overlay should extend template OR be a separate file) — orthogonal.

Avoided Traps

  • Defaulting to Option C because it's the least disruptive. Option A is the substrate-correct fix; "test-mode-bypass-for-testing" patterns accumulate substrate debt.
  • Treating this as cosmetic: the friction surfaced specifically because tests for cloud-deployment substrate (tenant-repo ingestion path) need to exercise the resolver. Future cloud-deployment-related tests will hit the same wall.

Related

  • Empirical anchor: @neo-gpt cycle-1 review of PR #12044 — flagged the residual Drift C gap; the workaround was needed to address it.
  • Sibling: #12042 (the export-drift guard ticket whose implementation surfaced this).
  • Empirical session: first real-world cloud Agent OS deployment, 2026-05-26.
tobiu referenced in commit 84758dd - "test(orchestrator): export-drift guard for resolveIngestionService (#12042) (#12044) on May 26, 2026, 10:37 PM
tobiu closed this issue on May 27, 2026, 2:18 PM
tobiu referenced in commit b66bc51 - "fix(ai): preserve config import materialization edits (#12047) (#12082) on May 27, 2026, 2:18 PM