DP6/DP8 of the Path Registry (BRAINY-PROD-LATENCY-TRIAD, the proven flicker mechanism): update paths staged RemoveFromVectorIndex then AddToVectorIndex as two separately-awaited transaction ops — between them a live row was in NEITHER index (dark to semantic recall, fine in metadata list). The native pair widened that window to seconds in production before their side's visibility-commit fix; the structural cure lands here: - hnswIndex.updateItem: absent → add; SAME vector → pure no-op (the production shape — a type-only update re-indexed an unchanged vector, remove+add did pure damage); changed vector → the node NEVER leaves the index: synchronous vector swap first (every query from that instant sees correct distances), then unlink/relink at the node's existing level via shared internals (linkNode/unlinkNodeEdges refactored out of add/remove; entry point and maxLevel provably unchanged). - ReplaceInVectorIndexOperation: ONE transaction leg; feature-detects provider updateItem (native seam flagged — their side ships updateItem, then the adjacent remove+add fallback is dead code). Both update staging sites swapped; delete sites untouched. - LAZY-OPEN GATE (fleet adoption find, SELF-ENGINE-PAIR-STANDARD): under disableAutoRebuild, ensureIndexesLoaded assessed ONLY the vector index — a not-ready native METADATA provider never blocked the completion latch and every find() silently returned [] on a populated store. All three providers now vote; any not-ready report falls through to the rebuild. - docs/path-registry.md: brainy's twin table for the 32 shared path IDs — service class, budgets, lifecycle, narration, and the cited pin per row; owed rows named (LC4 doors-open migration, MT4 yielding heals, LC7 downgrade contract) per the lifecycle-sprint choreography. Pins: update-item-atomic 9/9 (visibility-atomic swap, reverse-index parity vs fresh rebuild, entry-point invariants) · lazy-notready-honor 2/2. Gates: unit 1928/1928 (148 files) · integration 760 · conformance 27/27.
75 lines
3 KiB
TypeScript
75 lines
3 KiB
TypeScript
/**
|
||
* @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
|
||
* 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.
|
||
*
|
||
* 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 { 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>
|
||
rebuildIndexesIfNeeded(force?: boolean): Promise<void>
|
||
}
|
||
|
||
const brains: Brainy[] = []
|
||
|
||
afterEach(async () => {
|
||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||
vi.restoreAllMocks()
|
||
})
|
||
|
||
async function warmLazyBrain(): 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
|
||
internals.lazyRebuildCompleted = false // simulate the cold first query
|
||
return { brain, internals }
|
||
}
|
||
|
||
describe('lazy path honors EVERY provider’s not-ready report', () => {
|
||
it('a not-ready METADATA provider blocks the completion latch and fires the rebuild', async () => {
|
||
const { internals } = await warmLazyBrain()
|
||
|
||
// 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)
|
||
|
||
await internals.ensureIndexesLoaded()
|
||
|
||
expect(rebuildSpy, 'not-ready metadata provider must fire the lazy rebuild').toHaveBeenCalledWith(true)
|
||
})
|
||
|
||
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()
|
||
|
||
expect(rebuildSpy).not.toHaveBeenCalled()
|
||
expect(internals.lazyRebuildCompleted).toBe(true)
|
||
})
|
||
})
|