fix(8.0): gate native graph analytics on the provider readiness flag

The optional native graph-acceleration provider exposes `isInitialized`,
which is false during its cold-start / rebuild window (engine + cursor not
yet loaded). The accessor cached the resolved provider INSTANCE but never
re-checked readiness, so `brain.graph.*` could dispatch to a not-ready
provider and throw instead of answering.

Resolve and cache the instance once, but check `isInitialized` LIVE on every
call: route to the pure-TS analytics path while the provider is not ready,
and re-engage the native path automatically the moment it flips true. This
gates every native route at once — subgraph, export, rank, communities,
path, and the query→expand fusion. Adds a test asserting the provider is
never called (and the TS fallback returns real answers) while not ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
David Snelling 2026-06-24 15:18:50 -07:00
parent bf0afe8563
commit d321cf5f33
2 changed files with 65 additions and 19 deletions

View file

@ -18,7 +18,7 @@ import { NounType, VerbType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'
/** A stateful mock GraphAccelerationProvider; the test sets the columnar payloads. */
function makeMockAccel() {
function makeMockAccel(opts: { isInitialized?: boolean } = {}) {
const calls = { traverse: 0, cursorOpen: 0, cursorNext: 0, cursorClose: 0, rank: 0, communities: 0, path: 0 }
const state: {
calls: typeof calls
@ -44,7 +44,7 @@ function makeMockAccel() {
communityCount: 0
}
const provider = {
isInitialized: true,
isInitialized: opts.isInitialized ?? true,
traverse: async (seeds: any) => {
calls.traverse++
state.lastTraverseSeeds = seeds
@ -290,4 +290,45 @@ describe('brain.graph.* native routing + columnar hydration (native seam)', () =
expect(Array.isArray(mock.state.lastTraverseSeeds)).toBe(true) // bigint[], not a Buffer
expect(typeof (mock.state.lastTraverseSeeds as unknown[])[0]).toBe('bigint')
})
// Readiness gate (cor boundary-audit item #1): the native engine reports
// isInitialized=false during its cold-start/rebuild window. Brainy must route to the
// pure-TS path then — NOT call the not-ready provider (which would throw) — across the
// whole brain.graph.* surface, and re-engage the native path once it flips true.
it('routes to the TS fallback (never calls the provider) while accel.isInitialized is false', async () => {
const notReady = makeMockAccel({ isInitialized: false })
const b = new Brainy(createTestConfig())
b.use({
name: 'mock-graph-accel-not-ready',
activate: async (ctx: any) => {
ctx.registerProvider('graphAcceleration', notReady.provider)
return true
}
} as any)
await b.init()
const p = await b.add({ type: NounType.Person, subtype: 'employee', data: 'P' })
const q = await b.add({ type: NounType.Person, subtype: 'employee', data: 'Q' })
await b.relate({ from: p, to: q, type: VerbType.RelatedTo })
// Every brain.graph.* route + the query→expand fusion must NOT throw and must serve
// from the TS fallback while the engine is cold.
const sub = await b.graph.subgraph(p, { depth: 1 })
const ranked = await b.graph.rank()
const comm = await b.graph.communities()
const route = await b.graph.path(p, q)
const fused = await b.graph.subgraph({ type: NounType.Person }, { depth: 1 })
// The provider was never touched — all served by the JS fallback.
expect(notReady.state.calls.traverse).toBe(0)
expect(notReady.state.calls.rank).toBe(0)
expect(notReady.state.calls.communities).toBe(0)
expect(notReady.state.calls.path).toBe(0)
// And the TS fallback returned real answers (not throws / empties from a cold engine).
expect(sub.nodes.some((n) => n.id === p)).toBe(true)
expect(ranked.length).toBeGreaterThan(0)
expect(comm.count).toBeGreaterThan(0)
expect(route?.nodes).toEqual([p, q])
expect(fused.nodes.some((n) => n.id === p)).toBe(true)
await b.close()
})
})