brainy/tests/unit/get-index-status-readiness.test.ts
David Snelling d0f69c731f fix: honest index readiness — no silently-empty queries on a cold index
Closes the "dishonest readiness proxy" anti-pattern (Pattern A): size()>0 /
isInitialized were treated as "this index serves queries", but a cold native
index can load its COUNT before its SERVING structure, so a query returned a
silent [] indistinguishable from "no such data". A shared assessIndexReadiness()
now reads only the provider's honest isReady() signal (never size()), applied at
every site:

- Vector: a one-shot verifyVectorLive() guard on the semantic/proximity search
  path (a pure semantic find({query}) has no filter, so nothing guarded it). It
  prefers isReady(), else a known-vector self-match probe; self-heals via rebuild
  or throws the new VectorIndexNotReadyError instead of a silent [].
- Graph: getVerbsBySource/ByTarget skip the fast path when the provider reports
  not-ready (falling to the canonical shard scan), plus a one-shot probe that
  self-heals a no-isReady provider whose adjacency did not cold-load.
- getIndexStatus(): folds in per-index honest `ready` (making `populated`
  honest) + rebuildFailed/rebuildError/degradedIds, so a readiness probe never
  200s a brain that is still warming up or degraded.

Unblocked by the native providers now reporting serving-truth (graph via
SSTable-residency readiness, vector via durableBaseLoadFailed). New export:
VectorIndexNotReadyError. getIndexStatus gains additive fields. No breaking API.
13 new tests; existing readiness guards green.
2026-07-13 13:17:56 -07:00

59 lines
2.4 KiB
TypeScript

/**
* @module tests/unit/get-index-status-readiness
* @description Pattern-A / Finding 9: getIndexStatus reported `populated: size>0`
* and only `migrating`, so a native index that loaded its count but not its
* serving structure reported populated:true — a k8s readiness probe would 200 a
* brain that serves []. It now folds in the honest isReady() signal + the
* _indexRebuildFailed / _indexDegradedIds degraded states (mirroring
* validateIndexConsistency / checkHealth).
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy, NounType } from '../../src/index.js'
describe('getIndexStatus honest readiness (Finding 9)', () => {
let brain: any
beforeEach(async () => {
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, dimensions: 384 })
await brain.init()
await brain.add({ data: 'x', type: NounType.Concept })
await brain.flush()
})
it('a not-ready provider makes populated honest (false) and exposes ready:false', async () => {
brain.index.isReady = () => false // count present, serving structure NOT loaded
const status = await brain.getIndexStatus()
expect(status.hnswIndex.populated).toBe(false)
expect(status.hnswIndex.ready).toBe(false)
delete brain.index.isReady
})
it('a ready provider reports populated:true + ready:true', async () => {
brain.index.isReady = () => true
const status = await brain.getIndexStatus()
expect(status.hnswIndex.populated).toBe(true)
expect(status.hnswIndex.ready).toBe(true)
delete brain.index.isReady
})
it('rebuildFailed / rebuildError surface the init-degraded state', async () => {
brain._indexRebuildFailed = new Error('boom')
const status = await brain.getIndexStatus()
expect(status.rebuildFailed).toBe(true)
expect(status.rebuildError).toBe('boom')
brain._indexRebuildFailed = null
})
it('degradedIds surfaces the adopt-forward degraded set', async () => {
brain._indexDegradedIds.add('00000000-0000-4000-8000-0000000000de')
const status = await brain.getIndexStatus()
expect(status.degradedIds).toBe(1)
brain._indexDegradedIds.clear()
})
it('a JS-baseline provider (no isReady) omits ready and falls back to size>0', async () => {
const status = await brain.getIndexStatus()
expect(status.hnswIndex.ready).toBeUndefined()
expect(status.hnswIndex.populated).toBe(brain.index.size() > 0)
})
})