brainy/tests/unit/brainy/graph-native-routing.test.ts
David Snelling f3e69110f0 refactor(8.0): graph analytics contract — intent names, not algorithm names
The public brain.graph.* surface and the GraphAccelerationProvider seam must name
operations by INTENT, not by the algorithm that happens to implement them — so a
plugin can swap algorithms without the contract lying. The JS fallback computes
communities via connected-components and rank via PageRank, but a native provider
(cor) is free to use Louvain/Leiden for communities and personalized-PageRank /
eigenvector-centrality for rank: same intent, different algorithm, same name.

Renamed the (not-yet-implemented) Phase-C analytics methods + their option/result
types on GraphAccelerationProvider:
- pageRank -> rank            (PageRankOptions -> RankOptions; dropped the
                               pagerank-internal damping/maxIterations/tolerance
                               knobs from the contract — each impl tunes itself)
- connectedComponents -> communities   (ComponentsOptions -> CommunitiesOptions,
                               mode:'weak'|'strong' -> directed?:boolean;
                               GraphComponents -> GraphCommunities, componentIds/
                               Count -> communityIds/Count)
- shortestPath -> path        (ShortestPathOptions -> PathOptions; weight -> by:'hops'|'weight')
- neighborhoodSample -> sample (NeighborhoodSampleOptions -> SampleOptions)
- topByDegree -> mostConnected (TopByDegreeOptions -> MostConnectedOptions)

traverse / edgesForNode / graphCursorOpen/Next/Close + GraphScores/GraphPath/
OpaqueIdSet/Subgraph are UNCHANGED — everything cor has already built is untouched;
this is a pure pre-implementation rename (cor coordinated). JSDoc reworded to state
the algorithm is the provider's choice. index.ts exports + the native-seam test
mock updated. tsc/unit 1517/integration 599 green.
2026-06-22 09:54:01 -07:00

181 lines
7.8 KiB
TypeScript

