/** * @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 — `'hnsw-js'` for * Brainy's own JS index, a provider's own required `name` when it * self-identifies, and the tolerant-loud `'unknown-provider'` literal * (plus one console.warn) when a runtime instance lacks `name` * altogether (an older native provider compiled against the previous, * optional `providerId` contract) — never a silently-wrong guess. */ import { describe, it, expect, vi, afterEach } 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 name: 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', () => { afterEach(() => { vi.restoreAllMocks() }) it('stamps the real JS HNSW index as hnsw-js (self-identifying name)', () => { 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(hnsw-js)') expect(removeOp.name).toBe('RemoveFromVectorIndex(hnsw-js)') }) it('stamps a self-identifying provider\'s own name, 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('tolerant-loud: stamps "unknown-provider" and warns exactly once when a runtime provider lacks the required `name` (an older native provider compiled against the previous optional `providerId` contract)', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) // Simulate a native provider compiled before `name` was required — the // TypeScript contract requires `name`, but `as unknown as` mirrors what // actually reaches this code at runtime from an un-rebuilt native addon. const legacyProvider = { addItem: async (item: VectorDocument) => item.id, removeItem: async () => true, search: async () => [], size: () => 0, clear: () => {}, rebuild: async () => {}, flush: async () => 0, getPersistMode: () => 'immediate' as const } as unknown as VectorIndexProvider const addOp = new AddToVectorIndexOperation(legacyProvider, 'id-1', [1, 2, 3]) const removeOp = new RemoveFromVectorIndexOperation(legacyProvider, '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('hnsw-js') // Loud, not silent — but exactly once per provider instance, not once // per op stamped against it. expect(warnSpy).toHaveBeenCalledTimes(1) expect(warnSpy.mock.calls[0][0]).toContain('name') }) }) })