87 lines
4 KiB
TypeScript
87 lines
4 KiB
TypeScript
|
|
/**
|
||
|
|
* @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/)
|
||
|
|
})
|
||
|
|
})
|