feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
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

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).
This commit is contained in:
David Snelling 2026-08-24 12:45:51 -07:00
parent a8b5ca0c8f
commit f8f64780b1
19 changed files with 2160 additions and 652 deletions

View file

@ -1,38 +1,49 @@
/**
* @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 lazy
* 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). The law: a not-ready report from ANY provider falls through
* to the rebuild never a silent empty.
* 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 { Brainy } from '../../../src/index.js'
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 }
lazyRebuildCompleted: boolean
ensureIndexesLoaded(): Promise<void>
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 warmLazyBrain(): Promise<{ brain: Brainy; internals: BrainInternals }> {
async function warmBrain(): Promise<{ brain: Brainy; internals: BrainInternals }> {
const brain = new Brainy(createTestConfig({ disableAutoRebuild: true }))
await brain.init()
brains.push(brain)
@ -40,36 +51,59 @@ async function warmLazyBrain(): Promise<{ brain: Brainy; internals: BrainInterna
await brain.add({ data: `row ${i}`, type: NounType.Document, metadata: { i } })
}
const internals = brain as unknown as BrainInternals
internals.lazyRebuildCompleted = false // simulate the cold first query
return { brain, internals }
}
describe('lazy path honors EVERY providers not-ready report', () => {
it('a not-ready METADATA provider blocks the completion latch and fires the rebuild', async () => {
const { internals } = await warmLazyBrain()
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 latched complete here.
;(internals.metadataIndex as { isReady?: () => boolean }).isReady = () => false
const rebuildSpy = vi
.spyOn(internals, 'rebuildIndexesIfNeeded')
.mockResolvedValue(undefined)
// 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)
await internals.ensureIndexesLoaded()
expect(rebuildSpy, 'not-ready metadata provider must fire the lazy rebuild').toHaveBeenCalledWith(true)
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 → latch completes, no rebuild', async () => {
const { internals } = await warmLazyBrain()
;(internals.metadataIndex as { isReady?: () => boolean }).isReady = () => true
const rebuildSpy = vi
.spyOn(internals, 'rebuildIndexesIfNeeded')
.mockResolvedValue(undefined)
await internals.ensureIndexesLoaded()
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()
expect(internals.lazyRebuildCompleted).toBe(true)
})
})
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)
})

View file

@ -1,15 +1,18 @@
/**
* @module tests/unit/brainy/metadata-provider-contract
* @description Brainy-side wiring of the two metadata-provider contract additions
* confirmed with cor for the lockstep:
* @description Brainy-side wiring of the metadata-provider contract.
*
* 1. `probeConsistency()` an OPTIONAL O(1) cold-open consistency sampler. On the
* first read, brainy calls it once; on `false` it self-heals via
* `detectAndRepairCorruption()` (the metadata counterpart of the graph cold-load
* guard). The native provider implements it; the JS index omits it (no-op).
* 2. `getIdsForFilter(filter, opts?)` brainy passes a page bound on the UNSORTED
* `find({ type, where, limit })` path so a native provider can early-stop. The JS
* index ignores `opts`.
* `getIdsForFilter(filter, opts?)` brainy passes a page bound on the UNSORTED
* `find({ type, where, limit })` path so a native provider can early-stop. The JS
* index ignores `opts`.
*
* RETIRED (health-gate law): `probeConsistency()` / `ensureMetadataConsistencyProbed()`
* a read-time consistency probe that launches `detectAndRepairCorruption()` on
* `false` was exactly the read-triggered dark rebuild the law forbids (a read must
* never start a store walk or a rebuild). The probe's diagnostic value lives on in
* `validateIndexConsistency()` / `repairIndex()`, which remain explicit, operator-invoked
* calls. The pin below confirms the retirement: `probeConsistency()` is never called by
* a read, even when a provider exposes it.
*
* These are unit tests of brainy's CALL behaviour (the real end-to-end honoring is
* exercised by cor's combined matrix); they inject probe/spy hooks onto the live JS
@ -19,7 +22,7 @@ import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { NounType } from '../../../src/types/graphTypes'
describe('metadata-provider contract wiring (probeConsistency + getIdsForFilter opts)', () => {
describe('metadata-provider contract wiring (getIdsForFilter opts)', () => {
let brain: Brainy<any>
let mi: any
@ -29,47 +32,23 @@ describe('metadata-provider contract wiring (probeConsistency + getIdsForFilter
await brain.add({ data: 'a', type: NounType.Thing, metadata: { kind: 'x' } })
await brain.add({ data: 'b', type: NounType.Thing, metadata: { kind: 'y' } })
mi = (brain as any).metadataIndex
;(brain as any)._metadataConsistencyProbed = false // reset the one-shot guard
})
it('calls probeConsistency once on cold open and self-heals via detectAndRepairCorruption on false', async () => {
it('RETIRED: a read never calls probeConsistency() / self-heals via detectAndRepairCorruption — that is the read-triggered dark rebuild the health-gate law forbids', async () => {
let probes = 0
let repairs = 0
mi.probeConsistency = async () => { probes++; return false } // corrupt → must repair
mi.probeConsistency = async () => { probes++; return false } // would-be corrupt signal
const origRepair = mi.detectAndRepairCorruption.bind(mi)
mi.detectAndRepairCorruption = async () => { repairs++; return origRepair() }
await brain.find({ where: { kind: 'x' } })
expect(probes).toBe(1)
expect(repairs).toBe(1)
// Second read must NOT re-probe (once per brain).
await brain.find({ where: { kind: 'y' } })
expect(probes).toBe(1)
expect(repairs).toBe(1)
})
it('does NOT repair when the probe reports healthy', async () => {
let repairs = 0
mi.probeConsistency = async () => true // clean
const origRepair = mi.detectAndRepairCorruption.bind(mi)
mi.detectAndRepairCorruption = async () => { repairs++; return origRepair() }
expect(probes).toBe(0) // no read-time probe exists anymore
expect(repairs).toBe(0) // and therefore no read-triggered self-heal either
await brain.find({ where: { kind: 'x' } })
expect(repairs).toBe(0)
})
it('a probe failure never breaks the read (best-effort, retried next time)', async () => {
let probes = 0
mi.probeConsistency = async () => { probes++; throw new Error('probe boom') }
// The read still succeeds despite the throwing probe.
const rows = await brain.find({ where: { kind: 'x' } })
expect(rows.length).toBe(1)
expect(probes).toBe(1)
// Guard reset on failure → the next read retries the probe.
await brain.find({ where: { kind: 'y' } })
expect(probes).toBe(2)
delete mi.probeConsistency
mi.detectAndRepairCorruption = origRepair
})
it('passes a page bound to getIdsForFilter on the unsorted find path (offset 0, brainy re-windows)', async () => {

View file

@ -25,7 +25,7 @@
*/
import { describe, it, expect, afterEach, vi } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { Brainy, VectorIndexNotReadyError } from '../../../src/index.js'
import { NounType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'
import { BaseStorage } from '../../../src/storage/baseStorage.js'
@ -43,9 +43,8 @@ interface BrainInternals {
metadataIndex: { rebuild(...a: unknown[]): Promise<unknown> }
graphIndex: { size(): number; rebuild(...a: unknown[]): Promise<unknown> }
_indexEpochStale: boolean
lazyRebuildCompleted: boolean
rebuildIndexesIfNeeded(force?: boolean): Promise<void>
ensureIndexesLoaded(): Promise<void>
ensureIndexesLoaded(): void
storage: { readRawObject(p: string): Promise<unknown> }
}
@ -181,40 +180,40 @@ describe('rc.8 no-freeze migration deference (isMigrating / stampBrainFormat / b
expect(idxSpy).toHaveBeenCalledTimes(1)
})
// --- Hook 1: large-path first-query lazy force-rebuild deference ----------
// --- Hook 1: read-gate deference (RE-POINTED — the health-gate law retired
// the first-query lazy force-rebuild entirely: ensureIndexesLoaded() is now
// a pure CHECK that never calls rebuildIndexesIfNeeded, migrating or not.
// What survives from the original law is the DEFERENCE itself: a migrating
// provider's report is never judged by the gate — it neither throws nor
// rebuilds — while the exact same not-ready report on a NON-migrating
// provider throws the typed error instead of ever rebuilding.) ------------
it('lazy first-query force-rebuild is SKIPPED when the vector provider isMigrating()', async () => {
// disableAutoRebuild routes first queries through ensureIndexesLoaded() (the
// large-brain lazy path that would otherwise force a blocking rebuild).
it('the read gate defers to a migrating vector provider — a not-ready report neither throws nor rebuilds', async () => {
const brain = await makeWarmBrain(2, { disableAutoRebuild: true })
const internals = internalsOf(brain)
const rebuildSpy = vi.spyOn(internals, 'rebuildIndexesIfNeeded').mockResolvedValue(undefined)
// Simulate a cold/empty live vector index (cor is mid-swap, serving canonical).
vi.spyOn(internals.index, 'size').mockReturnValue(0)
internals.lazyRebuildCompleted = false
// Simulate a not-ready live vector index (cor is mid-swap, serving canonical).
;(internals.index as unknown as { isReady?: () => boolean }).isReady = () => false
setMigrating(internals.index, true)
await internals.ensureIndexesLoaded()
// A query during cor's background swap must not trigger brainy's blocking rebuild.
expect(() => internals.ensureIndexesLoaded()).not.toThrow()
// A query during cor's background swap must not trigger brainy's own
// rebuild — reads never rebuild in any case, migrating or not.
expect(rebuildSpy).toHaveBeenCalledTimes(0)
})
it('lazy first-query force-rebuild STILL fires when the vector provider is not migrating (control)', async () => {
it('the read gate THROWS for the same not-ready vector provider once migration clears (control)', async () => {
const brain = await makeWarmBrain(2, { disableAutoRebuild: true })
const internals = internalsOf(brain)
const rebuildSpy = vi.spyOn(internals, 'rebuildIndexesIfNeeded').mockResolvedValue(undefined)
vi.spyOn(internals.index, 'size').mockReturnValue(0)
internals.lazyRebuildCompleted = false
;(internals.index as unknown as { isReady?: () => boolean }).isReady = () => false
// No isMigrating → not deferring.
await internals.ensureIndexesLoaded()
// Without deference, the cold empty index drives the lazy force-rebuild.
expect(rebuildSpy).toHaveBeenCalledTimes(1)
expect(rebuildSpy).toHaveBeenCalledWith(true)
expect(() => internals.ensureIndexesLoaded()).toThrow(VectorIndexNotReadyError)
// Still never rebuilds — the gate refuses loudly instead.
expect(rebuildSpy).toHaveBeenCalledTimes(0)
})
// --- Hook 2: public stampBrainFormat() -----------------------------------

View file

@ -3,10 +3,14 @@
* reported cold `find({ where })` returning a silent `[]` on a freshly-opened
* brain (a native metadata index that reports data but has not loaded its field
* postings). This guard, the field-index counterpart of verifyGraphAdjacencyLive,
* probes a known persisted value on the first filtered find(): if the index does
* not serve it, brainy rebuilds and re-probes, and raises a loud
* MetadataIndexNotReadyError only if the rebuild still can't serve never a
* silent empty result that misrepresents existing data.
* probes a known persisted value on the first filtered find().
*
* RE-POINTED to the health-gate law: the guard NEVER rebuilds and NEVER walks
* the store from a read a read-path rebuild is exactly the dark-rebuild
* failure mode the law retires (open() alone owns building). When the probe
* cannot serve the known value it raises a loud MetadataIndexNotReadyError
* IMMEDIATELY, with no rebuild attempt in between never a silent empty
* result that misrepresents existing data.
*
* The 8.0 JS index cold-loads correctly, so we simulate the cold native failure
* mode by intercepting the provider's getIdsForFilter/rebuild.
@ -42,37 +46,19 @@ describe('Metadata cold-read guard (#venue silent-[])', () => {
mi.rebuild = origRebuild
})
it('cold index: verifyMetadataLive self-heals via rebuild — find({where}) is correct, NOT silent []', async () => {
it('cold index: verifyMetadataLive REFUSES immediately — find({where}) throws MetadataIndexNotReadyError, NEVER a silent [], and NEVER a rebuild attempt', async () => {
const mi = brain.metadataIndex
const origGetIds = mi.getIdsForFilter.bind(mi)
let rebuilds = 0
const origRebuild = mi.rebuild.bind(mi)
let cold = true
brain._metadataVerified = false // re-arm the one-shot for this scenario
mi.getIdsForFilter = async (...a: any[]) => (cold ? [] : origGetIds(...a))
mi.rebuild = async () => {
await origRebuild()
cold = false // the rebuild warms the postings
}
try {
const res = await brain.find({ where: { status: 'active' }, limit: 100 })
expect(res.length).toBe(1) // self-healed — the known entity is returned
} finally {
mi.getIdsForFilter = origGetIds
mi.rebuild = origRebuild
}
})
it('unrecoverably cold index: find({where}) throws MetadataIndexNotReadyError — never a silent []', async () => {
const mi = brain.metadataIndex
const origGetIds = mi.getIdsForFilter.bind(mi)
const origRebuild = mi.rebuild.bind(mi)
brain._metadataVerified = false
mi.getIdsForFilter = async () => [] // always cold; rebuild can't fix it
mi.rebuild = async () => {}
mi.getIdsForFilter = async () => [] // cold: the known value never resolves
mi.rebuild = async () => { rebuilds++; return origRebuild() }
try {
await expect(brain.find({ where: { status: 'active' }, limit: 100 })).rejects.toBeInstanceOf(
MetadataIndexNotReadyError
)
expect(rebuilds).toBe(0) // the guard never rebuilds from a read — it refuses loudly instead
} finally {
mi.getIdsForFilter = origGetIds
mi.rebuild = origRebuild

View file

@ -63,6 +63,9 @@ function inGate(rel: string): boolean {
return (
rel.startsWith('tests/unit/') ||
rel.startsWith('tests/integration/') ||
// The lifecycle biography lane — included by the integration config
// ('tests/lifecycle/**/*.test.ts'; see tests/lifecycle/README.md).
rel.startsWith('tests/lifecycle/') ||
rel.endsWith('.unit.test.ts') ||
rel.endsWith('.integration.test.ts')
)

View file

@ -0,0 +1,153 @@
/**
* @module tests/unit/utils/indexReadiness
* @description Pins for the read-gate authority, {@link assessProviderHealth}, and
* its older sibling {@link assessIndexReadiness}. The health-gate law: a provider's
* NAMED, synchronous, O(1) health report when exposed REPLACES the `isReady()`/
* size-heuristic fallback as the read gate's source of truth. A throw from
* `healthReport()` is a CONTRACT VIOLATION (never read as healthy, never swallowed
* into "unknown"); an `unledgered` family is UNKNOWN (never healthy, never broken
* `serving` is always the provider's own verdict, verbatim).
*/
import { describe, it, expect } from 'vitest'
import { assessIndexReadiness, assessProviderHealth } from '../../../src/utils/indexReadiness.js'
import type { HealthReport, LedgerInvariantResult } from '../../../src/plugin.js'
function invariant(overrides: Partial<LedgerInvariantResult> = {}): LedgerInvariantResult {
return {
name: 'manifest-residency',
holds: true,
detail: 'ok',
heal: 'none',
source: 'ledger',
...overrides
}
}
function report(overrides: Partial<HealthReport> = {}): HealthReport {
return {
provider: 'vector',
healthy: true,
serving: true,
invariants: [],
checkedAt: Date.now(),
durationMs: 1,
generation: 1,
unledgered: [],
...overrides
}
}
describe('assessIndexReadiness (legacy isReady() classifier)', () => {
it('unknown when the provider is null/undefined', () => {
expect(assessIndexReadiness(null)).toBe('unknown')
expect(assessIndexReadiness(undefined)).toBe('unknown')
})
it('unknown when isReady() is absent', () => {
expect(assessIndexReadiness({})).toBe('unknown')
})
it('ready / not-ready mirror isReady()', () => {
expect(assessIndexReadiness({ isReady: () => true })).toBe('ready')
expect(assessIndexReadiness({ isReady: () => false })).toBe('not-ready')
})
})
describe('assessProviderHealth — the read-gate authority', () => {
it('via "none": no provider at all', () => {
const a = assessProviderHealth(null)
expect(a.via).toBe('none')
expect(a.readiness).toBe('unknown')
expect(a.report).toBeNull()
expect(a.reasons.length).toBeGreaterThan(0)
})
it('via "size-heuristic": provider exposes neither healthReport() nor isReady()', () => {
const a = assessProviderHealth({})
expect(a.via).toBe('size-heuristic')
expect(a.readiness).toBe('unknown')
expect(a.report).toBeNull()
})
it('via "is-ready": provider exposes isReady() but no healthReport() — ready', () => {
const a = assessProviderHealth({ isReady: () => true })
expect(a.via).toBe('is-ready')
expect(a.readiness).toBe('ready')
expect(a.reasons).toEqual([])
})
it('via "is-ready": isReady() === false — not-ready with a reason', () => {
const a = assessProviderHealth({ isReady: () => false })
expect(a.via).toBe('is-ready')
expect(a.readiness).toBe('not-ready')
expect(a.reasons.length).toBeGreaterThan(0)
})
it('healthReport() present REPLACES isReady() — serving:true wins even if isReady() lies false', () => {
const p = { isReady: () => false, healthReport: () => report({ serving: true }) }
const a = assessProviderHealth(p)
expect(a.via).toBe('health-report')
expect(a.readiness).toBe('ready')
})
it('serving:true, healthy:true, no invariants failing → ready, no reasons', () => {
const p = { healthReport: () => report({ serving: true, healthy: true }) }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('ready')
expect(a.reasons).toEqual([])
expect(a.report).toEqual(report({ serving: true, healthy: true }))
})
it('serving:false with a named heal:"rebuild" failing invariant → not-ready, reason names it', () => {
const failing = invariant({ name: 'posted-count-floor', holds: false, heal: 'rebuild', detail: 'posted 10 < canonical 20' })
const p = { healthReport: () => report({ serving: false, healthy: false, invariants: [failing] }) }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('not-ready')
expect(a.reasons.some((r) => r.includes('posted-count-floor') && r.includes('heal:rebuild') && r.includes('posted 10 < canonical 20'))).toBe(true)
})
it('unledgered-only report (serving:true, no failing invariant) → ready, reason names the unledgered family', () => {
const p = { healthReport: () => report({ serving: true, healthy: true, unledgered: ['canonical-verb-coverage'] }) }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('ready')
expect(a.reasons.some((r) => r.includes('unledgered') && r.includes('canonical-verb-coverage'))).toBe(true)
})
it('UNLEDGERED IS UNKNOWN: an unledgered family never flips a NOT-serving provider to ready', () => {
const failing = invariant({ holds: false, heal: 'rebuild', name: 'x' })
const p = { healthReport: () => report({ serving: false, healthy: false, invariants: [failing], unledgered: ['some-family'] }) }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('not-ready')
})
it('serving:true, healthy:false with a heal:"repair" failure → still ready (degraded-but-serving)', () => {
const failing = invariant({ name: 'stale-counter', holds: false, heal: 'repair', detail: 'counter drift' })
const p = { healthReport: () => report({ serving: true, healthy: false, invariants: [failing] }) }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('ready')
expect(a.reasons.some((r) => r.includes('stale-counter') && r.includes('heal:repair'))).toBe(true)
})
it('healthReport() that THROWS is a CONTRACT VIOLATION: not-ready, via health-report, reason names the throw — never "unknown"', () => {
const p = { healthReport: () => { throw new Error('mmap window busy') } }
const a = assessProviderHealth(p)
expect(a.via).toBe('health-report')
expect(a.readiness).toBe('not-ready')
expect(a.report).toBeNull()
expect(a.reasons.some((r) => r.includes('mmap window busy'))).toBe(true)
expect(a.readiness).not.toBe('unknown')
})
it('healthReport() that throws a non-Error value still produces a named reason (String(err))', () => {
const p = { healthReport: () => { throw 'boom' } }
const a = assessProviderHealth(p)
expect(a.readiness).toBe('not-ready')
expect(a.reasons.some((r) => r.includes('boom'))).toBe(true)
})
it('the returned report carries the generation for narration dedup', () => {
const p = { healthReport: () => report({ generation: 42 }) }
const a = assessProviderHealth(p)
expect(a.report?.generation).toBe(42)
})
})

View file

@ -3,9 +3,14 @@
* @description Pattern-A / Finding 1: a pure semantic find({ query }) has no
* filter, so verifyMetadataLive never fires nothing guarded the vector index.
* A cold native vector index that loaded its COUNT but not its serving structure
* returned a silent []. verifyVectorLive() closes that: honest isReady() first,
* else a known-vector self-match probe; self-heal (rebuild) or throw
* VectorIndexNotReadyError never a silent empty result.
* returned a silent []. verifyVectorLive() closes that: the health-report/isReady()
* authority first, else a known-vector self-match probe.
*
* RE-POINTED to the health-gate law: the guard NEVER rebuilds and NEVER walks
* the store from a read a read-path rebuild is exactly the dark-rebuild
* failure mode the law retires (open() alone owns building). A not-serving
* signal (from either strategy) THROWS VectorIndexNotReadyError immediately,
* with no rebuild attempt in between never a silent empty result.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy, NounType, VectorIndexNotReadyError } from '../../src/index.js'
@ -34,50 +39,37 @@ describe('Vector cold-read guard (verifyVectorLive) — silent-[] on cold semant
vi.rebuild = origRebuild
})
it('cold index: verifyVectorLive self-heals via rebuild — semantic find is correct, NOT silent []', async () => {
const vi = brain.index
const origSearch = vi.search.bind(vi)
const origRebuild = vi.rebuild.bind(vi)
let cold = true
brain._vectorVerified = false
// size()>0 (count present) but search returns nothing until a rebuild warms it.
vi.search = async (...a: any[]) => (cold ? [] : origSearch(...a))
vi.rebuild = async (...a: any[]) => { await origRebuild(...a); cold = false }
try {
const res = await brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
expect(res.length).toBeGreaterThan(0) // self-healed
} finally {
vi.search = origSearch; vi.rebuild = origRebuild
}
})
it('unrecoverably cold index: semantic find throws VectorIndexNotReadyError', async () => {
it('cold index (no isReady()): verifyVectorLive REFUSES immediately — throws VectorIndexNotReadyError, NEVER rebuilds', async () => {
const vi = brain.index
const origSearch = vi.search.bind(vi)
let rebuilds = 0
const origRebuild = vi.rebuild.bind(vi)
brain._vectorVerified = false
vi.search = async () => [] // always cold; rebuild can't fix it
vi.rebuild = async () => {}
// size()>0 (count present) but search never returns a hit for the known vector.
vi.search = async () => []
vi.rebuild = async (...a: any[]) => { rebuilds++; return origRebuild(...a) }
try {
await expect(
brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
).rejects.toBeInstanceOf(VectorIndexNotReadyError)
expect(rebuilds).toBe(0) // the guard never rebuilds from a read — it refuses loudly instead
} finally {
vi.search = origSearch; vi.rebuild = origRebuild
}
})
it('native provider reporting isReady()===false rebuilds, then serves', async () => {
it('native provider reporting isReady()===false THROWS immediately — never rebuilds', async () => {
const vi = brain.index
let rebuilds = 0
const origRebuild = vi.rebuild.bind(vi)
let ready = false
brain._vectorVerified = false
vi.isReady = () => ready
vi.rebuild = async (...a: any[]) => { await origRebuild(...a); ready = true }
vi.isReady = () => false
vi.rebuild = async (...a: any[]) => { rebuilds++; return origRebuild(...a) }
try {
const res = await brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
expect(ready).toBe(true) // rebuild ran because isReady() was false
expect(res).toBeDefined()
await expect(
brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
).rejects.toBeInstanceOf(VectorIndexNotReadyError)
expect(rebuilds).toBe(0) // a not-ready report throws immediately — it is never a rebuild trigger
} finally {
delete vi.isReady; vi.rebuild = origRebuild
}