/** * @module tests/unit/brainy/lazy-notready-honor * @description THE SILENT-EMPTY TRAP pin (found during a fleet adoption, * SELF-ENGINE-PAIR-STANDARD): under `disableAutoRebuild: true`, the OLD lazy * first-query path (`ensureIndexesLoaded`) assessed ONLY the vector index's * readiness — a native METADATA provider reporting not-ready (its strand * report) never blocked the completion latch, so the promised lazy rebuild * never fired and every `find()` silently returned `[]` on a populated store * (measured: 52 entities durable-but-unqueryable, first query 0ms/0 rows). * * RE-POINTED to the health-gate law (a read never builds; a rebuild runs * entirely at open): `ensureIndexesLoaded()` is now a pure CHECK. A not-ready * report from ANY provider — metadata, vector, or graph — makes it THROW the * matching typed `*NotReadyError` rather than silently letting the read * proceed, and it NEVER calls `rebuildIndexesIfNeeded` (that is entirely * open()'s job now — see the second describe block below). The spirit is * unchanged: a not-ready report from any single provider can never be * shadowed into a silent empty result. * * White-box provider-double pattern per tests/unit/brainy/migration-deference. */ import { describe, it, expect, afterEach, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Brainy, MetadataIndexNotReadyError } from '../../../src/index.js' import { NounType } from '../../../src/types/graphTypes.js' import { createTestConfig } from '../../helpers/test-factory.js' interface BrainInternals { index: { size(): number } metadataIndex: { isReady?: () => boolean } ensureIndexesLoaded(): void rebuildIndexesIfNeeded(force?: boolean): Promise } const brains: Brainy[] = [] const dirs: string[] = [] afterEach(async () => { for (const b of brains.splice(0)) await b.close().catch(() => {}) for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) vi.restoreAllMocks() }) async function warmBrain(): Promise<{ brain: Brainy; internals: BrainInternals }> { const brain = new Brainy(createTestConfig({ disableAutoRebuild: true })) await brain.init() brains.push(brain) for (let i = 0; i < 3; i++) { await brain.add({ data: `row ${i}`, type: NounType.Document, metadata: { i } }) } const internals = brain as unknown as BrainInternals return { brain, internals } } describe('the read gate honors EVERY provider’s not-ready report', () => { it('a not-ready METADATA provider refuses loudly — it never lets a read proceed, and it never rebuilds', async () => { const { internals } = await warmBrain() // The trap's shape: vector side looks fine (populated), metadata // provider says NOT ready — the OLD gate silently latched complete here. // The new gate refuses loudly instead; a read never triggers a rebuild. internals.metadataIndex.isReady = () => false const rebuildSpy = vi.spyOn(internals, 'rebuildIndexesIfNeeded').mockResolvedValue(undefined) expect(() => internals.ensureIndexesLoaded()).toThrow(MetadataIndexNotReadyError) expect(rebuildSpy, 'a read NEVER triggers a rebuild — building is entirely open()\'s job now').not.toHaveBeenCalled() }) it('control: all providers ready/unknown+populated → the gate lets the read through, no rebuild', async () => { const { internals } = await warmBrain() internals.metadataIndex.isReady = () => true const rebuildSpy = vi.spyOn(internals, 'rebuildIndexesIfNeeded').mockResolvedValue(undefined) expect(() => internals.ensureIndexesLoaded()).not.toThrow() expect(rebuildSpy).not.toHaveBeenCalled() }) }) describe('the open-time build honors the same law: a needed rebuild runs at open, never deferred to a read', () => { it('disableAutoRebuild:true does not defer a needed rebuild past open() on a reopened, populated store', async () => { const dir = mkdtempSync(join(tmpdir(), 'brainy-lazy-notready-honor-')) dirs.push(dir) const writer = new Brainy(createTestConfig({ disableAutoRebuild: true, storage: { type: 'filesystem', path: dir } })) await writer.init() for (let i = 0; i < 3; i++) { await writer.add({ data: `row ${i}`, type: NounType.Document, metadata: { i } }) } await writer.flush() await writer.close() // Fresh instance over the same store: its derived indexes start empty in // memory, so open()'s rebuildIndexesIfNeeded MUST fire (and complete) // before init() returns — even though disableAutoRebuild is true, there // is no first-query lazy path left to defer to. const reader = new Brainy(createTestConfig({ disableAutoRebuild: true, storage: { type: 'filesystem', path: dir } })) const internals = reader as unknown as { rebuildIndexesIfNeeded(force?: boolean): Promise } const rebuildSpy = vi.spyOn(internals, 'rebuildIndexesIfNeeded') await reader.init() brains.push(reader) expect(rebuildSpy).toHaveBeenCalledTimes(1) const rows = await reader.find({ where: { i: 1 } }) expect(rows.length).toBe(1) }, 30000) })