import { LVS_Search_Mode } from '../_enums/lvs-search-mode.enum'; import { LVS_SearchResult } from '../_models/lvs-search-result.interface'; import { LVS_VectorPool_ControlService } from './lvs-vector-pool.control-service'; describe('| LVS_VectorPool_ControlService', () => { let vectorPool: LVS_VectorPool_ControlService; beforeEach(() => { vectorPool = new LVS_VectorPool_ControlService(); }); describe('| Constructor/Initialization', () => { it('| should initialize with empty pools', () => { const all: Map = vectorPool.getAll(); expect(all.size).toBe(0); }); }); describe('| addVector', () => { it('| should add a valid vector to the pool', () => { const id: string = 'vector1'; const vector: number[] = [1, 2, 3]; vectorPool.addVector(id, vector); const all: Map = vectorPool.getAll(); expect(all.size).toBe(1); expect(all.has(id)).toBeTrue(); expect(all.get(id)).toEqual(vector); }); it('| should add multiple vectors to the pool', () => { vectorPool.addVector('vector1', [1, 2, 3]); vectorPool.addVector('vector2', [4, 5, 6]); vectorPool.addVector('vector3', [7, 8, 9]); const all: Map = vectorPool.getAll(); expect(all.size).toBe(3); }); it('| should throw error when ID is empty string', () => { expect(() => { vectorPool.addVector('', [1, 2, 3]); }).toThrowError('Vector ID is required'); }); it('| should throw error when vector is empty array', () => { expect(() => { vectorPool.addVector('vector1', []); }).toThrowError('Vector must be a non-empty array'); }); it('| should throw error when vector is not an array', () => { expect(() => { vectorPool.addVector('vector1', null as any); }).toThrowError('Vector must be a non-empty array'); }); it('| should throw error when vector contains non-finite number', () => { expect(() => { vectorPool.addVector('vector1', [1, 2, Infinity]); }).toThrowError('Vector must contain only finite numbers at index 2'); }); it('| should throw error when vector contains NaN', () => { expect(() => { vectorPool.addVector('vector1', [1, 2, NaN]); }).toThrowError('Vector must contain only finite numbers at index 2'); }); it('| should store normalized vector internally', () => { const id: string = 'vector1'; const vector: number[] = [3, 4]; // Magnitude = 5, normalized = [0.6, 0.8] vectorPool.addVector(id, vector); // Verify original vector is stored const all: Map = vectorPool.getAll(); expect(all.get(id)).toEqual(vector); }); }); describe('| removeVector', () => { it('| should remove an existing vector from the pool', () => { vectorPool.addVector('vector1', [1, 2, 3]); vectorPool.addVector('vector2', [4, 5, 6]); vectorPool.removeVector('vector1'); const all: Map = vectorPool.getAll(); expect(all.size).toBe(1); expect(all.has('vector1')).toBeFalse(); expect(all.has('vector2')).toBeTrue(); }); it('| should not throw when removing non-existent vector', () => { expect(() => { vectorPool.removeVector('non-existent'); }).not.toThrow(); }); it('| should remove vector from both pools', () => { vectorPool.addVector('vector1', [1, 2, 3]); vectorPool.removeVector('vector1'); const all: Map = vectorPool.getAll(); expect(all.size).toBe(0); }); }); describe('| updateVector', () => { it('| should update an existing vector', () => { vectorPool.addVector('vector1', [1, 2, 3]); const newVector: number[] = [4, 5, 6]; vectorPool.updateVector('vector1', newVector); const all: Map = vectorPool.getAll(); expect(all.get('vector1')).toEqual(newVector); }); it('| should throw error when updating non-existent vector', () => { expect(() => { vectorPool.updateVector('non-existent', [1, 2, 3]); }).toThrowError('Vector with ID "non-existent" does not exist'); }); }); describe('| clearPool', () => { it('| should clear a populated pool', () => { vectorPool.addVector('vector1', [1, 2, 3]); vectorPool.addVector('vector2', [4, 5, 6]); vectorPool.addVector('vector3', [7, 8, 9]); vectorPool.clearPool(); const all: Map = vectorPool.getAll(); expect(all.size).toBe(0); }); it('| should not throw when clearing empty pool', () => { expect(() => { vectorPool.clearPool(); }).not.toThrow(); }); }); describe('| getAll', () => { it('| should return a copy of the pool, not a reference', () => { vectorPool.addVector('vector1', [1, 2, 3]); const all1: Map = vectorPool.getAll(); const all2: Map = vectorPool.getAll(); expect(all1).not.toBe(all2); expect(all1.size).toBe(all2.size); }); it('| should return empty map when pool is empty', () => { const all: Map = vectorPool.getAll(); expect(all.size).toBe(0); }); }); describe('| cosineSimilarity (static)', () => { it('| should return 1 for identical vectors', () => { const vector: number[] = [1, 2, 3]; const similarity: number = LVS_VectorPool_ControlService.cosineSimilarity(vector, vector); expect(similarity).toBeCloseTo(1, 10); }); it('| should return approximately 0 for orthogonal vectors', () => { const vector1: number[] = [1, 0, 0]; const vector2: number[] = [0, 1, 0]; const similarity: number = LVS_VectorPool_ControlService.cosineSimilarity(vector1, vector2); expect(similarity).toBeCloseTo(0, 10); }); it('| should throw error for vectors with different dimensions', () => { const vector1: number[] = [1, 2, 3]; const vector2: number[] = [1, 2]; expect(() => { LVS_VectorPool_ControlService.cosineSimilarity(vector1, vector2); }).toThrowError('Vectors must have the same dimension. Got 3 and 2'); }); it('| should calculate correct cosine similarity for known vectors', () => { // Two vectors pointing in similar direction const vector1: number[] = [1, 1, 0]; const vector2: number[] = [1, 0, 0]; // Normalized: [1/√2, 1/√2, 0] and [1, 0, 0] // Dot product: 1/√2 ≈ 0.707 const similarity: number = LVS_VectorPool_ControlService.cosineSimilarity(vector1, vector2); expect(similarity).toBeCloseTo(1 / Math.sqrt(2), 10); }); it('| should handle zero vector', () => { const vector1: number[] = [0, 0, 0]; const vector2: number[] = [1, 2, 3]; const similarity: number = LVS_VectorPool_ControlService.cosineSimilarity(vector1, vector2); expect(similarity).toBeCloseTo(0, 10); }); }); describe('| l2Distance (static)', () => { it('| should return 0 for identical vectors', () => { const vector: number[] = [1, 2, 3]; const distance: number = LVS_VectorPool_ControlService.l2Distance(vector, vector); expect(distance).toBe(0); }); it('| should throw error for vectors with different dimensions', () => { const vector1: number[] = [1, 2, 3]; const vector2: number[] = [1, 2]; expect(() => { LVS_VectorPool_ControlService.l2Distance(vector1, vector2); }).toThrowError('Vectors must have the same dimension. Got 3 and 2'); }); it('| should calculate correct L2 distance for known vectors', () => { const vector1: number[] = [0, 0, 0]; const vector2: number[] = [3, 4, 0]; // Distance = √(3² + 4² + 0²) = √25 = 5 const distance: number = LVS_VectorPool_ControlService.l2Distance(vector1, vector2); expect(distance).toBe(5); }); it('| should calculate correct L2 distance for 1D vectors', () => { const vector1: number[] = [5]; const vector2: number[] = [2]; const distance: number = LVS_VectorPool_ControlService.l2Distance(vector1, vector2); expect(distance).toBe(3); }); }); describe('| search', () => { beforeEach(() => { // Add test vectors vectorPool.addVector('vec1', [1, 0, 0]); vectorPool.addVector('vec2', [0, 1, 0]); vectorPool.addVector('vec3', [0, 0, 1]); vectorPool.addVector('vec4', [1, 1, 0]); }); it('| should return empty array for empty pool', () => { const emptyPool: LVS_VectorPool_ControlService = new LVS_VectorPool_ControlService(); const results: LVS_SearchResult[] = emptyPool.search([1, 0, 0], 3, LVS_Search_Mode.cosineSimilarity); expect(results.length).toBe(0); }); it('| should return results in descending order for cosine similarity', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 4, LVS_Search_Mode.cosineSimilarity ); expect(results.length).toBe(4); // vec1 should be most similar (identical) expect(results[0].id).toBe('vec1'); expect(results[0].score).toBeCloseTo(1, 10); // Results should be in descending order for (let i: number = 0; i < results.length - 1; i++) { expect(results[i].score).toBeGreaterThanOrEqual(results[i + 1].score); } }); it('| should return results in ascending order for L2 distance', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 4, LVS_Search_Mode.l2Distance ); expect(results.length).toBe(4); // vec1 should be closest (distance = 0) expect(results[0].id).toBe('vec1'); expect(results[0].score).toBe(0); // Results should be in ascending order for (let i: number = 0; i < results.length - 1; i++) { expect(results[i].score).toBeLessThanOrEqual(results[i + 1].score); } }); it('| should limit results to top-K when k < pool size', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 2, LVS_Search_Mode.cosineSimilarity ); expect(results.length).toBe(2); }); it('| should return all results when k > pool size', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 10, LVS_Search_Mode.cosineSimilarity ); expect(results.length).toBe(4); }); it('| should return all results when k = pool size', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 4, LVS_Search_Mode.cosineSimilarity ); expect(results.length).toBe(4); }); it('| should throw error for dimension mismatch', () => { const query: number[] = [1, 0]; // 2D query, pool has 3D vectors expect(() => { vectorPool.search(query, 3, LVS_Search_Mode.cosineSimilarity); }).toThrowError('Query vector dimension (2) does not match pool vector dimension (3)'); }); it('| should throw error for empty query vector', () => { expect(() => { vectorPool.search([], 3, LVS_Search_Mode.cosineSimilarity); }).toThrowError('Query vector must be a non-empty array'); }); it('| should throw error for non-array query', () => { expect(() => { vectorPool.search(null as any, 3, LVS_Search_Mode.cosineSimilarity); }).toThrowError('Query vector must be a non-empty array'); }); it('| should throw error for k = 0', () => { expect(() => { vectorPool.search([1, 0, 0], 0, LVS_Search_Mode.cosineSimilarity); }).toThrowError('k must be a positive number'); }); it('| should throw error for negative k', () => { expect(() => { vectorPool.search([1, 0, 0], -1, LVS_Search_Mode.cosineSimilarity); }).toThrowError('k must be a positive number'); }); it('| should throw error for unknown search mode', () => { expect(() => { vectorPool.search([1, 0, 0], 3, 'unknown-mode' as LVS_Search_Mode); }).toThrowError('Unknown search mode: unknown-mode'); }); it('| should return correct scores for cosine similarity', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 4, LVS_Search_Mode.cosineSimilarity ); // vec1 should have score = 1 (identical) const vec1Result: LVS_SearchResult | undefined = results.find((r: LVS_SearchResult) => r.id === 'vec1'); expect(vec1Result).toBeDefined(); expect(vec1Result!.score).toBeCloseTo(1, 10); }); it('| should produce cosine scores IDENTICAL to static cosineSimilarity (precomputed-normalized perf optimization, BFR-AM-013)', () => { // Regression-guard: a search optimalizalt (precomputed-normalized + alloc-mentes) cosine-ja // viselkedes-identikus a regi per-osszehasonlitas `cosineSimilarity(query, vector)`-ral. const query: number[] = [0.3, 0.7, 0.2]; const results: LVS_SearchResult[] = vectorPool.search(query, 4, LVS_Search_Mode.cosineSimilarity); expect(results.length).toBe(4); const originals: Map = vectorPool.getAll(); for (const result of results) { const original: number[] | undefined = originals.get(result.id); expect(original).toBeDefined(); const expected: number = LVS_VectorPool_ControlService.cosineSimilarity(query, original!); expect(result.score).toBeCloseTo(expected, 10); } }); it('| should return correct scores for L2 distance', () => { const query: number[] = [1, 0, 0]; const results: LVS_SearchResult[] = vectorPool.search( query, 4, LVS_Search_Mode.l2Distance ); // vec1 should have score = 0 (identical) const vec1Result: LVS_SearchResult | undefined = results.find((r: LVS_SearchResult) => r.id === 'vec1'); expect(vec1Result).toBeDefined(); expect(vec1Result!.score).toBe(0); }); it('| should restrict results to the candidateIds set (cosine)', () => { const query: number[] = [1, 0, 0]; const candidateIds: Set = new Set(['vec2', 'vec3']); const results: LVS_SearchResult[] = vectorPool.search( query, 10, LVS_Search_Mode.cosineSimilarity, candidateIds ); // CSAK a jelölt-halmaz ID-jai jelenhetnek meg (vec1/vec4 kizárva). expect(results.length).toBe(2); for (const r of results) { expect(candidateIds.has(r.id)).toBe(true); } }); it('| should restrict results to the candidateIds set (L2)', () => { const query: number[] = [1, 0, 0]; const candidateIds: Set = new Set(['vec1', 'vec4']); const results: LVS_SearchResult[] = vectorPool.search( query, 10, LVS_Search_Mode.l2Distance, candidateIds ); expect(results.length).toBe(2); expect(results.map((r: LVS_SearchResult) => r.id).sort()).toEqual(['vec1', 'vec4']); }); it('| should give candidateId-scoped scores IDENTICAL to an unfiltered search filtered post-hoc', () => { // A scoped keresés EREDMÉNYE (score-ok + sorrend) bit-azonos a teljes keresés azon // részhalmazával, amit utólag a candidate-ID-kra szűrnénk — a szűrés csak SZŰKÍT, nem torzít. const query: number[] = [1, 1, 0]; const candidateIds: Set = new Set(['vec1', 'vec2', 'vec4']); const scoped: LVS_SearchResult[] = vectorPool.search(query, 10, LVS_Search_Mode.cosineSimilarity, candidateIds); const full: LVS_SearchResult[] = vectorPool .search(query, 10, LVS_Search_Mode.cosineSimilarity) .filter((r: LVS_SearchResult) => candidateIds.has(r.id)); expect(scoped.length).toBe(full.length); for (let i: number = 0; i < scoped.length; i++) { expect(scoped[i].id).toBe(full[i].id); expect(scoped[i].score).toBeCloseTo(full[i].score, 12); } }); it('| should return empty array for an empty candidateIds set', () => { const results: LVS_SearchResult[] = vectorPool.search( [1, 0, 0], 10, LVS_Search_Mode.cosineSimilarity, new Set() ); expect(results.length).toBe(0); }); }); describe('| normalizedOnly mode (memory-optimized cosine-only pool)', () => { let pool: LVS_VectorPool_ControlService; beforeEach(() => { pool = new LVS_VectorPool_ControlService({ normalizedOnly: true }); pool.addVector('vec1', [1, 0, 0]); pool.addVector('vec2', [0, 1, 0]); pool.addVector('vec3', [1, 1, 0]); }); it('| should NOT store raw vectors (getAll empty) but size() correct', () => { expect(pool.getAll().size).toBe(0); expect(pool.size()).toBe(3); }); it('| cosine search works identically to a normal pool', () => { const normal: LVS_VectorPool_ControlService = new LVS_VectorPool_ControlService(); normal.addVector('vec1', [1, 0, 0]); normal.addVector('vec2', [0, 1, 0]); normal.addVector('vec3', [1, 1, 0]); const query: number[] = [1, 0.2, 0]; const a: LVS_SearchResult[] = pool.search(query, 3, LVS_Search_Mode.cosineSimilarity); const b: LVS_SearchResult[] = normal.search(query, 3, LVS_Search_Mode.cosineSimilarity); expect(a.length).toBe(b.length); for (let i: number = 0; i < a.length; i++) { expect(a[i].id).toBe(b[i].id); expect(a[i].score).toBeCloseTo(b[i].score, 12); } }); it('| cosine search honors candidateIds in normalizedOnly mode', () => { const results: LVS_SearchResult[] = pool.search( [1, 0, 0], 10, LVS_Search_Mode.cosineSimilarity, new Set(['vec2', 'vec3']) ); expect(results.length).toBe(2); for (const r of results) { expect(['vec2', 'vec3']).toContain(r.id); } }); it('| l2Distance throws in normalizedOnly mode (no raw vectors)', () => { expect(() => pool.search([1, 0, 0], 3, LVS_Search_Mode.l2Distance)) .toThrowError(/normalizedOnly/); }); it('| updateVector works (checks normalized pool, not raw)', () => { expect(() => pool.updateVector('vec1', [0, 0, 1])).not.toThrow(); expect(() => pool.updateVector('missing', [0, 0, 1])).toThrowError(/does not exist/); }); }); });