git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
307 lines
9.4 KiB
TypeScript
307 lines
9.4 KiB
TypeScript
/**
|
|
* RuVector WASM Bindings - Integration Tests
|
|
*
|
|
* Tests for RuVector vector database integration with WASM bindings
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
createMockRuVectorBindings,
|
|
MockWasmVectorIndex,
|
|
MockWasmEmbedder,
|
|
mockWasmLoader
|
|
} from '../../mocks/wasm.mock';
|
|
|
|
describe('RuVector WASM Integration', () => {
|
|
let ruvector: ReturnType<typeof createMockRuVectorBindings>;
|
|
|
|
beforeEach(() => {
|
|
ruvector = createMockRuVectorBindings();
|
|
});
|
|
|
|
describe('Document Indexing', () => {
|
|
it('should index single document', async () => {
|
|
await ruvector.index('doc-1', 'This is a test document about programming');
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(1);
|
|
});
|
|
|
|
it('should index multiple documents', async () => {
|
|
await ruvector.index('doc-1', 'React component patterns');
|
|
await ruvector.index('doc-2', 'Vue.js best practices');
|
|
await ruvector.index('doc-3', 'Angular architecture guide');
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(3);
|
|
});
|
|
|
|
it('should batch index documents', async () => {
|
|
const documents = [
|
|
{ id: 'doc-1', text: 'JavaScript fundamentals' },
|
|
{ id: 'doc-2', text: 'TypeScript advanced types' },
|
|
{ id: 'doc-3', text: 'Node.js performance tuning' },
|
|
{ id: 'doc-4', text: 'Deno runtime overview' }
|
|
];
|
|
|
|
await ruvector.batchIndex(documents);
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(4);
|
|
});
|
|
|
|
it('should handle empty documents', async () => {
|
|
await ruvector.index('empty-doc', '');
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(1);
|
|
});
|
|
|
|
it('should handle very long documents', async () => {
|
|
const longText = 'word '.repeat(10000);
|
|
|
|
await ruvector.index('long-doc', longText);
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Semantic Search', () => {
|
|
beforeEach(async () => {
|
|
await ruvector.batchIndex([
|
|
{ id: 'react-hooks', text: 'React hooks provide a way to use state and lifecycle in functional components' },
|
|
{ id: 'vue-composition', text: 'Vue composition API offers reactive state management' },
|
|
{ id: 'angular-rxjs', text: 'Angular uses RxJS for reactive programming patterns' },
|
|
{ id: 'svelte-stores', text: 'Svelte stores provide simple state management' },
|
|
{ id: 'solid-signals', text: 'SolidJS signals offer fine-grained reactivity' }
|
|
]);
|
|
});
|
|
|
|
it('should find semantically similar documents', async () => {
|
|
const results = await ruvector.search('React state management', 3);
|
|
|
|
expect(results).toHaveLength(3);
|
|
expect(results[0].score).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should rank results by similarity', async () => {
|
|
const results = await ruvector.search('React hooks', 5);
|
|
|
|
// Results should be sorted by score descending
|
|
for (let i = 1; i < results.length; i++) {
|
|
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
|
}
|
|
});
|
|
|
|
it('should respect topK limit', async () => {
|
|
const results = await ruvector.search('state management', 2);
|
|
|
|
expect(results).toHaveLength(2);
|
|
});
|
|
|
|
it('should handle queries with no good matches', async () => {
|
|
const results = await ruvector.search('quantum computing algorithms', 3);
|
|
|
|
// Should still return results, just with lower scores
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// Scores should be lower for unrelated queries
|
|
expect(results[0].score).toBeLessThan(0.9);
|
|
});
|
|
});
|
|
|
|
describe('Embedding Operations', () => {
|
|
it('should generate consistent embeddings', () => {
|
|
const text = 'Consistent embedding test';
|
|
|
|
const embedding1 = ruvector.embedder.embed(text);
|
|
const embedding2 = ruvector.embedder.embed(text);
|
|
|
|
expect(embedding1.length).toBe(embedding2.length);
|
|
for (let i = 0; i < embedding1.length; i++) {
|
|
expect(embedding1[i]).toBe(embedding2[i]);
|
|
}
|
|
});
|
|
|
|
it('should generate different embeddings for different texts', () => {
|
|
const embedding1 = ruvector.embedder.embed('First text');
|
|
const embedding2 = ruvector.embedder.embed('Second completely different text');
|
|
|
|
let identical = true;
|
|
for (let i = 0; i < embedding1.length; i++) {
|
|
if (embedding1[i] !== embedding2[i]) {
|
|
identical = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(identical).toBe(false);
|
|
});
|
|
|
|
it('should return correct dimension', () => {
|
|
expect(ruvector.embedder.dimension()).toBe(384);
|
|
});
|
|
|
|
it('should handle batch embedding', () => {
|
|
const texts = ['Text 1', 'Text 2', 'Text 3'];
|
|
const embeddings = ruvector.embedder.embedBatch(texts);
|
|
|
|
expect(embeddings).toHaveLength(3);
|
|
embeddings.forEach(e => {
|
|
expect(e.length).toBe(384);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Vector Index Operations', () => {
|
|
it('should add and retrieve vectors', () => {
|
|
const embedding = ruvector.embedder.embed('Test document');
|
|
ruvector.vectorIndex.add('test-id', embedding);
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(1);
|
|
});
|
|
|
|
it('should delete vectors', () => {
|
|
const embedding = ruvector.embedder.embed('To delete');
|
|
ruvector.vectorIndex.add('delete-id', embedding);
|
|
|
|
const deleted = ruvector.vectorIndex.delete('delete-id');
|
|
|
|
expect(deleted).toBe(true);
|
|
expect(ruvector.vectorIndex.size()).toBe(0);
|
|
});
|
|
|
|
it('should clear all vectors', async () => {
|
|
await ruvector.batchIndex([
|
|
{ id: 'doc-1', text: 'Text 1' },
|
|
{ id: 'doc-2', text: 'Text 2' }
|
|
]);
|
|
|
|
ruvector.vectorIndex.clear();
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(0);
|
|
});
|
|
|
|
it('should handle search on empty index', () => {
|
|
const embedding = ruvector.embedder.embed('Query');
|
|
const results = ruvector.vectorIndex.search(embedding, 10);
|
|
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Routing', () => {
|
|
beforeEach(() => {
|
|
ruvector.router.addRoute('generate.*code', 'coder');
|
|
ruvector.router.addRoute('write.*test', 'tester');
|
|
ruvector.router.addRoute('review.*pull', 'reviewer');
|
|
});
|
|
|
|
it('should route to correct handler', () => {
|
|
const result = ruvector.router.route('generate some code for me');
|
|
|
|
expect(result.handler).toBe('coder');
|
|
expect(result.confidence).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('should fallback for unmatched queries', () => {
|
|
const result = ruvector.router.route('random unrelated request');
|
|
|
|
expect(result.handler).toBe('default');
|
|
expect(result.metadata.fallback).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('RuVector Performance', () => {
|
|
let ruvector: ReturnType<typeof createMockRuVectorBindings>;
|
|
|
|
beforeEach(() => {
|
|
ruvector = createMockRuVectorBindings();
|
|
});
|
|
|
|
describe('Large Scale Operations', () => {
|
|
it('should handle 1000 documents', async () => {
|
|
const documents = Array.from({ length: 1000 }, (_, i) => ({
|
|
id: `doc-${i}`,
|
|
text: `Document ${i} containing text about topic ${i % 10}`
|
|
}));
|
|
|
|
const startIndex = performance.now();
|
|
await ruvector.batchIndex(documents);
|
|
const indexTime = performance.now() - startIndex;
|
|
|
|
expect(ruvector.vectorIndex.size()).toBe(1000);
|
|
expect(indexTime).toBeLessThan(5000); // Should complete in <5 seconds
|
|
});
|
|
|
|
it('should search efficiently in large index', async () => {
|
|
// Pre-populate index
|
|
const documents = Array.from({ length: 500 }, (_, i) => ({
|
|
id: `doc-${i}`,
|
|
text: `Content about subject ${i} with details`
|
|
}));
|
|
await ruvector.batchIndex(documents);
|
|
|
|
const startSearch = performance.now();
|
|
const results = await ruvector.search('subject 250', 10);
|
|
const searchTime = performance.now() - startSearch;
|
|
|
|
expect(results).toHaveLength(10);
|
|
expect(searchTime).toBeLessThan(100); // Should complete in <100ms
|
|
});
|
|
});
|
|
|
|
describe('Memory Efficiency', () => {
|
|
it('should report memory usage', () => {
|
|
const memory = mockWasmLoader.getWasmMemory();
|
|
|
|
expect(memory.used).toBeDefined();
|
|
expect(memory.total).toBeDefined();
|
|
expect(memory.used).toBeLessThan(memory.total);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('RuVector Error Handling', () => {
|
|
let ruvector: ReturnType<typeof createMockRuVectorBindings>;
|
|
|
|
beforeEach(() => {
|
|
ruvector = createMockRuVectorBindings();
|
|
});
|
|
|
|
describe('Dimension Validation', () => {
|
|
it('should reject mismatched embedding dimensions', () => {
|
|
const wrongDimension = new Float32Array(256).fill(0.5);
|
|
|
|
expect(() => {
|
|
ruvector.vectorIndex.add('wrong', wrongDimension);
|
|
}).toThrow('dimension mismatch');
|
|
});
|
|
|
|
it('should reject mismatched query dimensions', async () => {
|
|
await ruvector.index('doc-1', 'Test document');
|
|
|
|
const wrongQuery = new Float32Array(256).fill(0.5);
|
|
|
|
expect(() => {
|
|
ruvector.vectorIndex.search(wrongQuery, 10);
|
|
}).toThrow('dimension mismatch');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('RuVector WASM Loader', () => {
|
|
it('should check WASM support', () => {
|
|
const supported = mockWasmLoader.isWasmSupported();
|
|
expect(typeof supported).toBe('boolean');
|
|
});
|
|
|
|
it('should load vector index', async () => {
|
|
const index = await mockWasmLoader.loadVectorIndex(768);
|
|
|
|
expect(index).toBeInstanceOf(MockWasmVectorIndex);
|
|
});
|
|
|
|
it('should load embedder', async () => {
|
|
const embedder = await mockWasmLoader.loadEmbedder(768);
|
|
|
|
expect(embedder).toBeInstanceOf(MockWasmEmbedder);
|
|
});
|
|
});
|