/** * @module tests/unit/brainy/graph-export * @description Graph engine #22: `brain.graph.export()` — stream the whole graph * in one O(N+E) pass (the right primitive for visualizing all data, vs. paging * per node). Exercises the pure-TS fallback (no native provider in CI), which * streams all nouns then all verbs via cursor pagination. */ 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' import type { GraphView } from '../../../src/index.js' async function collect(stream: AsyncIterable): Promise<{ nodes: Map edges: Map chunks: number }> { const nodes = new Map() const edges = new Map() let chunks = 0 for await (const chunk of stream) { chunks++ for (const n of chunk.nodes) nodes.set(n.id, n) for (const e of chunk.edges) edges.set(e.id, e) } return { nodes, edges, chunks } } describe('brain.graph.export() (graph engine #22)', () => { let brain: Brainy let a: string, b: string, c: string, iso: 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.Project, subtype: 'milestone', data: 'C' }) iso = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'isolated' }) 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('streams the WHOLE graph — every node (incl. isolated) and every edge', async () => { const { nodes, edges } = await collect(brain.graph.export()) // All four nouns, including the isolated one (the node stream catches it). expect(new Set(nodes.keys())).toEqual(new Set([a, b, c, iso])) // Node refs carry type/subtype straight from the noun records. expect(nodes.get(c)?.type).toBe(NounType.Project) expect(nodes.get(c)?.subtype).toBe('milestone') // Both edges. const pairs = [...edges.values()].map((e) => `${e.from}->${e.to}`).sort() expect(pairs).toEqual([`${a}->${b}`, `${b}->${c}`].sort()) }) it('excludes internal-visibility nodes/edges by default; includeInternal surfaces them', async () => { const hidden = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'hidden', visibility: 'internal' }) await brain.relate({ from: a, to: hidden, type: VerbType.RelatedTo, subtype: 'colleague', visibility: 'internal' }) const visible = await collect(brain.graph.export()) expect(visible.nodes.has(hidden)).toBe(false) expect([...visible.edges.values()].some((e) => e.to === hidden)).toBe(false) const all = await collect(brain.graph.export({ includeInternal: true })) expect(all.nodes.has(hidden)).toBe(true) expect([...all.edges.values()].some((e) => e.to === hidden)).toBe(true) }) it('includeNodes:false streams only edges; includeEdges:false streams only nodes', async () => { const edgesOnly = await collect(brain.graph.export({ includeNodes: false })) expect(edgesOnly.nodes.size).toBe(0) expect(edgesOnly.edges.size).toBe(2) const nodesOnly = await collect(brain.graph.export({ includeEdges: false })) expect(nodesOnly.edges.size).toBe(0) expect(nodesOnly.nodes.size).toBe(4) }) it('chunks the stream by chunkSize (multiple passes, same total)', async () => { const { nodes, edges, chunks } = await collect(brain.graph.export({ chunkSize: 1 })) // 4 nodes + 2 edges at 1 per chunk → several chunks. expect(chunks).toBeGreaterThan(1) expect(nodes.size).toBe(4) expect(edges.size).toBe(2) }) })