63 lines
2.7 KiB
TypeScript
63 lines
2.7 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/integration/null-metadata-delete
|
||
|
|
* @description The null-metadata delete skip is CLOSED. remove() used to
|
||
|
|
* guard its index legs with `if (metadata)` — a row whose canonical
|
||
|
|
* metadata was unreadable at delete time (torn, or a leg lost to an old
|
||
|
|
* defect) kept its postings FOREVER, silently. Now: the JS index gets an
|
||
|
|
* id-keyed cleanup (deleted bitmap + id mapper), the id-keyed native
|
||
|
|
* contract is used when a provider offers it, and the one remaining
|
||
|
|
* skip-shape (native without the contract) is narrated and tracked, never
|
||
|
|
* silent. Pinned: a metadata-less row with live postings deletes cleanly
|
||
|
|
* and leaves the query universe.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, afterEach } from 'vitest'
|
||
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
||
|
|
import { tmpdir } from 'node:os'
|
||
|
|
import { join } from 'node:path'
|
||
|
|
import { Brainy } from '../../src/index.js'
|
||
|
|
import { NounType } from '../../src/types/graphTypes.js'
|
||
|
|
|
||
|
|
type RawBox = {
|
||
|
|
storage: {
|
||
|
|
readNounRaw(id: string): Promise<{ metadata: unknown; vector: unknown }>
|
||
|
|
writeNounRaw(id: string, r: { metadata: unknown; vector: unknown }): Promise<void>
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const dirs: string[] = []
|
||
|
|
const brains: Brainy[] = []
|
||
|
|
afterEach(async () => {
|
||
|
|
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||
|
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('null-metadata delete', () => {
|
||
|
|
it('a row whose metadata leg is gone still deletes — id-keyed cleanup, no silent skip, gone from the query universe', async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-nullmeta-del-'))
|
||
|
|
dirs.push(dir)
|
||
|
|
const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true })
|
||
|
|
await brain.init()
|
||
|
|
brains.push(brain)
|
||
|
|
|
||
|
|
const keep = await brain.add({ data: 'survivor', type: NounType.Document, metadata: { team: 'atlas' } })
|
||
|
|
const victim = await brain.add({ data: 'doomed', type: NounType.Document, metadata: { team: 'atlas' } })
|
||
|
|
await brain.flush()
|
||
|
|
expect((await brain.find({ where: { team: 'atlas' } })).length).toBe(2)
|
||
|
|
|
||
|
|
// Manufacture the shape: the victim's metadata leg vanishes behind the
|
||
|
|
// engine's back (vector leg + postings stay live).
|
||
|
|
const storage = (brain as unknown as RawBox).storage
|
||
|
|
const raw = await storage.readNounRaw(victim)
|
||
|
|
await storage.writeNounRaw(victim, { metadata: null, vector: raw.vector })
|
||
|
|
|
||
|
|
// THE PIN: the delete neither throws nor silently strands postings.
|
||
|
|
await brain.remove(victim)
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
const after = await brain.find({ where: { team: 'atlas' } })
|
||
|
|
expect(after.length, 'victim left the query universe; survivor serves').toBe(1)
|
||
|
|
expect(after[0].id).toBe(keep)
|
||
|
|
expect(await brain.get(victim)).toBeNull()
|
||
|
|
}, 120000)
|
||
|
|
})
|