/** * @module tests/integration/cold-graph-connected-8.0 * @description BRAINY-COLD-GRAPH-CONNECTED (8.0) — regression coverage for the silent-empty * graph-traversal bug, gated on the honest readiness signal: a sync `graphIndex.isReady()` * that is true ONLY when the source→target EDGES are loaded (NOT the membership/manifest count). * * On the FIRST `find({ connected })` after a cold process start, a native graph adjacency can * reload its relationship COUNT (so `size() > 0`) but NOT its edges — so `getNeighbors()` returns * `[]` for EVERY source and brainy would serve that `[]` as if the anchor were genuinely edgeless. * * RE-POINTED to the health-gate law: `verifyGraphAdjacencyLive` NEVER rebuilds and NEVER walks the * store from a read — a read-path rebuild is exactly the dark-rebuild failure mode the law retires * (open() alone owns building). The guard now: * - `isReady() === false` → THROWS {@link GraphIndexNotReadyError} immediately — no rebuild attempt; * - a genuinely edgeless anchor with `isReady() === true` verifies 'live' and the empty result * stands — no spurious throw; * - a provider WITHOUT `isReady()` falls back to the shipped known-edge-sample probe, which is * now READ-ONLY: it refuses loudly (throws) rather than self-healing via rebuild. * * These exercise REAL `find({ connected })` against an in-memory brain whose graph index is * instrumented with a test-double `isReady()` (and, for the fallback case, an always-empty * `getNeighbors`). Only the readiness/edge surface is wrapped; the underlying real adjacency * (built by `relate()`) is what a healthy provider actually serves. */ import { describe, it, expect, afterEach, vi } from 'vitest' import { Brainy } from '../../src/index.js' import { NounType, VerbType } from '../../src/types/graphTypes.js' import { GraphIndexNotReadyError } from '../../src/errors/brainyError.js' import { createTestConfig } from '../helpers/test-factory.js' /** * Build a real in-memory brain. Always seeds an UNRELATED edge (`E -> F`) so a GLOBAL known-edge * sample exists for the fallback probe even when the queried anchor is genuinely edgeless. When * `anchorEdges` is set, the anchor links out to three target nouns via `Knows`. Ids are * brainy-generated and returned for assertions. */ async function buildBrain( opts: { anchorEdges: boolean } ): Promise<{ brain: any; anchorId: string; targetIds: string[] }> { const brain: any = new Brainy(createTestConfig({ silent: true })) await brain.init() // Unrelated edge — guarantees storage.getVerbs() always yields a known-edge source. const eId = await brain.add({ data: 'node E', type: NounType.Person }) const fId = await brain.add({ data: 'node F', type: NounType.Person }) await brain.relate({ from: eId, to: fId, type: VerbType.Knows }) const anchorId = await brain.add({ data: 'anchor', type: NounType.Person }) const targetIds: string[] = [] if (opts.anchorEdges) { for (const label of ['B', 'C', 'D']) { const tId = await brain.add({ data: `node ${label}`, type: NounType.Person }) await brain.relate({ from: anchorId, to: tId, type: VerbType.Knows }) targetIds.push(tId) } } // Fresh cold-start state: nothing verified yet. brain._graphAdjacencyVerified = false return { brain, anchorId, targetIds } } /** * Instrument the brain's real graph index with a test-double `isReady()` (the honest-readiness * contract) plus an edge surface that goes empty while NOT ready. `getNeighbors` returns `[]` * while `!ready` (modelling the cold-unloaded adjacency) and delegates to the REAL index once * `ready` flips true (used only by the "healthy" control cases — the guard itself never flips * this anymore, since it never rebuilds). `rebuild` is counted so tests can assert it is NEVER * called by a read. */ function instrumentIsReady( brain: any, opts: { ready: boolean } ): { rebuildCalls: number; ready: boolean } { const gi = brain.graphIndex const origGetNeighbors = gi.getNeighbors.bind(gi) const state = { ready: opts.ready, rebuildCalls: 0 } gi.isReady = (): boolean => state.ready gi.getNeighbors = async (id: bigint, options?: any): Promise => state.ready ? origGetNeighbors(id, options) : [] gi.rebuild = async (): Promise => { state.rebuildCalls++ } return state } /** * Fallback instrumentation — a provider WITHOUT `isReady()` (older cortex / JS baseline). Wraps * `getNeighbors` to always return `[]` while `broken`. This is the shipped known-edge-sample * probe path — now READ-ONLY: it refuses loudly rather than self-healing. */ function instrumentNoIsReady( brain: any, opts: { broken: boolean } ): { rebuildCalls: number } { const gi = brain.graphIndex // Ensure the provider does NOT expose isReady() — the default JS provider doesn't. expect(typeof gi.isReady).toBe('undefined') const origGetNeighbors = gi.getNeighbors.bind(gi) const state = { broken: opts.broken, rebuildCalls: 0 } gi.getNeighbors = async (id: bigint, options?: any): Promise => state.broken ? [] : origGetNeighbors(id, options) gi.rebuild = async (): Promise => { state.rebuildCalls++ } return state } describe('BRAINY-COLD-GRAPH-CONNECTED 8.0 — isReady()-gated, never serves a silent [], never rebuilds from a read', () => { let brains: any[] = [] afterEach(async () => { for (const b of brains) { try { await b.close() } catch { /* best-effort cleanup */ } } brains = [] vi.restoreAllMocks() }) it('(a) isReady() false → THROWS GraphIndexNotReadyError immediately, no rebuild attempt', async () => { const { brain, anchorId } = await buildBrain({ anchorEdges: true }) brains.push(brain) const state = instrumentIsReady(brain, { ready: false }) await expect( brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) ).rejects.toBeInstanceOf(GraphIndexNotReadyError) expect(state.rebuildCalls).toBe(0) // a read never rebuilds — it refuses loudly instead }) it('(b) isReady() stays false → throws GraphIndexNotReadyError (NOT a silent [])', async () => { const { brain, anchorId } = await buildBrain({ anchorEdges: true }) brains.push(brain) const state = instrumentIsReady(brain, { ready: false }) await expect( brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) ).rejects.toBeInstanceOf(GraphIndexNotReadyError) expect(state.rebuildCalls).toBe(0) }) it('(c) edgeless anchor + isReady() true → returns [] with NO rebuild and NO throw', async () => { // The anchor has no edges, but E -> F does — the adjacency is genuinely loaded (ready). const { brain, anchorId } = await buildBrain({ anchorEdges: false }) brains.push(brain) const state = instrumentIsReady(brain, { ready: true }) const results = await brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) expect(results).toEqual([]) // genuinely edgeless — empty is the truth expect(state.rebuildCalls).toBe(0) // ready adjacency → no spurious rebuild }) it('(d) healthy isReady() true → correct results, NO rebuild', async () => { const { brain, anchorId, targetIds } = await buildBrain({ anchorEdges: true }) brains.push(brain) const state = instrumentIsReady(brain, { ready: true }) const results = await brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) expect(state.rebuildCalls).toBe(0) const ids = results.map((r: any) => r.id).sort() expect(ids).toEqual(targetIds.sort()) }) it('(e) provider WITHOUT isReady() → the known-edge-sample probe REFUSES LOUDLY (never self-heals)', async () => { const { brain, anchorId } = await buildBrain({ anchorEdges: true }) brains.push(brain) const state = instrumentNoIsReady(brain, { broken: true }) await expect( brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) ).rejects.toBeInstanceOf(GraphIndexNotReadyError) expect(state.rebuildCalls).toBe(0) // the fallback probe is READ-ONLY — it never calls rebuild() }) it('(f) an empty connectedIds set re-verifies against a not-serving adjacency and throws, rather than serving [] as truth', async () => { // executeGraphSearch's cold-load guard (connectedIds.size === 0 → re-verify) used to // interpret a healed rebuild as "re-collect and serve." That rebuild-and-heal path is // retired: the re-verify now either confirms a genuinely edgeless anchor ('live', case (c)) // or — as here — discovers the adjacency itself is not serving, and throws. const { brain, anchorId } = await buildBrain({ anchorEdges: true }) brains.push(brain) const state = instrumentIsReady(brain, { ready: false }) await expect( brain.find({ connected: { from: anchorId, direction: 'out' }, limit: 10 }) ).rejects.toBeInstanceOf(GraphIndexNotReadyError) expect(state.rebuildCalls).toBe(0) }) })