130 lines
5.5 KiB
TypeScript
130 lines
5.5 KiB
TypeScript
/**
|
|
* @module tests/unit/storage/registered-blob-contract
|
|
* @description Declared derived-index blob FAMILIES are
|
|
* undeletable through the storage layer — an in-process GC/sweeper cannot remove
|
|
* a load-bearing index file (the lost-main.dkann class). Covers declare →
|
|
* protected-delete-throws; unregister → delete-ok; transient passthrough;
|
|
* namespace prefix protects children; removeRawPrefix refuses a protected
|
|
* intersection; persistence across reopen; missing-on-open detection.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import * as fs from 'node:fs'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
|
|
import { FileSystemStorage } from '../../../src/storage/adapters/fileSystemStorage.js'
|
|
import { ProtectedArtifactError } from '../../../src/index.js'
|
|
|
|
describe('registered-blob family contract', () => {
|
|
let storage: any
|
|
|
|
beforeEach(async () => {
|
|
storage = new MemoryStorage()
|
|
await storage.init()
|
|
})
|
|
|
|
const seedVectorFamily = async () => {
|
|
await storage.saveBinaryBlob('_system/vector-index/main.dkann', Buffer.from([1]))
|
|
await storage.saveBinaryBlob('_system/vector-index/main.slotmap', Buffer.from([2]))
|
|
await storage.saveBinaryBlob('_system/vector-index/main.slotrev', Buffer.from([3]))
|
|
await storage.registerDerivedFamily({
|
|
name: 'vector-base',
|
|
members: [
|
|
'_system/vector-index/main.dkann',
|
|
'_system/vector-index/main.slotmap',
|
|
'_system/vector-index/main.slotrev'
|
|
]
|
|
})
|
|
}
|
|
|
|
it('a declared member is undeletable (throws ProtectedArtifactError)', async () => {
|
|
await seedVectorFamily()
|
|
await expect(storage.deleteBinaryBlob('_system/vector-index/main.dkann')).rejects.toBeInstanceOf(
|
|
ProtectedArtifactError
|
|
)
|
|
// The blob is still there.
|
|
expect(await storage.loadBinaryBlob('_system/vector-index/main.dkann')).not.toBeNull()
|
|
})
|
|
|
|
it('unregistering the family makes its members deletable again', async () => {
|
|
await seedVectorFamily()
|
|
await storage.unregisterDerivedFamily('vector-base')
|
|
await expect(storage.deleteBinaryBlob('_system/vector-index/main.dkann')).resolves.toBeUndefined()
|
|
expect(await storage.loadBinaryBlob('_system/vector-index/main.dkann')).toBeNull()
|
|
})
|
|
|
|
it('an undeclared blob is still deletable (contract does not lock everything)', async () => {
|
|
await seedVectorFamily()
|
|
await storage.saveBinaryBlob('graph-lsm/source/sstable-1', Buffer.from([9]))
|
|
await expect(storage.deleteBinaryBlob('graph-lsm/source/sstable-1')).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('a transient (*.tmp.*) is deletable even if it sits under a protected namespace', async () => {
|
|
await storage.registerDerivedFamily({
|
|
name: 'vector-ns',
|
|
members: ['_system/vector-index/'],
|
|
namespace: true
|
|
})
|
|
await storage.saveBinaryBlob('_system/vector-index/main.dkann.tmp.123', Buffer.from([1]))
|
|
await expect(
|
|
storage.deleteBinaryBlob('_system/vector-index/main.dkann.tmp.123')
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('a namespace family protects a growing child key', async () => {
|
|
await storage.registerDerivedFamily({
|
|
name: 'vector-segments',
|
|
members: ['_system/vector-index/'],
|
|
namespace: true
|
|
})
|
|
await storage.saveBinaryBlob('_system/vector-index/seg-42', Buffer.from([7]))
|
|
await expect(storage.deleteBinaryBlob('_system/vector-index/seg-42')).rejects.toBeInstanceOf(
|
|
ProtectedArtifactError
|
|
)
|
|
})
|
|
|
|
it('checkDerivedFamiliesPresent() names a member deleted outside the write path', async () => {
|
|
await seedVectorFamily()
|
|
// Simulate an EXTERNAL deleter (bypasses the in-process refusal): drop a
|
|
// member straight from the underlying store.
|
|
;(storage as any).blobStore.delete('_system/vector-index/main.slotmap')
|
|
const incomplete = await storage.checkDerivedFamiliesPresent()
|
|
expect(incomplete).toHaveLength(1)
|
|
expect(incomplete[0].name).toBe('vector-base')
|
|
expect(incomplete[0].missing).toContain('_system/vector-index/main.slotmap')
|
|
expect(incomplete[0].rebuildable).toBe(true)
|
|
})
|
|
|
|
it('no families registered → deleteBinaryBlob behaves exactly as before (no enforcement)', async () => {
|
|
await storage.saveBinaryBlob('some/blob', Buffer.from([1]))
|
|
await expect(storage.deleteBinaryBlob('some/blob')).resolves.toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('registered-blob family protection survives a reopen (FileSystemStorage)', () => {
|
|
let dir: string
|
|
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-regblob-'))
|
|
})
|
|
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))
|
|
|
|
it('a family declared in one session protects members after reopen', async () => {
|
|
const s1: any = new FileSystemStorage(dir)
|
|
await s1.init()
|
|
await s1.saveBinaryBlob('_system/vector-index/main.dkann', Buffer.from([1]))
|
|
await s1.registerDerivedFamily({ name: 'vector-base', members: ['_system/vector-index/main.dkann'] })
|
|
|
|
// Reopen — the registry is loaded from _system/derived-artifacts.json.
|
|
const s2: any = new FileSystemStorage(dir)
|
|
await s2.init()
|
|
expect(await s2.listDerivedFamilies()).toHaveLength(1)
|
|
await expect(s2.deleteBinaryBlob('_system/vector-index/main.dkann')).rejects.toBeInstanceOf(
|
|
ProtectedArtifactError
|
|
)
|
|
// removeRawPrefix nuking the vector dir is also refused.
|
|
await expect(s2.removeRawPrefix('_blobs/_system/vector-index')).rejects.toBeInstanceOf(
|
|
ProtectedArtifactError
|
|
)
|
|
})
|
|
})
|