LearnNewsExamplesServices
Frontmatter
id12331
titleIsolate unit-test chroma + sqlite-daemon stores + orphan-purge
stateClosed
labels
bugaitesting
assigneesneo-opus-ada
createdAtJun 2, 2026, 1:29 AM
updatedAtJun 7, 2026, 7:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/12331
authorneo-opus-ada
commentsCount6
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[x] 10384 Revalidate full-suite Neo.setupClass contamination after test isolation lands, [x] 12180 Orphan test-* Chroma collections accumulate (incomplete test cleanup), [x] 12143 Test runs leak test-* collections into the production unified Chroma store
closedAtJun 6, 2026, 1:02 AM

Isolate unit-test chroma + sqlite-daemon stores + orphan-purge

neo-opus-ada
neo-opus-ada commented on Jun 2, 2026, 1:29 AM

Context

Operator-directed (2026-06-02), after exploring the unit-test isolation: the chroma prod-wipe risk is genuinely resolved (process-isolated test-* collection names + afterAll cleanupChromaManager + the FATAL UNIT_TEST_MODE guard protect the real collections), but the orphan accumulation is not. Empirical, live store 2026-06-02:

  • Chroma: 1,281 test-* orphan collections in default_tenant/default_database vs only 4 real (neo-agent-memory, neo-agent-sessions, neo-knowledge-base, neo-native-graph) — up from #12180's 1,263 (2026-05-29).
  • SQLite-daemon: test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs writes its db and daemon dir into the prod data dir (DB_PATH = .neo-ai-data/sqlite/test-daemon-${testId}.sqlite, DAEMON_DIR = .neo-ai-data/wake-daemon-test-${testId}) — the SQLite sibling of the chroma leak; 12 leaked dbs were just cleaned but bleed back on every run.

This is the unified test-store-isolation gap that lets #12143/#12180 (and the daemon leak) actually close.

The Problem

Unit tests write to the live stores. Process-isolated names + afterAll cleanup protect prod from wipes and handle the happy path, but interrupted/Ctrl-C/CI-cancel runs and bare-npx bypasses still leak, and there's no ephemeral isolation (so crashes can touch prod-adjacent namespaces) and no purge for the existing backlog. The graph store already does this right (config:112:memory: under UNIT_TEST_MODE); chroma + the daemon db do not.

The Architectural Reality

  • Chroma names: ai/mcp/server/memory-core/config.template.mjs:119-120test-memory-${ts}-${rand} / test-session-… under UNIT_TEST_MODE, but the same default_tenant/default_database as prod.
  • Chroma client: ai/services/memory-core/managers/ChromaManager.mjs#connect()chromaConnect({client}) (ai/services/shared/vector/chromaClientPrimitives.mjs); client is injectable; ChromaManager.deleteCollection({name, confirmation}) (+ DestructiveOperationGuard) is the safe delete path.
  • SQLite-daemon: daemon.spec.mjs DB_PATH/DAEMON_DIR resolve under .neo-ai-data/ (prod). cleanupChromaManager (util.mjs) has the FATAL UNIT_TEST_MODE guard.
  • Already-correct precedent: graph store :memory: under UNIT_TEST_MODE (config:112).
  • No purge: defragChromaDB.mjs cleans orphaned segment directories only, NOT the test-* collection entries.

The Fix

  1. Chroma ephemeral isolation — under UNIT_TEST_MODE, route the chroma client to an isolated namespace so test collections never land in default_tenant/default_database. Recommended mechanism: a dedicated test tenant/database (chroma multi-tenancy), whole-namespace-droppable. (Mechanism — tenant/db vs throwaway persist-dir vs in-memory — confirm at implementation against the actual client-creation site; prod path stays default_tenant/default_database.)
  2. SQLite-daemon isolationdaemon.spec.mjs DB_PATH + DAEMON_DIRos.tmpdir()-based paths + afterEach/afterAll cleanup. Off the prod data dir by construction.
  3. Orphan-purge utilityai/scripts/maintenance/purgeTestCollections.mjs: lists chroma collections, deletes test-memory-* / test-session-* (via the ChromaManager/guarded delete path; or drops the test tenant/db) + the .neo-ai-data/sqlite/test-daemon-* residue. Runnable on demand (clears the existing 1,281 backlog) and optionally at suite start.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
