open-brainy/tests/integration/find-connected-order.test.ts
David Snelling e766ed0a84
Some checks are pending
CI / Node 22 (push) Waiting to run
CI / Node 24 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run
test(find-connected): close the brain this file leaks, and name the half a short answer came from
TWO THINGS, both about the same file.

THE LEAK, which is a defect of the test. `afterAll` set `brain = null`. That
does not close a brain — it only makes it unreachable from here. The instance
stayed open and registered with its unref'd cadence timer running, and the gate
config runs the whole suite in ONE process (pool: 'forks', singleFork: true —
two files report the same process.pid), so a brain leaked in this file goes on
narrating its flushes into every file that runs after it. This one holds 151
entities and 30 relations. It is closed now.

It is not the only leaker in the suite — a create-versus-close scan turns up 67
files with the same shape, and this is one of them, not the cause of anything on
its own. Fixing the file I was already in.

THE DIAGNOSTIC. 'walks the vector leg over the neighbours only' went red on the
gate box (1 row of a requested 5) while passing here in isolation eight runs out
of eight, beside its own box predecessor, and under a perturbed random stream —
and it passed on the box one gate earlier behind the IDENTICAL predecessor. So
the cause is process state accumulated by the time this file runs, and a bare
count mismatch says nothing about which half broke.

The case now runs the same query without the vector leg first, as a control,
and reports both counts: both short means the neighbour set or the filter, only
the vector leg short means the walk — which matters here because every row in
this corpus carries an IDENTICAL vector, so the walk is ranking an exact tie and
a tie has no defined order to return 5 of.

The assertion is unchanged: still exactly 5, still every row a neighbour.
2026-09-02 16:14:24 -07:00

193 lines
7.5 KiB
TypeScript

