open-brainy/tests/integration/level-field-shadow.test.ts
David Snelling 1a09be0628
Some checks failed
CI / Node 22 (push) Successful in 12m17s
CI / Node 24 (push) Has been cancelled
CI / Bun (latest) (push) Has been cancelled
fix: user metadata named 'level' is a real field everywhere — the engine-internal node layer no longer shadows it in sort/filter/aggregation, and the indexing views stop stamping a phantom 0 into its column; index epoch 2 rebuilds existing brains at first open
Also completes the v8.10.2 write-granularity law for the transact() plan
path: a metadata-only batch update never rewrites the vector-bearing noun
record (planUpdate staged the unconditional save the update() fix removed).
Seven pins in tests/integration/level-field-shadow.test.ts including the
reporting consumer's exact repro rows; orderBy JSDoc documents the ordering
contract and the announced field-addressing law.
2026-08-03 11:57:32 -07:00

147 lines
5.1 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)
})
})