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).
153 lines
6.5 KiB
TypeScript
153 lines
6.5 KiB
TypeScript
/**
|
|
* @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)
|
|
})
|
|
})
|