open-brainy/tests/integration/level-field-shadow.test.ts

194 lines
6.6 KiB
TypeScript

/**
* @module tests/integration/level-field-shadow
* @description The reserved-name shadow fix (VENUE-BRAINY-ORDERBY-NOOP,
* 2026-08-03): `level` is HNSW plumbing, not an entity field — it must never
* shadow user metadata of the same name. Pre-fix, STANDARD_ENTITY_FIELDS
* listed `level`, so every by-name read returned the engine's internal 0
* (all-equal → stable sort → insertion order, silently), and the indexing
* views stamped level:0 into the same flattened column as user values
* (multi-valued [0, real] poison). Laws:
* (1) venue's exact repro sorts: three adds with metadata.level 3/9/6 →
* find({orderBy:'level'}) returns 9,6,3 desc and 3,6,9 asc;
* (2) where {level: N} matches through filter AND egress guard;
* (3) the index column carries the user value only (no 0 poison);
* (4) update() keeps `level` readable (the update indexing view is clean too);
* (5) the transact() update path never rewrites the noun record on a
* metadata-only patch (the planUpdate granularity completion).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import { EXPECTED_INDEX_EPOCH } from '../../src/storage/brainFormat.js'
const stubEmbedding = async (text: string): Promise<number[]> => {
const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return new Array(384).fill(0).map((_, i) => Math.sin(hash + i))
}
describe('level field shadow — user metadata named level is a real field', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({
requireSubtype: false,
storage: { type: 'memory' as const },
embeddingFunction: stubEmbedding
})
await brain.init()
})
afterEach(async () => {
await brain.close()
})
async function addProbeRows(): Promise<string[]> {
const ids: string[] = []
for (const level of [3, 9, 6]) {
ids.push(
await brain.add({
data: `probe character level ${level}`,
type: NounType.Person,
subtype: 'probe-char',
metadata: { name: `char-${level}`, level }
})
)
}
return ids
}
it("venue's exact repro: orderBy 'level' sorts desc and asc", async () => {
await addProbeRows()
const desc = await brain.find({
type: NounType.Person,
subtype: 'probe-char',
orderBy: 'level',
order: 'desc',
limit: 100
})
expect(desc.map((r: any) => r.metadata?.level)).toEqual([9, 6, 3])
const asc = await brain.find({
type: NounType.Person,
subtype: 'probe-char',
orderBy: 'level',
order: 'asc',
limit: 100
})
expect(asc.map((r: any) => r.metadata?.level)).toEqual([3, 6, 9])
})
it('ordered reads are COMPLETE — no row dropped (the 2-of-3 face)', async () => {
const ids = await addProbeRows()
const desc = await brain.find({
type: NounType.Person,
subtype: 'probe-char',
orderBy: 'level',
order: 'desc',
limit: 100
})
expect(desc).toHaveLength(3)
expect(new Set(desc.map((r: any) => r.id))).toEqual(new Set(ids))
})
it('where {level: N} matches through the filter and the egress guard', async () => {
const ids = await addProbeRows()
const hit = await brain.find({ where: { level: 9 } })
expect(hit).toHaveLength(1)
expect(hit[0].id).toBe(ids[1])
expect(hit[0].metadata?.level).toBe(9)
})
it('the index column carries ONLY the user value (no 0 poison)', async () => {
const ids = await addProbeRows()
const metadataIndex = (brain as any).metadataIndex
const value = await metadataIndex.getFieldValueForEntity(ids[1], 'level')
expect(value).toBe(9)
// Zero must not match anything — pre-fix every entity carried a phantom 0.
const phantom = await brain.find({ where: { level: 0 } })
expect(phantom).toHaveLength(0)
})
it('update() keeps level readable (the update indexing view is clean)', async () => {
const ids = await addProbeRows()
await brain.update({ id: ids[0], metadata: { level: 12 } })
const desc = await brain.find({
type: NounType.Person,
subtype: 'probe-char',
orderBy: 'level',
order: 'desc',
limit: 100
})
expect(desc.map((r: any) => r.metadata?.level)).toEqual([12, 9, 6])
})
it('transact() metadata-only update never rewrites the noun record', async () => {
const ids = await addProbeRows()
const storage = (brain as any).storage
const saveNounSpy = vi.spyOn(storage, 'saveNoun')
await brain.transact([
{ op: 'update', id: ids[0], metadata: { level: 4 } },
{ op: 'update', id: ids[2], metadata: { level: 7 } }
])
expect(saveNounSpy).not.toHaveBeenCalled()
saveNounSpy.mockRestore()
const after = await brain.get(ids[0], { includeVectors: true })
expect(after?.metadata?.level).toBe(4)
expect(Array.isArray(after?.vector) && after!.vector!.length).toBe(384)
})
it('this build runs index epoch 2 (the paired level-indexability rebuild)', () => {
expect(EXPECTED_INDEX_EPOCH).toBe(2)
})
})
describe('noun-record writes never stamp over stored graph state', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({
requireSubtype: false,
storage: { type: 'memory' as const },
embeddingFunction: stubEmbedding
})
await brain.init()
})
afterEach(async () => {
await brain.close()
})
it('a data-changing update preserves LEGACY inline connections in the record', async () => {
// Codec-era records carry an EMPTY connections field by design (the
// adjacency lives in a separate compressed blob) — the clobber window
// exists only for legacy pre-codec records whose adjacency is inline.
// Simulate one: write the record with inline connections directly.
const id = await brain.add({
data: 'legacy-shaped node',
type: NounType.Concept,
metadata: { n: 1 }
})
const storage = (brain as any).storage
const rec = await storage.getNoun(id)
const legacy = {
...rec,
connections: new Map([[0, new Set(['00000000-0000-4000-8000-00000000aaaa'])]]),
level: 1
}
await storage.saveNoun(legacy)
const before = await storage.getNoun(id)
expect(before.connections.size).toBeGreaterThan(0)
// A data-changing update stages SaveNounOperation with placeholder
// adjacency — the legacy inline connections must survive the write.
await brain.update({ id, data: 'completely re-embedded text' })
const after = await storage.getNoun(id)
expect(after.connections.size).toBeGreaterThan(0)
expect(after.level).toBe(1)
})
})