tests/integration/find-*.test.ts and tests/unit/brainy/find*.test.ts each opened one or more Brainy instances (via beforeAll/beforeEach) and never closed them — the leaked instance's cadence timer stays armed for the rest of the single-forked vitest run and keeps narrating into every later file. find-unified-integration.test.ts was a real bug, not just a missing hook: its afterAll called a no-op TestCleanup().cleanup() (nothing was ever registered with it) and then discarded the brain reference with `brain = null` — the brain was never actually closed.
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
/**
|
|
* @module find-index-integrity-guard.test
|
|
* @description Regression for the "phantom row" class (Venue 2026-06-24): a
|
|
* corrupt/stale metadata-index posting must never surface an entity that does
|
|
* not actually match the find() predicate.
|
|
*
|
|
* The metadata index is an acceleration structure; the loaded entity is ground
|
|
* truth. find() re-validates every result against `type`/`subtype`/`where`/
|
|
* `service` at a single egress chokepoint (the live-path mirror of the
|
|
* historical path's `entityMatchesFind`). These tests inject a poisoned
|
|
* `getIdsForFilter` (the exact failure mode seen in production: a native index
|
|
* returns an id whose record matches NEITHER the type nor the where filter) and
|
|
* assert the phantom is dropped while the genuine matches survive.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { Brainy } from '../../../src/brainy'
|
|
import { NounType } from '../../../src/types/graphTypes'
|
|
|
|
describe('find() index-integrity guard (phantom row class)', () => {
|
|
let brain: Brainy<any>
|
|
let staffId: string
|
|
let timeslotId: string
|
|
let customerId: string
|
|
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
|
|
// A real staff Person (the only legitimate match for the roster query).
|
|
staffId = await brain.add({
|
|
data: 'Kristine Perry',
|
|
type: NounType.Person,
|
|
metadata: { entityType: 'staff', firstName: 'Kristine', lastName: 'Perry' }
|
|
})
|
|
// A timeslot Event — matches NEITHER type:Person NOR where.entityType:'staff'
|
|
// (the production phantom was exactly this: a NounType.Event timeslot).
|
|
timeslotId = await brain.add({
|
|
data: 'kitten-experience 2026-08-19 12:00',
|
|
type: NounType.Event,
|
|
metadata: { entityType: 'timeslot', date: '2026-08-19', startTime: '12:00' }
|
|
})
|
|
// A Person that matches type:Person but NOT where.entityType:'staff' — proves
|
|
// the WHERE leg of the guard, not just the structural type leg.
|
|
customerId = await brain.add({
|
|
data: 'Walk-in Customer',
|
|
type: NounType.Person,
|
|
metadata: { entityType: 'customer', firstName: 'Walk-in' }
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
it('healthy index: the discriminant query returns only the staff Person', async () => {
|
|
const rows = await brain.find({ type: NounType.Person, where: { entityType: 'staff' }, limit: 100 })
|
|
expect(rows.map((r) => r.id)).toEqual([staffId])
|
|
})
|
|
|
|
it('drops a phantom of the WRONG TYPE that a corrupt index leaks into the where bucket', async () => {
|
|
// Poison the metadata index so it returns the timeslot's id for the staff
|
|
// query — the precise production corruption (a cross-bucket/stale posting).
|
|
const mi = (brain as any).metadataIndex
|
|
const original = mi.getIdsForFilter.bind(mi)
|
|
mi.getIdsForFilter = async (filter: any): Promise<string[]> => {
|
|
const ids = await original(filter)
|
|
return ids.includes(timeslotId) ? ids : [...ids, timeslotId]
|
|
}
|
|
|
|
const rows = await brain.find({ type: NounType.Person, where: { entityType: 'staff' }, limit: 100 })
|
|
|
|
// The timeslot (NounType.Event, entityType:'timeslot') must NOT appear.
|
|
expect(rows.map((r) => r.id)).not.toContain(timeslotId)
|
|
// The genuine staff row survives.
|
|
expect(rows.map((r) => r.id)).toEqual([staffId])
|
|
|
|
mi.getIdsForFilter = original
|
|
})
|
|
|
|
it('drops a phantom of the RIGHT TYPE but WRONG where-value (exercises the where leg)', async () => {
|
|
// The customer is a Person (passes type) but entityType:'customer' (fails
|
|
// where). A corrupt index leaking it into the 'staff' bucket must still be
|
|
// rejected by the where re-validation.
|
|
const mi = (brain as any).metadataIndex
|
|
const original = mi.getIdsForFilter.bind(mi)
|
|
mi.getIdsForFilter = async (filter: any): Promise<string[]> => {
|
|
const ids = await original(filter)
|
|
return ids.includes(customerId) ? ids : [...ids, customerId]
|
|
}
|
|
|
|
const rows = await brain.find({ type: NounType.Person, where: { entityType: 'staff' }, limit: 100 })
|
|
|
|
expect(rows.map((r) => r.id)).not.toContain(customerId)
|
|
expect(rows.map((r) => r.id)).toEqual([staffId])
|
|
|
|
mi.getIdsForFilter = original
|
|
})
|
|
|
|
it('a healthy query for the phantom\'s own bucket still returns it (guard is not over-eager)', async () => {
|
|
// Sanity: the guard must only drop genuine non-matches. Querying the
|
|
// timeslot's real bucket returns it normally.
|
|
const rows = await brain.find({ type: NounType.Event, where: { entityType: 'timeslot' }, limit: 100 })
|
|
expect(rows.map((r) => r.id)).toEqual([timeslotId])
|
|
})
|
|
})
|