82 lines
4 KiB
TypeScript
82 lines
4 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/unit/brainy/related-visibility-fast-path
|
||
|
|
* @description Graph-perf #1 (8.0): the visibility-aware fast adjacency path.
|
||
|
|
*
|
||
|
|
* `related({ from })` / `related({ to })` route through the O(degree)
|
||
|
|
* GraphAdjacencyIndex fast paths in `storage.getVerbs`. Before this fix, the
|
||
|
|
* default visibility exclusion (`excludeVisibility: ['internal','system']`,
|
||
|
|
* which every default `related()` sets) DISQUALIFIED those fast paths and forced
|
||
|
|
* a full O(E) shard scan — the root cause of multi-second per-node edge lookups
|
||
|
|
* on large graphs. The fast paths now hydrate each candidate's metadata and
|
||
|
|
* apply the subtype / visibility filters on the small O(degree) candidate set.
|
||
|
|
*
|
||
|
|
* Two observable contracts are pinned here (the perf change itself is structural
|
||
|
|
* — these prove the fast path produces the SAME correct results the scan did):
|
||
|
|
* 1. default `related()` excludes `internal` / `system` edges; `includeInternal`
|
||
|
|
* / `includeSystem` opt them back in.
|
||
|
|
* 2. returned `Relation`s now carry `visibility` (previously dropped by
|
||
|
|
* `hydrateVerbWithMetadata`, so `related({ includeInternal })` could not even
|
||
|
|
* report which edges were internal).
|
||
|
|
*/
|
||
|
|
|
||
|
|
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() visibility-aware fast adjacency (graph-perf #1)', () => {
|
||
|
|
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 → B public, A → C internal, A → D public.
|
||
|
|
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' })
|
||
|
|
await brain.relate({ from: a, to: c, type: VerbType.RelatedTo, subtype: 'colleague', visibility: 'internal' })
|
||
|
|
await brain.relate({ from: a, to: d, type: VerbType.RelatedTo, subtype: 'colleague' })
|
||
|
|
})
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await brain.close()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('default related({ from }) excludes internal edges (out-direction fast path)', async () => {
|
||
|
|
const edges = await brain.related({ from: a })
|
||
|
|
const targets = edges.map((e) => e.to).sort()
|
||
|
|
expect(targets).toEqual([b, d].sort())
|
||
|
|
// None of the default results carry a non-public visibility.
|
||
|
|
expect(edges.every((e) => e.visibility === undefined || e.visibility === 'public')).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('related({ from, includeInternal }) surfaces the internal edge AND reports its visibility', async () => {
|
||
|
|
const edges = await brain.related({ from: a, includeInternal: true })
|
||
|
|
expect(edges.map((e) => e.to).sort()).toEqual([b, c, d].sort())
|
||
|
|
const internalEdge = edges.find((e) => e.to === c)
|
||
|
|
// The latent-bug fix: visibility now flows through hydration to the Relation.
|
||
|
|
expect(internalEdge?.visibility).toBe('internal')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('default related({ to }) excludes an internal edge (in-direction fast path)', async () => {
|
||
|
|
// B has one public in-edge; C has one internal in-edge.
|
||
|
|
const intoB = await brain.related({ to: b })
|
||
|
|
expect(intoB.map((e) => e.from)).toEqual([a])
|
||
|
|
|
||
|
|
const intoC = await brain.related({ to: c })
|
||
|
|
expect(intoC).toHaveLength(0) // internal edge hidden by default
|
||
|
|
|
||
|
|
const intoCAll = await brain.related({ to: c, includeInternal: true })
|
||
|
|
expect(intoCAll.map((e) => e.from)).toEqual([a])
|
||
|
|
expect(intoCAll[0]?.visibility).toBe('internal')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('subtype filter still works on the fast path (returns only matching, excludes internal)', async () => {
|
||
|
|
const colleagues = await brain.related({ from: a, subtype: 'colleague' })
|
||
|
|
expect(colleagues.map((e) => e.to).sort()).toEqual([b, d].sort())
|
||
|
|
})
|
||
|
|
})
|