/** * @module tests/unit/brainy/related-node-bidirectional * @description Graph-perf #4 (8.0): `related({ node })` — the one-call, * both-direction "all edges on a noun" query. * * `related({ from })` / `related({ to })` each return one direction; `related({ node })` * unions both (edges where the node is source OR target), deduped, in a single * O(degree) call — the indexed counterpart of merging two `related()` calls by hand. * It composes with `type` / `subtype` / visibility filters and is mutually exclusive * with `from` / `to`. */ 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('related({ node }) both-direction incident edges (graph-perf #4)', () => { let brain: Brainy let a: string, b: string, c: string, d: 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.Person, subtype: 'employee', data: 'D' }) // a's incident edges: a→b (out), c→a (in), a→d (out, ReportsTo), c→a internal (in, hidden) await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' }) await brain.relate({ from: c, to: a, type: VerbType.RelatedTo, subtype: 'colleague' }) await brain.relate({ from: a, to: d, type: VerbType.ReportsTo, subtype: 'direct' }) }) afterEach(async () => { await brain.close() }) it('returns edges incident in BOTH directions (source OR target)', async () => { const edges = await brain.related({ node: a }) expect(edges).toHaveLength(3) // Both out-edges (a is `from`) and the in-edge (a is `to`) are present. const pairs = edges.map((e) => `${e.from}->${e.to}`).sort() expect(pairs).toEqual([`${a}->${b}`, `${a}->${d}`, `${c}->${a}`].sort()) }) it('matches the union of related({from}) + related({to})', async () => { const out = await brain.related({ from: a }) const inc = await brain.related({ to: a }) const expected = new Set([...out, ...inc].map((e) => e.id)) const node = await brain.related({ node: a }) expect(new Set(node.map((e) => e.id))).toEqual(expected) }) it('dedupes a self-loop (an edge where the node is both source and target)', async () => { await brain.relate({ from: a, to: a, type: VerbType.RelatedTo, subtype: 'self' }) const edges = await brain.related({ node: a }) const selfLoops = edges.filter((e) => e.from === a && e.to === a) expect(selfLoops).toHaveLength(1) // appears once, not twice }) it('composes with a type filter (both directions)', async () => { const related = await brain.related({ node: a, type: VerbType.RelatedTo }) // a→b and c→a are RelatedTo; a→d is ReportsTo (excluded). expect(related.map((e) => `${e.from}->${e.to}`).sort()).toEqual( [`${a}->${b}`, `${c}->${a}`].sort() ) }) it('excludes internal edges by default, includes them with includeInternal', async () => { const e = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'E' }) await brain.relate({ from: e, to: a, type: VerbType.RelatedTo, subtype: 'colleague', visibility: 'internal' }) const visible = await brain.related({ node: a }) expect(visible.some((r) => r.from === e)).toBe(false) const all = await brain.related({ node: a, includeInternal: true }) const internalEdge = all.find((r) => r.from === e) expect(internalEdge?.visibility).toBe('internal') }) it("throws when 'node' is combined with 'from' or 'to'", async () => { await expect(brain.related({ node: a, from: b })).rejects.toThrow(/mutually exclusive/) await expect(brain.related({ node: a, to: b })).rejects.toThrow(/mutually exclusive/) }) })