194 lines
6.6 KiB
TypeScript
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)
|
|
})
|
|
})
|