mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
b91627f282
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>
646 lines
19 KiB
TypeScript
646 lines
19 KiB
TypeScript
/**
|
|
* Integration Tests for Code Intelligence + RuVector
|
|
*
|
|
* Tests the complete integration of:
|
|
* - CodeChunkStore with RuVector PostgreSQL
|
|
* - CodeIntelligenceHybridRouter for embeddings
|
|
* - CodeIntelligenceOrchestrator with database persistence
|
|
*
|
|
* Prerequisites:
|
|
* - RuVector PostgreSQL container running
|
|
* - Ollama with nomic-embed-text model
|
|
*
|
|
* Run with: npm run test:integration -- --grep "RuVector"
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
|
import { Pool } from 'pg';
|
|
import { createSeededRandom } from '../../../src/utils/SeededRandom';
|
|
import {
|
|
CodeChunkStore,
|
|
createDockerCodeChunkStore,
|
|
} from '../../../src/code-intelligence/storage/CodeChunkStore.js';
|
|
import {
|
|
CodeIntelligenceHybridRouter,
|
|
createDockerCodeIntelligenceRouter,
|
|
} from '../../../src/code-intelligence/router/CodeIntelligenceHybridRouter.js';
|
|
import { VectorSearch } from '../../../src/code-intelligence/search/VectorSearch.js';
|
|
|
|
// Skip integration tests only if explicitly requested
|
|
const SKIP_INTEGRATION = process.env.SKIP_INTEGRATION_TESTS === 'true';
|
|
const RUVECTOR_HOST = process.env.RUVECTOR_HOST ?? 'localhost';
|
|
const RUVECTOR_PORT = parseInt(process.env.RUVECTOR_PORT ?? '5432');
|
|
|
|
// Jest equivalent of describe.skipIf
|
|
const describeIntegration = SKIP_INTEGRATION ? describe.skip : describe;
|
|
|
|
describeIntegration('RuVector Integration', () => {
|
|
let pool: Pool;
|
|
let chunkStore: CodeChunkStore;
|
|
|
|
beforeAll(async () => {
|
|
// Connect to RuVector PostgreSQL
|
|
pool = new Pool({
|
|
host: RUVECTOR_HOST,
|
|
port: RUVECTOR_PORT,
|
|
database: process.env.RUVECTOR_DATABASE ?? 'ruvector',
|
|
user: process.env.RUVECTOR_USER ?? 'ruvector',
|
|
password: process.env.RUVECTOR_PASSWORD ?? 'ruvector',
|
|
});
|
|
|
|
// Verify connection
|
|
try {
|
|
await pool.query('SELECT 1');
|
|
} catch (error) {
|
|
console.warn('RuVector not available, skipping integration tests');
|
|
throw error;
|
|
}
|
|
|
|
// Create CodeChunkStore
|
|
chunkStore = new CodeChunkStore({
|
|
host: RUVECTOR_HOST,
|
|
port: RUVECTOR_PORT,
|
|
database: process.env.RUVECTOR_DATABASE ?? 'ruvector',
|
|
user: process.env.RUVECTOR_USER ?? 'ruvector',
|
|
password: process.env.RUVECTOR_PASSWORD ?? 'ruvector',
|
|
embeddingDimension: 768,
|
|
});
|
|
|
|
await chunkStore.initialize();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (chunkStore) {
|
|
await chunkStore.close();
|
|
}
|
|
if (pool) {
|
|
await pool.end();
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clear data between tests
|
|
await chunkStore.clear();
|
|
});
|
|
|
|
describe('CodeChunkStore', () => {
|
|
it('should store and retrieve a code chunk', async () => {
|
|
// Generate a mock embedding (768 dimensions)
|
|
const rng = createSeededRandom(13000);
|
|
const embedding = Array.from({ length: 768 }, () => rng.random() - 0.5);
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'test-chunk-1',
|
|
filePath: '/src/test.ts',
|
|
content: 'function hello() { return "world"; }',
|
|
embedding,
|
|
chunkType: 'function',
|
|
name: 'hello',
|
|
startLine: 1,
|
|
endLine: 3,
|
|
});
|
|
|
|
// Verify it was stored
|
|
const health = await chunkStore.healthCheck();
|
|
expect(health.healthy).toBe(true);
|
|
expect(health.chunkCount).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should search for similar code chunks', async () => {
|
|
// Store multiple chunks with different embeddings
|
|
const chunks = [
|
|
{
|
|
id: 'auth-1',
|
|
filePath: '/src/auth.ts',
|
|
content: 'function authenticate(user: string, password: string) {}',
|
|
chunkType: 'function',
|
|
name: 'authenticate',
|
|
},
|
|
{
|
|
id: 'utils-1',
|
|
filePath: '/src/utils.ts',
|
|
content: 'function formatDate(date: Date) { return date.toISOString(); }',
|
|
chunkType: 'function',
|
|
name: 'formatDate',
|
|
},
|
|
{
|
|
id: 'db-1',
|
|
filePath: '/src/db.ts',
|
|
content: 'async function queryDatabase(sql: string) { return []; }',
|
|
chunkType: 'function',
|
|
name: 'queryDatabase',
|
|
},
|
|
];
|
|
|
|
// Store with mock embeddings (in real scenario, these would be from Ollama)
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const embedding = Array.from({ length: 768 }, (_, j) =>
|
|
// Create somewhat different embeddings for each chunk
|
|
Math.sin(i * 0.1 + j * 0.01)
|
|
);
|
|
|
|
await chunkStore.storeChunk({
|
|
...chunks[i],
|
|
embedding,
|
|
startLine: 1,
|
|
endLine: 5,
|
|
});
|
|
}
|
|
|
|
// Search with query embedding similar to first chunk (auth.ts at i=0)
|
|
// auth.ts uses Math.sin(0 * 0.1 + j * 0.01) = Math.sin(j * 0.01)
|
|
const queryEmbedding = Array.from({ length: 768 }, (_, j) =>
|
|
Math.sin(0 * 0.1 + j * 0.01)
|
|
);
|
|
|
|
const results = await chunkStore.search(queryEmbedding, { topK: 3 });
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// First result should be most similar to auth chunk
|
|
expect(results[0].filePath).toBe('/src/auth.ts');
|
|
});
|
|
|
|
it('should filter by entity type', async () => {
|
|
// Store a function and a class
|
|
const rng = createSeededRandom(13100);
|
|
const functionEmbedding = Array.from({ length: 768 }, () => rng.random());
|
|
const classEmbedding = Array.from({ length: 768 }, () => rng.random());
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'func-1',
|
|
filePath: '/src/service.ts',
|
|
content: 'function process() {}',
|
|
embedding: functionEmbedding,
|
|
chunkType: 'function',
|
|
name: 'process',
|
|
startLine: 1,
|
|
endLine: 1,
|
|
});
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'class-1',
|
|
filePath: '/src/service.ts',
|
|
content: 'class Service {}',
|
|
embedding: classEmbedding,
|
|
chunkType: 'class',
|
|
name: 'Service',
|
|
startLine: 5,
|
|
endLine: 10,
|
|
});
|
|
|
|
// Search for only classes
|
|
const results = await chunkStore.search(classEmbedding, {
|
|
topK: 10,
|
|
entityType: 'class',
|
|
});
|
|
|
|
expect(results.every((r) => r.chunkType === 'class')).toBe(true);
|
|
});
|
|
|
|
it('should perform hybrid search (vector + keyword)', async () => {
|
|
const rng = createSeededRandom(13200);
|
|
const embedding = Array.from({ length: 768 }, () => rng.random() - 0.5);
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'test-1',
|
|
filePath: '/src/auth.ts',
|
|
content: 'function validateUser(username: string) { return true; }',
|
|
embedding,
|
|
chunkType: 'function',
|
|
name: 'validateUser',
|
|
startLine: 1,
|
|
endLine: 3,
|
|
});
|
|
|
|
// Hybrid search with keyword "validateUser"
|
|
const results = await chunkStore.hybridSearch(
|
|
embedding,
|
|
'validateUser',
|
|
{ topK: 10, semanticWeight: 0.5 }
|
|
);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].content).toContain('validateUser');
|
|
});
|
|
|
|
it('should store and query entities', async () => {
|
|
await chunkStore.storeEntity({
|
|
id: 'entity-1',
|
|
name: 'UserService',
|
|
entityType: 'class',
|
|
filePath: '/src/services/UserService.ts',
|
|
startLine: 1,
|
|
endLine: 100,
|
|
signature: 'class UserService',
|
|
});
|
|
|
|
// Verify entity exists
|
|
const stats = await chunkStore.getStats();
|
|
expect(stats.entityCount).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should store relationships', async () => {
|
|
// Store two entities first
|
|
await chunkStore.storeEntity({
|
|
id: 'class-user',
|
|
name: 'User',
|
|
entityType: 'class',
|
|
filePath: '/src/models/User.ts',
|
|
startLine: 1,
|
|
endLine: 50,
|
|
});
|
|
|
|
await chunkStore.storeEntity({
|
|
id: 'class-admin',
|
|
name: 'Admin',
|
|
entityType: 'class',
|
|
filePath: '/src/models/Admin.ts',
|
|
startLine: 1,
|
|
endLine: 30,
|
|
});
|
|
|
|
// Store relationship
|
|
await chunkStore.storeRelationship({
|
|
sourceId: 'class-admin',
|
|
targetId: 'class-user',
|
|
relationshipType: 'extends',
|
|
metadata: { inherited: true },
|
|
});
|
|
|
|
const stats = await chunkStore.getStats();
|
|
expect(stats.relationshipCount).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should delete chunks by file path', async () => {
|
|
const rng = createSeededRandom(13300);
|
|
const embedding = Array.from({ length: 768 }, () => rng.random());
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'delete-1',
|
|
filePath: '/src/to-delete.ts',
|
|
content: 'const x = 1;',
|
|
embedding,
|
|
startLine: 1,
|
|
endLine: 1,
|
|
});
|
|
|
|
await chunkStore.storeChunk({
|
|
id: 'keep-1',
|
|
filePath: '/src/keep.ts',
|
|
content: 'const y = 2;',
|
|
embedding,
|
|
startLine: 1,
|
|
endLine: 1,
|
|
});
|
|
|
|
const deleted = await chunkStore.deleteByFilePath('/src/to-delete.ts');
|
|
expect(deleted).toBe(1);
|
|
|
|
const health = await chunkStore.healthCheck();
|
|
expect(health.chunkCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('VectorSearch with CodeChunkStore', () => {
|
|
it('should use database backend when configured', async () => {
|
|
const vectorSearch = new VectorSearch({
|
|
dimensions: 768,
|
|
metric: 'cosine',
|
|
database: {
|
|
host: RUVECTOR_HOST,
|
|
port: RUVECTOR_PORT,
|
|
database: process.env.RUVECTOR_DATABASE ?? 'ruvector',
|
|
user: process.env.RUVECTOR_USER ?? 'ruvector',
|
|
password: process.env.RUVECTOR_PASSWORD ?? 'ruvector',
|
|
embeddingDimension: 768,
|
|
},
|
|
});
|
|
|
|
await vectorSearch.initialize();
|
|
|
|
expect(vectorSearch.isUsingDatabase()).toBe(true);
|
|
|
|
// Add a document
|
|
const rng = createSeededRandom(13400);
|
|
const embedding = Array.from({ length: 768 }, () => rng.random() - 0.5);
|
|
|
|
await vectorSearch.addDocument({
|
|
id: 'vs-test-1',
|
|
embedding,
|
|
metadata: {
|
|
filePath: '/src/test.ts',
|
|
content: 'test content',
|
|
startLine: 1,
|
|
endLine: 5,
|
|
entityType: 'function',
|
|
entityName: 'test',
|
|
},
|
|
});
|
|
|
|
// Search
|
|
const results = await vectorSearch.search(embedding, 5);
|
|
expect(results.length).toBeGreaterThan(0);
|
|
|
|
await vectorSearch.close();
|
|
});
|
|
|
|
it('should fall back to in-memory when database not configured', async () => {
|
|
const vectorSearch = new VectorSearch({
|
|
dimensions: 768,
|
|
metric: 'cosine',
|
|
// No database config
|
|
});
|
|
|
|
await vectorSearch.initialize();
|
|
|
|
expect(vectorSearch.isUsingDatabase()).toBe(false);
|
|
|
|
const rng = createSeededRandom(13500);
|
|
const embedding = Array.from({ length: 768 }, () => rng.random() - 0.5);
|
|
|
|
await vectorSearch.addDocument({
|
|
id: 'mem-test-1',
|
|
embedding,
|
|
metadata: {
|
|
filePath: '/src/test.ts',
|
|
content: 'in-memory content',
|
|
startLine: 1,
|
|
endLine: 5,
|
|
},
|
|
});
|
|
|
|
// Should use in-memory search
|
|
const results = await vectorSearch.search(embedding, 5);
|
|
expect(results.length).toBeGreaterThan(0);
|
|
|
|
await vectorSearch.close();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Check if Ollama is available (requires running Ollama server)
|
|
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
|
|
const SKIP_OLLAMA_TESTS = process.env.ENABLE_OLLAMA_TESTS !== 'true';
|
|
|
|
// Skip router tests that require Ollama unless explicitly enabled
|
|
const describeOllamaIntegration = SKIP_OLLAMA_TESTS ? describe.skip : describeIntegration;
|
|
|
|
describeOllamaIntegration('CodeIntelligenceHybridRouter Integration', () => {
|
|
let router: CodeIntelligenceHybridRouter;
|
|
|
|
beforeAll(async () => {
|
|
// Only run if both RuVector and Ollama are available
|
|
router = createDockerCodeIntelligenceRouter();
|
|
|
|
try {
|
|
await router.initialize();
|
|
} catch (error) {
|
|
console.warn('Router initialization failed:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (router) {
|
|
await router.shutdown();
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
if (router) {
|
|
await router.clearStorage();
|
|
}
|
|
});
|
|
|
|
it('should embed and store code chunks', async () => {
|
|
const chunk = {
|
|
id: 'router-test-1',
|
|
filePath: '/src/router-test.ts',
|
|
content: 'export function processRequest(req: Request) { return req.body; }',
|
|
startLine: 1,
|
|
endLine: 3,
|
|
language: 'typescript',
|
|
entityType: 'function',
|
|
entityName: 'processRequest',
|
|
};
|
|
|
|
const embedded = await router.embedAndStoreChunk(chunk);
|
|
|
|
expect(embedded.embedding).toBeDefined();
|
|
expect(embedded.embedding.length).toBe(768);
|
|
expect(embedded.chunk.id).toBe(chunk.id);
|
|
});
|
|
|
|
it('should search for code using semantic similarity', async () => {
|
|
// Store some test chunks
|
|
const chunks = [
|
|
{
|
|
id: 'search-1',
|
|
filePath: '/src/auth.ts',
|
|
content: 'function login(username: string, password: string) { /* auth logic */ }',
|
|
startLine: 1,
|
|
endLine: 5,
|
|
language: 'typescript',
|
|
entityType: 'function',
|
|
entityName: 'login',
|
|
},
|
|
{
|
|
id: 'search-2',
|
|
filePath: '/src/api.ts',
|
|
content: 'function fetchData(url: string) { return fetch(url); }',
|
|
startLine: 1,
|
|
endLine: 3,
|
|
language: 'typescript',
|
|
entityType: 'function',
|
|
entityName: 'fetchData',
|
|
},
|
|
];
|
|
|
|
await router.embedAndStoreChunks(chunks);
|
|
|
|
// Search for authentication-related code
|
|
const results = await router.searchCode('user authentication login', {
|
|
topK: 5,
|
|
});
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// The auth function should rank higher
|
|
const authResult = results.find((r) => r.filePath === '/src/auth.ts');
|
|
expect(authResult).toBeDefined();
|
|
});
|
|
|
|
it('should perform hybrid search with keyword boosting', async () => {
|
|
const chunks = [
|
|
{
|
|
id: 'hybrid-1',
|
|
filePath: '/src/validate.ts',
|
|
content: 'function validateEmail(email: string): boolean { return /^[^@]+@[^@]+$/.test(email); }',
|
|
startLine: 1,
|
|
endLine: 3,
|
|
language: 'typescript',
|
|
entityType: 'function',
|
|
entityName: 'validateEmail',
|
|
},
|
|
];
|
|
|
|
await router.embedAndStoreChunks(chunks);
|
|
|
|
// Hybrid search with keyword
|
|
const results = await router.hybridSearchCode('validateEmail', {
|
|
topK: 5,
|
|
keywordWeight: 0.7,
|
|
vectorWeight: 0.3,
|
|
});
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].content).toContain('validateEmail');
|
|
});
|
|
|
|
it('should return router statistics', async () => {
|
|
const stats = await router.getCodeRouterStats();
|
|
|
|
expect(stats.embeddingDimension).toBe(768);
|
|
expect(stats.routerHealthy).toBe(true);
|
|
expect(typeof stats.chunkCount).toBe('number');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* End-to-end test with real data
|
|
*/
|
|
describeIntegration('End-to-End RuVector Integration', () => {
|
|
it('should index and search a real TypeScript file', async () => {
|
|
const chunkStore = new CodeChunkStore({
|
|
host: RUVECTOR_HOST,
|
|
port: RUVECTOR_PORT,
|
|
database: process.env.RUVECTOR_DATABASE ?? 'ruvector',
|
|
user: process.env.RUVECTOR_USER ?? 'ruvector',
|
|
password: process.env.RUVECTOR_PASSWORD ?? 'ruvector',
|
|
embeddingDimension: 768,
|
|
});
|
|
|
|
await chunkStore.initialize();
|
|
await chunkStore.clear();
|
|
|
|
// Simulate indexing a real TypeScript file
|
|
const realCode = `
|
|
/**
|
|
* User authentication service
|
|
*/
|
|
export class AuthService {
|
|
private tokenStore: Map<string, string> = new Map();
|
|
|
|
async login(username: string, password: string): Promise<string> {
|
|
// Validate credentials
|
|
const isValid = await this.validateCredentials(username, password);
|
|
if (!isValid) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
// Generate token
|
|
const token = this.generateToken(username);
|
|
this.tokenStore.set(username, token);
|
|
return token;
|
|
}
|
|
|
|
async logout(username: string): Promise<void> {
|
|
this.tokenStore.delete(username);
|
|
}
|
|
|
|
private async validateCredentials(username: string, password: string): Promise<boolean> {
|
|
// Check against database
|
|
return username.length > 0 && password.length >= 8;
|
|
}
|
|
|
|
private generateToken(username: string): string {
|
|
return Buffer.from(\`\${username}:\${Date.now()}\`).toString('base64');
|
|
}
|
|
}
|
|
`;
|
|
|
|
// Create chunks for different parts
|
|
const chunks = [
|
|
{
|
|
id: 'e2e-class-1',
|
|
filePath: '/src/services/AuthService.ts',
|
|
content: realCode,
|
|
chunkType: 'class',
|
|
name: 'AuthService',
|
|
startLine: 4,
|
|
endLine: 32,
|
|
},
|
|
{
|
|
id: 'e2e-method-1',
|
|
filePath: '/src/services/AuthService.ts',
|
|
content: `async login(username: string, password: string): Promise<string> {
|
|
const isValid = await this.validateCredentials(username, password);
|
|
if (!isValid) throw new Error('Invalid credentials');
|
|
const token = this.generateToken(username);
|
|
this.tokenStore.set(username, token);
|
|
return token;
|
|
}`,
|
|
chunkType: 'method',
|
|
name: 'login',
|
|
startLine: 8,
|
|
endLine: 17,
|
|
},
|
|
{
|
|
id: 'e2e-method-2',
|
|
filePath: '/src/services/AuthService.ts',
|
|
content: `async logout(username: string): Promise<void> {
|
|
this.tokenStore.delete(username);
|
|
}`,
|
|
chunkType: 'method',
|
|
name: 'logout',
|
|
startLine: 19,
|
|
endLine: 21,
|
|
},
|
|
];
|
|
|
|
// Generate mock embeddings (in production, use Ollama nomic-embed-text)
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const embedding = Array.from({ length: 768 }, (_, j) =>
|
|
Math.sin((i + 1) * 0.1 + j * 0.01) * 0.5
|
|
);
|
|
|
|
await chunkStore.storeChunk({
|
|
...chunks[i],
|
|
embedding,
|
|
startLine: chunks[i].startLine,
|
|
endLine: chunks[i].endLine,
|
|
});
|
|
}
|
|
|
|
// Store entity
|
|
await chunkStore.storeEntity({
|
|
id: 'e2e-entity-1',
|
|
name: 'AuthService',
|
|
entityType: 'class',
|
|
filePath: '/src/services/AuthService.ts',
|
|
startLine: 4,
|
|
endLine: 32,
|
|
signature: 'export class AuthService',
|
|
});
|
|
|
|
// Verify data stored
|
|
const stats = await chunkStore.getStats();
|
|
expect(stats.chunkCount).toBe(3);
|
|
expect(stats.entityCount).toBe(1);
|
|
|
|
// Search for "user login authentication"
|
|
const queryEmbedding = Array.from({ length: 768 }, (_, j) =>
|
|
Math.sin(0.15 + j * 0.01) * 0.5
|
|
);
|
|
|
|
const results = await chunkStore.search(queryEmbedding, { topK: 5 });
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results.some((r) => r.name === 'login')).toBe(true);
|
|
|
|
// Health check
|
|
const health = await chunkStore.healthCheck();
|
|
expect(health.healthy).toBe(true);
|
|
|
|
await chunkStore.close();
|
|
});
|
|
});
|