/**
* @module tests/unit/brainy/graph-native-routing
* @description Graph engine — NATIVE seam coverage. brain.graph.subgraph/export
* route to a registered GraphAccelerationProvider and hydrate its columnar
* `Subgraph` (node ints -> ids, depth alignment, edge verb-ints -> Relations).
* In production that provider is cor's native engine, cross-layer-tested on
* bxl9000; brainy CI never registers one, so graphSubgraphNative /
* graphExportNative / hydrateNativeSubgraph + the provider-resolution + routing
* were previously UNEXERCISED — a return-shape or hydration-alignment drift would
* pass CI silently. This registers a faithful MOCK provider (returning a columnar
* Subgraph built from the brain's REAL ints, so hydration resolves to real
* entities/relations) to lock those paths.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/index.js'
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() {
const calls = { traverse: 0, cursorOpen: 0, cursorNext: 0, cursorClose: 0 }
const state: { calls: typeof calls; traverseResult: any; cursorChunks: any[] } = {
calls,
traverseResult: null,
cursorChunks: []
}
const empty = { nodeInts: new BigInt64Array(0), scores: new Float64Array(0) }
const provider = {
isInitialized: true,
traverse: async () => {
calls.traverse++
return state.traverseResult
},
edgesForNode: async () => state.traverseResult,
graphCursorOpen: async () => {
calls.cursorOpen++
return 'mock-handle'
},
graphCursorNext: async () => {
calls.cursorNext++
const subgraph = state.cursorChunks.shift()
return { subgraph, done: state.cursorChunks.length === 0 }
},
graphCursorClose: async () => {
calls.cursorClose++
},
rank: async () => empty,
communities: async () => ({ nodeInts: new BigInt64Array(0), communityIds: new Uint32Array(0), communityCount: 0 }),
path: async () => null,
sample: async () => state.traverseResult,
mostConnected: async () => empty
}
return { provider, state }
}
describe('brain.graph.* native routing + columnar hydration (native seam)', () => {
let brain: Brainy
let mock: ReturnType<typeof makeMockAccel>
let a: string, b: string, c: string
// Build a faithful columnar Subgraph from the brain's REAL ints, so brainy's
// hydration resolves the node ints to ids and the verb ints to Relations.
async function realSubgraph(withUnresolvable = false) {
const gei = (id: string): bigint => (brain as any).graphEntityInt(id)
const gi = (brain as any).graphIndex
const intA = gei(a), intB = gei(b), intC = gei(c)
const vAB = (await gi.getVerbIdsBySource(intA))[0] as bigint // verb int a->b
const vBC = (await gi.getVerbIdsBySource(intB))[0] as bigint // verb int b->c
const nodeInts = [intA, intB, intC]
const depths = [0, 1, 2]
if (withUnresolvable) {
nodeInts.push(999_999_999n) // a never-assigned int (simulates a deleted/unknown node)
depths.push(3)
}
return {
nodes: BigInt64Array.from(nodeInts),
nodeDepth: Uint8Array.from(depths),
edgeSources: BigInt64Array.from([intA, intB]),
edgeTargets: BigInt64Array.from([intB, intC]),
edgeVerbInts: BigInt64Array.from([vAB, vBC]),
edgeTypes: Uint16Array.from([0, 0]),
truncated: false
}
}
beforeEach(async () => {
mock = makeMockAccel()
brain = new Brainy(createTestConfig())
brain.use({
name: 'mock-graph-accel',
activate: async (ctx: any) => {
ctx.registerProvider('graphAcceleration', mock.provider)
return true
}
} as any)
await brain.init()
a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
c = await brain.add({ type: NounType.Project, subtype: 'milestone', data: 'C' })
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' })
await brain.relate({ from: b, to: c, type: VerbType.ParticipatesIn, subtype: 'assignment' })
})
afterEach(async () => {
await brain.close()
})
it('subgraph() routes to the native provider and hydrates the columnar result', async () => {
mock.state.traverseResult = await realSubgraph()
const view = await brain.graph.subgraph(a, { depth: 2 })
expect(mock.state.calls.traverse).toBe(1) // native path, not the TS fallback
const byId = new Map(view.nodes.map((n) => [n.id, n]))
expect(new Set(byId.keys())).toEqual(new Set([a, b, c])) // node int -> id
expect(byId.get(a)?.depth).toBe(0)
expect(byId.get(c)?.depth).toBe(2) // depth column aligned to node column
expect(byId.get(c)?.type).toBe(NounType.Project) // node type hydrated via batchGet
const pairs = view.edges.map((e) => `${e.from}->${e.to}`).sort()
expect(pairs).toEqual([`${a}->${b}`, `${b}->${c}`].sort()) // verb int -> Relation
})
it('keeps node<->depth alignment when a node int does not resolve (deleted/unknown)', async () => {
mock.state.traverseResult = await realSubgraph(true) // appends an unresolvable int at depth 3
const view = await brain.graph.subgraph(a, { depth: 3 })
const byId = new Map(view.nodes.map((n) => [n.id, n]))
// The 3 real nodes keep their CORRECT depths — the unresolvable int is dropped,
// not collapsed into the array (which would shift every later depth).
expect(byId.get(a)?.depth).toBe(0)
expect(byId.get(b)?.depth).toBe(1)
expect(byId.get(c)?.depth).toBe(2)
expect(view.nodes.length).toBe(3)
})
it('export() routes to the native graph cursor, hydrates chunks, and always closes', async () => {
mock.state.cursorChunks = [await realSubgraph()]
const chunks: any[] = []
for await (const v of brain.graph.export()) chunks.push(v)
expect(mock.state.calls.cursorOpen).toBe(1)
expect(mock.state.calls.cursorClose).toBe(1) // cursor released even on normal completion
const nodes = new Set(chunks.flatMap((c) => c.nodes.map((n: any) => n.id)))
expect(nodes).toEqual(new Set([a, b, c]))
const edges = chunks.flatMap((c) => c.edges.map((e: any) => `${e.from}->${e.to}`)).sort()
expect(edges).toEqual([`${a}->${b}`, `${b}->${c}`].sort())
})
it('resolves a provider registered as a FACTORY (storage) => provider, not just an instance', async () => {
const m = makeMockAccel()
const fb = new Brainy(createTestConfig())
fb.use({
name: 'mock-graph-accel-factory',
activate: async (ctx: any) => {
ctx.registerProvider('graphAcceleration', () => m.provider) // factory form
return true
}
} as any)
await fb.init()
const x = await fb.add({ type: NounType.Person, subtype: 'employee', data: 'X' })
const y = await fb.add({ type: NounType.Person, subtype: 'employee', data: 'Y' })
await fb.relate({ from: x, to: y, type: VerbType.RelatedTo, subtype: 'colleague' })
const gei = (id: string): bigint => (fb as any).graphEntityInt(id)
const gi = (fb as any).graphIndex
const ix = gei(x), iy = gei(y)
const vxy = (await gi.getVerbIdsBySource(ix))[0] as bigint
m.state.traverseResult = {
nodes: BigInt64Array.from([ix, iy]),
nodeDepth: Uint8Array.from([0, 1]),
edgeSources: BigInt64Array.from([ix]),
edgeTargets: BigInt64Array.from([iy]),
edgeVerbInts: BigInt64Array.from([vxy]),
edgeTypes: Uint16Array.from([0]),
truncated: false
}
const view = await fb.graph.subgraph(x, { depth: 1 })
expect(m.state.calls.traverse).toBe(1) // factory was invoked + provider routed
expect(new Set(view.nodes.map((n) => n.id))).toEqual(new Set([x, y]))
await fb.close()
})
})