This repository has been archived on 2026-09-03. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
open-brainy/tests/integration/find-connected-order.test.ts

194 lines
7.5 KiB
TypeScript
Raw Normal View History

fix(find): connected finds are graph-first — neighbours, then the filter over those ids, then the page With `connected` present, find() materialized the whole-store filtered id list, paged it, hydrated the page, and only then intersected with the neighbour set. Every such call paid O(store) for the filter and the hydration of rows that were never neighbours, and a neighbour outside the first page of the filtered STORE was silently dropped — the answer depended on the store's order and the page size. The neighbour set is now the candidate universe: resolved first from the adjacency, the metadata filter evaluated over those ids only through the provider's own evaluation (a new optional `filterIdsWithin` door on MetadataIndexProvider; the reference index implements it from its own getIdsForFilter so the two can never disagree; a provider without it is served by the whole-store answer intersected here), `orderBy` sorts the whole neighbour set before the page is cut, and the vector leg walks the neighbours as its candidate set. The text leg of a hybrid find keeps its post-intersection — it has no candidate door. Pinned in tests/integration/find-connected-order.test.ts: paging reaches every matching neighbour and never a non-neighbour; a `missing` negation is evaluated over the neighbours; the index is asked about the neighbour ids only and hydration is one page; orderBy sorts the whole set; the vector leg stays inside the neighbours; an edgeless anchor answers [] before the filter is asked.
2026-09-01 11:29:44 -07:00
/**
* @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 () => {
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
// 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()
fix(find): connected finds are graph-first — neighbours, then the filter over those ids, then the page With `connected` present, find() materialized the whole-store filtered id list, paged it, hydrated the page, and only then intersected with the neighbour set. Every such call paid O(store) for the filter and the hydration of rows that were never neighbours, and a neighbour outside the first page of the filtered STORE was silently dropped — the answer depended on the store's order and the page size. The neighbour set is now the candidate universe: resolved first from the adjacency, the metadata filter evaluated over those ids only through the provider's own evaluation (a new optional `filterIdsWithin` door on MetadataIndexProvider; the reference index implements it from its own getIdsForFilter so the two can never disagree; a provider without it is served by the whole-store answer intersected here), `orderBy` sorts the whole neighbour set before the page is cut, and the vector leg walks the neighbours as its candidate set. The text leg of a hybrid find keeps its post-intersection — it has no candidate door. Pinned in tests/integration/find-connected-order.test.ts: paging reaches every matching neighbour and never a non-neighbour; a `missing` negation is evaluated over the neighbours; the index is asked about the neighbour ids only and hydration is one page; orderBy sorts the whole set; the vector leg stays inside the neighbours; an edgeless anchor answers [] before the filter is asked.
2026-09-01 11:29:44 -07:00
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 () => {
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
// 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
})
fix(find): connected finds are graph-first — neighbours, then the filter over those ids, then the page With `connected` present, find() materialized the whole-store filtered id list, paged it, hydrated the page, and only then intersected with the neighbour set. Every such call paid O(store) for the filter and the hydration of rows that were never neighbours, and a neighbour outside the first page of the filtered STORE was silently dropped — the answer depended on the store's order and the page size. The neighbour set is now the candidate universe: resolved first from the adjacency, the metadata filter evaluated over those ids only through the provider's own evaluation (a new optional `filterIdsWithin` door on MetadataIndexProvider; the reference index implements it from its own getIdsForFilter so the two can never disagree; a provider without it is served by the whole-store answer intersected here), `orderBy` sorts the whole neighbour set before the page is cut, and the vector leg walks the neighbours as its candidate set. The text leg of a hybrid find keeps its post-intersection — it has no candidate door. Pinned in tests/integration/find-connected-order.test.ts: paging reaches every matching neighbour and never a non-neighbour; a `missing` negation is evaluated over the neighbours; the index is asked about the neighbour ids only and hydration is one page; orderBy sorts the whole set; the vector leg stays inside the neighbours; an edgeless anchor answers [] before the filter is asked.
2026-09-01 11:29:44 -07:00
const results = await brain.find({
vector: sharedVector,
connected: { from: anchor, direction: 'out' },
where: { kind: 'note' },
limit: 5
})
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
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)
fix(find): connected finds are graph-first — neighbours, then the filter over those ids, then the page With `connected` present, find() materialized the whole-store filtered id list, paged it, hydrated the page, and only then intersected with the neighbour set. Every such call paid O(store) for the filter and the hydration of rows that were never neighbours, and a neighbour outside the first page of the filtered STORE was silently dropped — the answer depended on the store's order and the page size. The neighbour set is now the candidate universe: resolved first from the adjacency, the metadata filter evaluated over those ids only through the provider's own evaluation (a new optional `filterIdsWithin` door on MetadataIndexProvider; the reference index implements it from its own getIdsForFilter so the two can never disagree; a provider without it is served by the whole-store answer intersected here), `orderBy` sorts the whole neighbour set before the page is cut, and the vector leg walks the neighbours as its candidate set. The text leg of a hybrid find keeps its post-intersection — it has no candidate door. Pinned in tests/integration/find-connected-order.test.ts: paging reaches every matching neighbour and never a non-neighbour; a `missing` negation is evaluated over the neighbours; the index is asked about the neighbour ids only and hydration is one page; orderBy sorts the whole set; the vector leg stays inside the neighbours; an edgeless anchor answers [] before the filter is asked.
2026-09-01 11:29:44 -07:00
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()
}
})
})