open-brainy/tests/unit/brainy/graph-adjacency-cold-load.test.ts
David Snelling fd699d0e07 fix: never serve a silent [] from find({connected}) on a cold-loaded graph
On a cold process start of a large brain (above the eager index-rebuild
threshold), a native graph adjacency can report size()>0 — its membership set
reloaded — while the source->target edges did NOT load, so find({connected}),
neighbors() and related() returned [] for edges that are persisted on disk. A
database returning empty for data that exists, based purely on warm/cold state,
is a correctness bug; it hit a production deployment after every deploy.

Refactor the cold-load guard into verifyGraphAdjacencyLive(), which gates its
heal/throw decision on a GLOBAL known-edge sample (a persisted verb's source,
which by definition has an outgoing edge) rather than the queried anchor: a
stale adjacency is rebuilt from storage, an unrecoverable one throws
GraphIndexNotReadyError instead of serving [], and a genuinely edgeless anchor
still returns [] with no spurious rebuild. executeGraphSearch re-verifies before
trusting an empty connected result and re-collects after a heal. Adds a 5-case
integration test (self-heal / loud-throw / edgeless-no-false-positive / healthy
/ re-collect) plus updated unit coverage.
2026-06-29 16:40:02 -07:00

130 lines
5.3 KiB
TypeScript

/**
* @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<any> {
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()
})
})