Files
Profa b91627f282 fix(quality): implement GOAP Issue #149 code quality remediation
Phase 1: Test Determinism
- Create SeededRandom utility with Mulberry32 PRNG (src/utils/SeededRandom.ts)
- Migrate 258+ Math.random() calls to SeededRandom across 65+ test files
- Add timer test utilities (tests/helpers/timerTestUtils.ts)
- Add jest.useFakeTimers() pattern to 13 test files

Phase 2: Type Safety (63.5% reduction)
- Reduce `any` types from ~1,800 to 656
- Add 50+ new interfaces across agents, core, mcp, cli modules
- Fix all TypeScript errors (0 errors, build passes)
- Replace Record<string, any> with Record<string, unknown>
- Add proper type guards for hook methods (PostTaskData, TaskErrorData)

Key improvements:
- agents/: 105 → 0 any types (100% reduction)
- mcp/: 226 → 102 any types (55% reduction)
- cli/: 277 → 168 any types (39% reduction)
- core/: 199 → 175 any types (12% reduction)

Closes #149

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 17:08:12 +00:00

305 lines
8.8 KiB
TypeScript

/**
* Unit Tests for VectorSearch
*
* Tests in-memory vector search and pgvector integration.
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { VectorSearch, VectorDocument } from '../../../src/code-intelligence/search/VectorSearch.js';
import { createSeededRandom } from '../../../src/utils/SeededRandom';
describe('VectorSearch', () => {
let search: VectorSearch;
const rng = createSeededRandom(20000);
// Helper to create test documents with embeddings
function createDoc(
id: string,
embedding: number[],
content: string = 'test content'
): VectorDocument {
return {
id,
embedding,
metadata: {
filePath: `/src/${id}.ts`,
content,
startLine: 1,
endLine: 10,
entityType: 'function',
entityName: id,
},
};
}
// Helper to create random embedding
function randomEmbedding(dims: number = 768): number[] {
return Array.from({ length: dims }, () => rng.random() - 0.5);
}
// Helper to create normalized embedding
function normalizeEmbedding(embedding: number[]): number[] {
const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
return embedding.map((v) => v / norm);
}
beforeEach(() => {
search = new VectorSearch({ dimensions: 768 });
});
describe('document management', () => {
it('should add a document', async () => {
const doc = createDoc('doc1', randomEmbedding());
await search.addDocument(doc);
expect(search.getDocumentCount()).toBe(1);
expect(search.getDocumentIds()).toContain('doc1');
});
it('should add multiple documents', async () => {
const docs = [
createDoc('doc1', randomEmbedding()),
createDoc('doc2', randomEmbedding()),
createDoc('doc3', randomEmbedding()),
];
await search.addDocuments(docs);
expect(search.getDocumentCount()).toBe(3);
});
it('should reject wrong dimension embeddings', async () => {
const doc = createDoc('doc1', randomEmbedding(100)); // Wrong dims
await expect(search.addDocument(doc)).rejects.toThrow('Expected 768 dimensions');
});
it('should remove document', async () => {
const doc = createDoc('doc1', randomEmbedding());
await search.addDocument(doc);
const removed = await search.removeDocument('doc1');
expect(removed).toBe(true);
expect(search.getDocumentCount()).toBe(0);
});
it('should get document by ID', async () => {
const doc = createDoc('doc1', randomEmbedding());
await search.addDocument(doc);
const retrieved = await search.getDocument('doc1');
expect(retrieved).toBeDefined();
expect(retrieved?.id).toBe('doc1');
expect(retrieved?.metadata.content).toBe('test content');
});
it('should clear all documents', async () => {
await search.addDocuments([
createDoc('doc1', randomEmbedding()),
createDoc('doc2', randomEmbedding()),
]);
await search.clear();
expect(search.getDocumentCount()).toBe(0);
});
});
describe('vector search', () => {
it('should find similar documents', async () => {
// Create documents with specific embeddings
const baseEmbedding = normalizeEmbedding(randomEmbedding());
// Create similar embedding (small perturbation)
const similarEmbedding = normalizeEmbedding(
baseEmbedding.map((v) => v + (rng.random() - 0.5) * 0.1)
);
// Create dissimilar embedding
const dissimilarEmbedding = normalizeEmbedding(
baseEmbedding.map((v) => -v)
);
await search.addDocuments([
createDoc('similar', similarEmbedding, 'similar content'),
createDoc('dissimilar', dissimilarEmbedding, 'dissimilar content'),
]);
const results = await search.search(baseEmbedding, 2);
expect(results.length).toBe(2);
// Similar should rank higher
expect(results[0].id).toBe('similar');
expect(results[0].score).toBeGreaterThan(results[1].score);
});
it('should respect topK limit', async () => {
// Add many documents
const docs = Array.from({ length: 20 }, (_, i) =>
createDoc(`doc${i}`, randomEmbedding())
);
await search.addDocuments(docs);
const queryEmbedding = randomEmbedding();
const results = await search.search(queryEmbedding, 5);
expect(results.length).toBe(5);
});
it('should return scores in descending order', async () => {
await search.addDocuments([
createDoc('doc1', randomEmbedding()),
createDoc('doc2', randomEmbedding()),
createDoc('doc3', randomEmbedding()),
]);
const results = await search.search(randomEmbedding(), 3);
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
}
});
it('should include metadata in results', async () => {
await search.addDocument(
createDoc('doc1', randomEmbedding())
);
const results = await search.search(randomEmbedding(), 1);
expect(results[0].filePath).toBe('/src/doc1.ts');
expect(results[0].entityType).toBe('function');
expect(results[0].entityName).toBe('doc1');
});
it('should handle empty search', async () => {
const results = await search.search(randomEmbedding(), 10);
expect(results).toEqual([]);
});
it('should filter by entity type', async () => {
const doc1 = createDoc('func', randomEmbedding());
doc1.metadata.entityType = 'function';
const doc2 = createDoc('class', randomEmbedding());
doc2.metadata.entityType = 'class';
await search.addDocuments([doc1, doc2]);
const results = await search.search(randomEmbedding(), 10, {
entityType: 'function',
});
expect(results.length).toBe(1);
expect(results[0].entityType).toBe('function');
});
it('should filter by file pattern', async () => {
const doc1 = createDoc('services', randomEmbedding());
doc1.metadata.filePath = '/src/services/auth.ts';
const doc2 = createDoc('utils', randomEmbedding());
doc2.metadata.filePath = '/src/utils/helper.ts';
await search.addDocuments([doc1, doc2]);
const results = await search.search(randomEmbedding(), 10, {
filePattern: 'services',
});
expect(results.length).toBe(1);
expect(results[0].filePath).toContain('services');
});
});
describe('similarity metrics', () => {
it('should use cosine similarity by default', () => {
const config = search.getConfig();
expect(config.metric).toBe('cosine');
});
it('should support euclidean distance', async () => {
const euclideanSearch = new VectorSearch({
dimensions: 768,
metric: 'euclidean',
});
await euclideanSearch.addDocument(createDoc('doc1', randomEmbedding()));
const results = await euclideanSearch.search(randomEmbedding(), 1);
expect(results.length).toBe(1);
});
it('should support dot product', async () => {
const dotSearch = new VectorSearch({
dimensions: 768,
metric: 'dot',
});
await dotSearch.addDocument(createDoc('doc1', randomEmbedding()));
const results = await dotSearch.search(randomEmbedding(), 1);
expect(results.length).toBe(1);
});
});
describe('configuration', () => {
it('should use default config', () => {
const config = search.getConfig();
expect(config.dimensions).toBe(768);
expect(config.metric).toBe('cosine');
expect(config.indexType).toBe('hnsw');
});
it('should allow custom dimensions', () => {
const customSearch = new VectorSearch({ dimensions: 384 });
expect(customSearch.getConfig().dimensions).toBe(384);
});
});
describe('cosine similarity edge cases', () => {
it('should return 1 for identical vectors', async () => {
const embedding = normalizeEmbedding(randomEmbedding());
await search.addDocument(createDoc('doc1', embedding));
const results = await search.search(embedding, 1);
expect(results[0].score).toBeCloseTo(1, 5);
});
it('should return close to 0 for orthogonal vectors', async () => {
// Create two orthogonal unit vectors
const e1 = new Array(768).fill(0);
e1[0] = 1;
const e2 = new Array(768).fill(0);
e2[1] = 1;
await search.addDocument(createDoc('doc1', e1));
const results = await search.search(e2, 1);
expect(results[0].score).toBeCloseTo(0, 5);
});
it('should return -1 for opposite vectors', async () => {
const embedding = normalizeEmbedding(randomEmbedding());
const opposite = embedding.map((v) => -v);
await search.addDocument(createDoc('doc1', opposite));
const results = await search.search(embedding, 1);
expect(results[0].score).toBeCloseTo(-1, 5);
});
});
});