/** * @module tests/unit/brainy/graph-subgraph * @description Graph engine #25: `brain.graph.subgraph()` — bounded multi-hop * neighborhood extraction. These exercise the pure-TS fallback (no native * GraphAccelerationProvider is registered in CI); the native path returns the * same `GraphView` shape, cross-layer-tested against the provider. * * Graph under test (RelatedTo unless noted): * e → a → b → c , a →(ReportsTo) d , a →(internal) f */ 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' describe('brain.graph.subgraph() (graph engine #25)', () => { let brain: Brainy let a: string, b: string, c: string, d: string, e: string, f: string beforeEach(async () => { brain = new Brainy(createTestConfig()) 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.Person, subtype: 'employee', data: 'C' }) d = await brain.add({ type: NounType.Project, subtype: 'milestone', data: 'D' }) e = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'E' }) f = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'F' }) await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' }) await brain.relate({ from: b, to: c, type: VerbType.RelatedTo, subtype: 'colleague' }) await brain.relate({ from: a, to: d, type: VerbType.ReportsTo, subtype: 'direct' }) await brain.relate({ from: e, to: a, type: VerbType.RelatedTo, subtype: 'colleague' }) await brain.relate({ from: a, to: f, type: VerbType.RelatedTo, subtype: 'colleague', visibility: 'internal' }) }) afterEach(async () => { await brain.close() }) it('depth-1 returns the seed + immediate neighbors (both directions) with depths', async () => { const view = await brain.graph.subgraph(a, { depth: 1 }) const byId = new Map(view.nodes.map((n) => [n.id, n])) // a is the seed (depth 0); b, d (out) and e (in) are 1 hop. f is internal → hidden. expect(new Set(byId.keys())).toEqual(new Set([a, b, d, e])) expect(byId.get(a)?.depth).toBe(0) expect(byId.get(b)?.depth).toBe(1) expect(byId.get(e)?.depth).toBe(1) // Edges among them. const pairs = view.edges.map((r) => `${r.from}->${r.to}`).sort() expect(pairs).toEqual([`${a}->${b}`, `${a}->${d}`, `${e}->${a}`].sort()) }) it('depth-2 expands one more hop (c via b), tracking depth', async () => { const view = await brain.graph.subgraph(a, { depth: 2 }) const byId = new Map(view.nodes.map((n) => [n.id, n])) expect(byId.has(c)).toBe(true) expect(byId.get(c)?.depth).toBe(2) expect(view.edges.some((r) => r.from === b && r.to === c)).toBe(true) }) it("direction:'out' follows only outgoing edges", async () => { const view = await brain.graph.subgraph(a, { depth: 1, direction: 'out' }) const ids = new Set(view.nodes.map((n) => n.id)) expect(ids).toEqual(new Set([a, b, d])) // e (e→a, incoming) excluded }) it('composes with a verb-type filter', async () => { const view = await brain.graph.subgraph(a, { depth: 1, type: VerbType.ReportsTo }) expect(view.edges.map((r) => `${r.from}->${r.to}`)).toEqual([`${a}->${d}`]) expect(new Set(view.nodes.map((n) => n.id))).toEqual(new Set([a, d])) }) it('includeInternal surfaces the hidden edge/node', async () => { const view = await brain.graph.subgraph(a, { depth: 1, includeInternal: true }) expect(view.nodes.some((n) => n.id === f)).toBe(true) expect(view.edges.some((r) => r.to === f)).toBe(true) }) it('hydrates node type/subtype by default; skips it with hydrateNodes:false', async () => { const hydrated = await brain.graph.subgraph(a, { depth: 1 }) const dNode = hydrated.nodes.find((n) => n.id === d) expect(dNode?.type).toBe(NounType.Project) expect(dNode?.subtype).toBe('milestone') const bare = await brain.graph.subgraph(a, { depth: 1, hydrateNodes: false }) expect(bare.nodes.every((n) => n.type === undefined)).toBe(true) }) it('caps the result and flags truncated when maxNodes is hit', async () => { const view = await brain.graph.subgraph(a, { depth: 2, maxNodes: 2 }) expect(view.truncated).toBe(true) expect(view.nodes.length).toBeLessThanOrEqual(2) }) it('accepts multiple seeds', async () => { const view = await brain.graph.subgraph([a, c], { depth: 0 }) // depth 0 = just the seeds, no expansion. expect(new Set(view.nodes.map((n) => n.id))).toEqual(new Set([a, c])) }) })