Some checks are pending
CI / Node 22 (push) Waiting to run
CI / Node 24 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run
id-normalization.test.ts's makeBrain() and degraded-reads-surfaced.test.ts's per-test brains had nothing tracking them — both now use a describe-scoped opened[] array drained by afterEach. find-hybrid-filter-before-hydrate.test.ts had two beforeAll-built brains (one per describe block) with no matching afterAll. multi-process-safety.test.ts and plugin-autodetect.test.ts/plugin.test.ts left a brain whose init() was expected to reject (a rejected init() still registers the instance in Brainy's global instance registry — the constructor does that unconditionally — so it still needs close() to deregister, or the process-level shutdown hooks never see the registry go idle for the rest of the run).
87 lines
3.8 KiB
TypeScript
87 lines
3.8 KiB
TypeScript
/**
|
|
* @module tests/unit/brainy/degraded-reads-surfaced
|
|
* @description Finding 10: a known-degraded derived index must be SURFACED, not
|
|
* silently served as authoritative. Two degraded sources:
|
|
* (a) a non-fatal index rebuild failure at init() (`_indexRebuildFailed`), and
|
|
* (b) an adopt-forward failed-rollback recovery (`commitSingleOp`'s `degraded`
|
|
* ids, previously dropped on the floor by persistSingleOp).
|
|
* Both now fold into checkHealth()/validateIndexConsistency() and emit ONE loud
|
|
* read-path warning per degraded window; repairIndex() reconciles + clears them.
|
|
*
|
|
* The private fields are the observable contract of the fix, so the test drives
|
|
* them directly (a real failed rollback is exercised elsewhere).
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { Brainy } from '../../../src/brainy.js'
|
|
import { NounType } from '../../../src/types/graphTypes.js'
|
|
import { prodLog } from '../../../src/utils/logger.js'
|
|
|
|
const UUID = (suffix: string): string => `00000000-0000-4000-8000-0000000000${suffix}`
|
|
|
|
describe('Finding 10 — degraded derived-index state is surfaced on reads', () => {
|
|
const opened: Brainy[] = []
|
|
|
|
beforeEach(() => {
|
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
|
})
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks()
|
|
for (const b of opened.splice(0)) await b.close().catch(() => {})
|
|
})
|
|
|
|
it('checkHealth() reports adopt-forward degraded ids as unhealthy', async () => {
|
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
|
opened.push(brain)
|
|
await brain.init()
|
|
;(brain as any)._indexDegradedIds.add(UUID('de'))
|
|
|
|
const health = await brain.checkHealth()
|
|
expect(health.healthy).toBe(false)
|
|
expect(health.recommendation).toMatch(/repairIndex\(\)/)
|
|
})
|
|
|
|
it('find()/get() warn loudly while degraded, ONCE, then repairIndex() clears it', async () => {
|
|
const warn = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
|
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
|
opened.push(brain)
|
|
await brain.init()
|
|
await brain.add({ id: UUID('a1'), data: 'x', type: NounType.Document })
|
|
;(brain as any)._indexRebuildFailed = new Error('rebuild boom')
|
|
|
|
await brain.find({ type: NounType.Document })
|
|
await brain.get(UUID('a1')) // second read: must NOT double-warn
|
|
const degradedWarns = warn.mock.calls.filter((c) =>
|
|
String(c[0]).includes('derived index is INCOMPLETE')
|
|
)
|
|
expect(degradedWarns.length).toBe(1)
|
|
|
|
await brain.repairIndex()
|
|
expect((brain as any)._indexRebuildFailed).toBeNull()
|
|
expect((brain as any)._indexDegradedIds.size).toBe(0)
|
|
|
|
warn.mockClear()
|
|
await brain.find({ type: NounType.Document })
|
|
expect(warn.mock.calls.filter((c) => String(c[0]).includes('INCOMPLETE')).length).toBe(0)
|
|
})
|
|
|
|
it('persistSingleOp records receipt.degraded (widened return type, not dropped)', async () => {
|
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
|
opened.push(brain)
|
|
await brain.init()
|
|
// Simulate a degraded receipt by wrapping the generation store's commitSingleOp.
|
|
const gs: any = (brain as any).generationStore
|
|
const realCommit = gs.commitSingleOp.bind(gs)
|
|
vi.spyOn(gs, 'commitSingleOp').mockImplementation(async (args: any) => {
|
|
const r = await realCommit(args)
|
|
return { ...r, degraded: [...(args.touched.nouns ?? [])] }
|
|
})
|
|
|
|
await brain.add({ id: UUID('b2'), data: 'y', type: NounType.Document })
|
|
expect((brain as any)._indexDegradedIds.size).toBeGreaterThan(0)
|
|
expect((brain as any)._indexDegradedIds.has(UUID('b2'))).toBe(true)
|
|
|
|
const health = await brain.checkHealth()
|
|
expect(health.healthy).toBe(false)
|
|
expect(health.recommendation).toMatch(/repairIndex\(\)/)
|
|
})
|
|
})
|