/** * @module tests/unit/brainy/graph-adjacency-cold-load * @description 7.33.2 — the graph-adjacency cold-load consistency guard. A native * graph provider can load its relationship COUNT (manifest) on a cold open but fail * to load the source→target adjacency (observed with cor 2.7.5's NativeGraphAdjacencyIndex * on mmap-filesystem: the SSTable-segment load is swallowed). The result is * `find({ connected })` / `neighbors()` / `related()` silently returning `[]` despite * persisted edges. Brainy now probes one known persisted edge on the first graph read; * if the index claims edges but the source resolves to none, it forces a rebuild from * storage, and only if even that fails does it throw a loud, catchable error instead of * serving `[]` as truth. These exercise the guard against a stubbed graph index. */ import { describe, it, expect } from 'vitest' import { Brainy } from '../../../src/index.js' import { GraphIndexNotReadyError } from '../../../src/errors/brainyError.js' import { createTestConfig } from '../../helpers/test-factory.js' /** Replace the brain's graph index with a stub that models a (possibly broken) cold load. */ function stubGraphIndex( brain: any, opts: { size: number; neighborsBeforeRebuild: string[]; neighborsAfterRebuild?: string[] } ) { let rebuilt = false const calls = { rebuild: 0, getNeighbors: 0 } brain.graphIndex = { size: () => opts.size, getNeighbors: async () => { calls.getNeighbors++ return rebuilt ? opts.neighborsAfterRebuild ?? [] : opts.neighborsBeforeRebuild }, rebuild: async () => { calls.rebuild++ rebuilt = true }, flush: async () => {}, close: async () => {} } return calls } async function freshBrain(): Promise { const brain: any = new Brainy(createTestConfig()) await brain.init() brain._graphAdjacencyVerified = false return brain } describe('7.33.2 graph-adjacency cold-load consistency guard', () => { it('is a no-op when the adjacency is live (a persisted edge resolves to neighbors)', async () => { const brain = await freshBrain() const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: ['n1'] }) brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false }) await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(0) await brain.close() }) it('force-rebuilds when the index reports edges but the source probes empty', async () => { const brain = await freshBrain() const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [], neighborsAfterRebuild: ['n1'] }) brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false }) await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(1) // detected the empty adjacency + rebuilt it // Cached: a second call does not re-probe or re-rebuild. await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(1) await brain.close() }) it('throws GraphIndexNotReadyError when even a rebuild cannot load the adjacency', async () => { const brain = await freshBrain() stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [], neighborsAfterRebuild: [] }) // never loads brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false }) await expect(brain.verifyGraphAdjacencyLive()).rejects.toBeInstanceOf(GraphIndexNotReadyError) await brain.close() }) it('no-op when no edges are claimed (size 0) — never even probes storage', async () => { const brain = await freshBrain() const calls = stubGraphIndex(brain, { size: 0, neighborsBeforeRebuild: [] }) let probedStorage = false brain.storage.getVerbs = async () => { probedStorage = true return { items: [], hasMore: false } } await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(0) expect(probedStorage).toBe(false) await brain.close() }) it('no-op when storage has no edges (stale count) — does not rebuild or throw', async () => { const brain = await freshBrain() const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [] }) brain.storage.getVerbs = async () => ({ items: [], hasMore: false }) await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(0) await brain.close() }) it('does not loop or break the query on a transient probe failure (re-checks next time)', async () => { const brain = await freshBrain() const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [], neighborsAfterRebuild: ['n1'] }) let firstCall = true brain.storage.getVerbs = async () => { if (firstCall) { firstCall = false throw new Error('transient storage hiccup') } return { items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false } } // First call: probe throws transiently → swallowed (no GraphIndexNotReadyError), flag reset. await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(0) expect(brain._graphAdjacencyVerified).toBe(false) // re-check allowed // Second call: storage works → detects the empty adjacency + rebuilds. await brain.verifyGraphAdjacencyLive() expect(calls.rebuild).toBe(1) await brain.close() }) })