Chroma client namespace under UNIT_TEST_MODE ChromaManager#connect + config.template.mjs Test client resolves to an isolated tenant/database (or equiv); prod unchanged (default_tenant/default_database) If multi-tenancy is unavailable at the client/SDK level, fall back to a throwaway persist-dir or keep test-* names + mandatory purge-at-suite-start config JSDoc Static: prod path asserts default_*; test path asserts the isolated namespace
purgeTestCollections.mjs (new maintenance surface) this ticket + ai/scripts/maintenance/ siblings Deletes test-memory-*/test-session-* collections + test-daemon-*.sqlite residue; refuses to touch the 4 real collections Dry-run flag; never deletes a non-test-* name (hard guard) script JSDoc + npm run entry Unit: given a mixed collection list, only test-* deleted; real collections untouched

Acceptance Criteria

  • AC1 — Under UNIT_TEST_MODE, chroma test collections resolve to a namespace distinct from default_tenant/default_database (statically verifiable); by construction no test run — including crashed/npx-bypassed — can create collections in the prod namespace.
  • AC2 — daemon.spec.mjs writes its db + daemon dir under os.tmpdir(), not .neo-ai-data/ (statically verifiable); afterEach/afterAll cleanup present.
  • AC3 — purgeTestCollections.mjs deletes test-* chroma collections + test-daemon-* sqlite residue, with a hard guard that never deletes a non-test-* name; running it against the live store leaves only the 4 real collections (verified by a follow-up count).
  • AC4 — No prod regression: prod chroma still uses default_tenant/default_database; the prod-wipe guards (DestructiveOperationGuard, FATAL UNIT_TEST_MODE guard) remain intact.
  • AC5 — On merge + purge-run, #12143 (leak) + #12180 (accumulation) are resolved (the daemon leak too).

Out of Scope

  • GraphLog CDC-log compaction (#12329 — separate; the transient sync log, not the test stores).
  • Broader graph/Dream-pipeline hygiene.

Related

  • Resolves-by-completion: #12143, #12180
  • Sibling: the daemon.spec.mjs SQLite leak (no standalone ticket; folded here)
  • Adjacent: #12329 (GraphLog compaction), #10867 (the prod-wipe P0 protection this builds on)

Origin Session ID: da9a6007-1250-4363-8c15-dff69eccb3be Retrieval Hint: "unit-test chroma sqlite-daemon ephemeral isolation orphan purge test-* collections"

tobiu referenced in commit b988f02 - "feat(ai): unit-test daemon store isolation + chroma orphan-purge (#12331) (#12336) on Jun 2, 2026, 2:56 AM
tobiu referenced in commit eccfe22 - "feat(ai): isolate unit-test Chroma into a dedicated database (#12335) (#12397) on Jun 3, 2026, 1:11 AM
tobiu
tobiu Jun 6, 2026, 1:02 AM

ran it:

tobiasuhlig@819fe61f-83c8-4461-95d7-a3ac14b840ba neo % npm run ai:purge-test-collections -- --apply

> neo.mjs@12.1.0 ai:purge-test-collections > node ./ai/scripts/maintenance/purgeTestCollections.mjs --apply

🧹 Purge test collections — APPLY (destructive) 🔌 Chroma: localhost:8000 Collection schema deserialization was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package. Collection schema deserialization was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package. Collection neo-knowledge-base was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package. 📚 1302 collections total → 1296 test orphans, 6 retained. ✨ Retained: test-where, neo-agent-memory, test-where-in, neo-knowledge-base, neo-agent-sessions, neo-native-graph

🔥 Deleting 1296 test orphans: …deleted 100/1296 …deleted 200/1296 …deleted 300/1296 …deleted 400/1296 …deleted 500/1296 …deleted 600/1296 …deleted 700/1296 …deleted 800/1296 …deleted 900/1296 …deleted 1000/1296 …deleted 1100/1296 …deleted 1200/1296

🧽 SQLite-daemon residue under /Users/Shared/github/neomjs/neo/.neo-ai-data: ✅ none found. Collection schema deserialization was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package. Collection schema deserialization was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package. Collection neo-knowledge-base was created with the dummy_embedding_function embedding function. However, the @chroma-core/dummy_embedding_function package is not installed. 'add' and 'query' will fail unless you provide them embeddings directly, or install the @chroma-core/dummy_embedding_function package.

🎉 Purge complete: deleted 1296 collections (0 failed); 6 remain (0 test orphans left).

tobiu closed this issue on Jun 6, 2026, 1:02 AM