/**
* @module tests/integration/find-connected-order
* @description The graph-first law for `find({ connected })` (10.4.8).
*
* With `connected` present the neighbour set is the candidate universe: it is
* resolved from the adjacency first, the metadata filter is evaluated over
* those ids only, and the page is cut last. The earlier order materialized the
* whole-store filtered id list, paged it, hydrated the page, and only then
* intersected with the neighbours — so a neighbour outside the first page of
* the filtered STORE was silently dropped, and every call paid O(store).
*
* These pins hold both halves. The answer: every matching neighbour is
* reachable by paging, a non-neighbour never appears, a negation (`missing`)
* is evaluated over the neighbours, `orderBy` sorts the whole neighbour set
* before the page is cut, and the vector leg walks the neighbours only. The
* cost shape: the metadata index is asked about the neighbour ids only, and
* hydration is one page — never the store.
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
import { Brainy } from '../../src/brainy'
import { NounType, VerbType } from '../../src/types/graphTypes'
import { v5 } from '../../src/universal/uuid'
import { generateTestVector } from '../helpers/test-factory'
/** Matching rows that are NOT neighbours — added FIRST, so the whole-store filtered list leads with them. */
const NOISE = 120
/** Matching rows that ARE neighbours of the anchor. */
const NEIGHBOURS = 30
/** Neighbours carrying `retracted: true` — excluded by the `missing` negation. */
const RETRACTED = 4
describe('find({ connected }) is graph-first: neighbours → filter → page', () => {
let brain: Brainy<any>
const anchor = 'anchor'
const sharedVector = generateTestVector()
const neighbourIds = new Set(Array.from({ length: NEIGHBOURS }, (_, i) => v5(`nb-${i}`)))
beforeAll(async () => {
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
await brain.init()
await brain.add({
id: anchor,
data: 'the anchor',
type: NounType.Person,
metadata: { kind: 'anchor' },
vector: generateTestVector()
})
for (let i = 0; i < NOISE; i++) {
await brain.add({
id: `noise-${i}`,
data: `noise ${i}`,
type: NounType.Person,
metadata: { kind: 'note', rank: 1000 + i },
vector: sharedVector
})
}
for (let i = 0; i < NEIGHBOURS; i++) {
await brain.add({
id: `nb-${i}`,
data: `neighbour ${i}`,
type: NounType.Person,
metadata: { kind: 'note', rank: i + 1, ...(i < RETRACTED ? { retracted: true } : {}) },
vector: sharedVector
})
await brain.relate({ from: anchor, to: `nb-${i}`, type: VerbType.Knows })
}
})
afterAll(async () => {
// CLOSE IT. Dropping the reference does not close a brain — it only makes
// it unreachable from here. The instance stays open and registered, its
// unref'd cadence timer keeps running, and because the gate config runs the
// whole suite in ONE process (pool: 'forks', singleFork: true) it goes on
// narrating its flushes into every test file that runs after this one.
// A test that leaks a brain is a defect of the test.
await brain?.close()
brain = null as any
})
it('returns the matching neighbours page by page — none dropped, never a non-neighbour', async () => {
const seen = new Set<string>()
for (let offset = 0; offset <= NEIGHBOURS; offset += 10) {
const page = await brain.find({
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
limit: 10,
offset
})
expect(page).toHaveLength(offset < NEIGHBOURS ? 10 : 0)
for (const r of page) {
expect(neighbourIds.has(r.entity.id)).toBe(true)
expect(seen.has(r.entity.id)).toBe(false)
seen.add(r.entity.id)
}
}
expect(seen.size).toBe(NEIGHBOURS)
})
it('evaluates a negation (`missing`) over the neighbour set, not the store', async () => {
const results = await brain.find({
connected: { from: anchor, direction: 'out' },
where: { kind: 'note', retracted: { missing: true } },
limit: 100
})
expect(results).toHaveLength(NEIGHBOURS - RETRACTED)
for (const r of results) {
expect(neighbourIds.has(r.entity.id)).toBe(true)
expect(r.entity.metadata.retracted).toBeUndefined()
}
})
it('asks the metadata index about the neighbour ids only, and hydrates one page', async () => {
const index = (brain as any).metadataIndex
const within = vi.spyOn(index, 'filterIdsWithin')
const hydrate = vi.spyOn(brain as any, 'batchGet')
try {
const results = await brain.find({
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
limit: 10
})
expect(results).toHaveLength(10)
expect(within).toHaveBeenCalledTimes(1)
const askedIds = within.mock.calls[0][1] as string[]
expect(askedIds).toHaveLength(NEIGHBOURS)
for (const id of askedIds) expect(neighbourIds.has(id)).toBe(true)
expect(hydrate).toHaveBeenCalledTimes(1)
expect(hydrate.mock.calls[0][0]).toHaveLength(10)
} finally {
within.mockRestore()
hydrate.mockRestore()
}
})
it('orders the WHOLE neighbour set before cutting the page', async () => {
const results = await brain.find({
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
orderBy: 'rank',
order: 'desc',
limit: 5
})
expect(results.map((r) => r.entity.metadata.rank)).toEqual([30, 29, 28, 27, 26])
})
it('walks the vector leg over the neighbours only', async () => {
// The SAME query without the vector leg, first. Both legs draw from the
// one neighbour set, so this is the control: it says whether a short answer
// came from the adjacency/filter (both legs short) or from the vector walk
// alone (only the vector leg short). Cheap, and it turns a bare count
// mismatch into a named half — this case has gone red on the gate box
// while passing in isolation and beside its own predecessor, so the next
// red must arrive already carrying the half it belongs to.
const control = await brain.find({
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
limit: 5
})
const results = await brain.find({
vector: sharedVector,
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
limit: 5
})
expect(
results.length,
`the vector leg returned ${results.length} of a requested 5. The same query ` +
`WITHOUT the vector returned ${control.length}: if that is also short the ` +
`neighbour set or the filter is the cause, and if it is 5 the vector walk is — ` +
`note every row in this corpus carries an identical vector, so the walk is ` +
`ranking an exact tie.`
).toBe(5)
for (const r of results) expect(neighbourIds.has(r.entity.id)).toBe(true)
})
it('an anchor without neighbours answers [] before the filter is asked', async () => {
const index = (brain as any).metadataIndex
const within = vi.spyOn(index, 'filterIdsWithin')
try {
const results = await brain.find({
connected: { from: 'noise-0', direction: 'out' },
where: { kind: 'note' },
limit: 10
})
expect(results).toEqual([])
expect(within).not.toHaveBeenCalled()
} finally {
within.mockRestore()
}
})
})