open-brainy/tests/unit/brainy/lazy-notready-honor.test.ts
David Snelling f8f64780b1
All checks were successful
CI / Node 22 (push) Successful in 12m19s
CI / Node 24 (push) Successful in 12m16s
CI / Integration + conformance (Node 22) (push) Successful in 18m41s
CI / Bun (latest) (push) Successful in 12m20s
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:

- the first-read lazy build retires (open builds instead, regardless of
  size — the ≥10k deferral and the "lazy loading on first query" branch go;
  disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
  ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
  one explicit door: rebuilds the named leg unconditionally and reports
  rebuilt per family; bare repairIndex() stays report-driven.

test(lifecycle): the biography lane — a store's whole life, refereed

tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00

109 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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<void>
}
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 providers 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<void> }
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)
})