/** * @module tests/unit/transaction/vectorIndexOperations-rename * @description Coverage for the backend-neutral vector-index transaction * operations (formerly `AddToHNSWOperation` / `RemoveFromHNSWOperation`). * These op-name strings surface directly in consumer-visible transaction * journals and timings — a fossil "HNSW" name misdirects an operator running * a native (non-HNSW) vector provider. Verifies: * 1. rollback wiring stayed byte-identical under the rename, * 2. the emitted `name` stamps the active backend — `'js-hnsw'` for * Brainy's own JS index, a provider's own `providerId` when it * self-identifies, and the honest `'unknown-provider'` literal when * neither applies (never a silently-wrong guess). */ import { describe, it, expect } from 'vitest' import { AddToVectorIndexOperation, RemoveFromVectorIndexOperation, BatchAddToVectorIndexOperation } from '../../../src/transaction/operations/IndexOperations.js' import type { VectorIndexProvider } from '../../../src/plugin.js' import type { VectorDocument, Vector } from '../../../src/coreTypes.js' import { JsHnswVectorIndex } from '../../../src/hnsw/hnswIndex.js' /** * A minimal, real (not mocked) VectorIndexProvider implementation backed by * a plain Map — exercises the exact contract the operations call through, * without any Brainy internals. */ class FakeVectorProvider implements VectorIndexProvider { readonly items = new Map() constructor(readonly providerId?: string) {} async addItem(item: VectorDocument): Promise { this.items.set(item.id, item.vector) return item.id } async removeItem(id: string): Promise { return this.items.delete(id) } async getItem(id: string): Promise { const vector = this.items.get(id) return vector ? { id, vector } : undefined } async search(): Promise> { return [] } size(): number { return this.items.size } clear(): void { this.items.clear() } async rebuild(): Promise {} async flush(): Promise { return 0 } getPersistMode(): 'immediate' | 'deferred' { return 'immediate' } } describe('Vector index transaction operations — backend-neutral rename', () => { describe('rollback wiring (byte-identical behavior under the rename)', () => { it('AddToVectorIndexOperation rollback removes a newly-added item', async () => { const provider = new FakeVectorProvider('fake-provider') const op = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3]) const rollback = await op.execute() expect(provider.items.has('id-1')).toBe(true) await rollback() expect(provider.items.has('id-1')).toBe(false) }) it('AddToVectorIndexOperation rollback is a no-op when the item pre-existed (update semantics)', async () => { const provider = new FakeVectorProvider('fake-provider') await provider.addItem({ id: 'id-1', vector: [9, 9, 9] }) const op = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3]) const rollback = await op.execute() expect(provider.items.get('id-1')).toEqual([1, 2, 3]) await rollback() // Pre-existing item is NOT removed by rollback — it stays (the update // itself is not undone by this op; that's RemoveFromVectorIndexOperation's job). expect(provider.items.has('id-1')).toBe(true) }) it('RemoveFromVectorIndexOperation rollback re-adds the item with its original vector', async () => { const provider = new FakeVectorProvider('fake-provider') await provider.addItem({ id: 'id-1', vector: [4, 5, 6] }) const op = new RemoveFromVectorIndexOperation(provider, 'id-1', [4, 5, 6]) const rollback = await op.execute() expect(provider.items.has('id-1')).toBe(false) await rollback() expect(provider.items.get('id-1')).toEqual([4, 5, 6]) }) it('BatchAddToVectorIndexOperation rolls back every item in reverse order on undo', async () => { const provider = new FakeVectorProvider('fake-provider') const op = new BatchAddToVectorIndexOperation(provider, [ { id: 'a', vector: [1] }, { id: 'b', vector: [2] }, { id: 'c', vector: [3] } ]) const rollback = await op.execute() expect([...provider.items.keys()].sort()).toEqual(['a', 'b', 'c']) await rollback() expect(provider.items.size).toBe(0) }) }) describe('backend stamping in the emitted op name', () => { it('stamps the real JS HNSW index as js-hnsw (self-identifying providerId)', () => { const index = new JsHnswVectorIndex() const addOp = new AddToVectorIndexOperation(index, 'id-1', [1, 2, 3]) const removeOp = new RemoveFromVectorIndexOperation(index, 'id-1', [1, 2, 3]) expect(addOp.name).toBe('AddToVectorIndex(js-hnsw)') expect(removeOp.name).toBe('RemoveFromVectorIndex(js-hnsw)') }) it('stamps a self-identifying provider\'s own providerId, never "HNSW"', () => { const provider = new FakeVectorProvider('acme-diskann') const addOp = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3]) const removeOp = new RemoveFromVectorIndexOperation(provider, 'id-1', [1, 2, 3]) const batchOp = new BatchAddToVectorIndexOperation(provider, [{ id: 'a', vector: [1] }]) expect(addOp.name).toBe('AddToVectorIndex(acme-diskann)') expect(removeOp.name).toBe('RemoveFromVectorIndex(acme-diskann)') expect(batchOp.name).toBe('BatchAddToVectorIndex(acme-diskann)') expect(addOp.name).not.toContain('HNSW') expect(removeOp.name).not.toContain('HNSW') }) it('honestly reports "unknown-provider" rather than guessing when a provider omits providerId', () => { const provider = new FakeVectorProvider(undefined) const addOp = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3]) const removeOp = new RemoveFromVectorIndexOperation(provider, 'id-1', [1, 2, 3]) expect(addOp.name).toBe('AddToVectorIndex(unknown-provider)') expect(removeOp.name).toBe('RemoveFromVectorIndex(unknown-provider)') // Never silently falls back to the JS engine's name for a provider it // knows nothing about — that's the exact fossil-naming bug being fixed. expect(addOp.name).not.toContain('js-hnsw') }) }) })