mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
3d7a2f91dd
Security (CodeQL #193-196): - command-validator: replace greedy .* with [^)]* in $() pattern - input-sanitizer: replace .* with \S+(?:\.\S+)* in UPDATE SET pattern - path-traversal-validator: split alternation into two .replace() calls - regex-safety-validator: use [^\]]* and non-greedy variants in detection patterns Flaky test indicators (5 files, 58 real setTimeout calls eliminated): - file-watcher.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - qcsd-refinement.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - qcsd-ideation-url.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - server-client.test.ts: process.nextTick for data callbacks - stream-controller.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync File size discipline (issue #374 P2): - time-crystal.ts (1,714 → 6 files, max 447 lines): time-crystal-types, phase-executor, time-crystal-persistence, time-crystal-analysis, time-crystal-scheduling, time-crystal (controller) - qe-reasoning-bank.ts (1,941 → 7 files, max 738 lines): qe-reasoning-bank-types, pretrained-patterns, agent-routing, embedding-utils, pattern-promotion, qe-reasoning-bank (class) All re-exports preserved for backward compatibility. 536 directly affected tests pass. Build clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
580 lines
19 KiB
TypeScript
580 lines
19 KiB
TypeScript
/**
|
|
* Agentic QE v3 - RuVector Server Client Unit Tests
|
|
*
|
|
* Tests for RuVectorServerClient lifecycle management and operations.
|
|
*
|
|
* Note: These tests mock the child_process and fetch to avoid
|
|
* actually starting the ruvector server during tests.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
|
|
import {
|
|
RuVectorServerClient,
|
|
createRuVectorServerClient,
|
|
createRuVectorServerClientSync,
|
|
getSharedServerClient,
|
|
resetSharedServerClient,
|
|
DEFAULT_SERVER_CONFIG,
|
|
type RuVectorServerConfig,
|
|
type ServerHealthResult,
|
|
type VectorSearchResult,
|
|
} from '../../../../src/integrations/ruvector/server-client';
|
|
import type { QESONAPattern, QEPatternType } from '../../../../src/integrations/ruvector/sona-wrapper';
|
|
import type { DomainName } from '../../../../src/integrations/rl-suite/interfaces';
|
|
|
|
// Mock child_process
|
|
vi.mock('child_process', () => ({
|
|
spawn: vi.fn(),
|
|
}));
|
|
|
|
// Mock fetch
|
|
const mockFetch = vi.fn();
|
|
global.fetch = mockFetch;
|
|
|
|
// Get mocked spawn
|
|
import { spawn } from 'child_process';
|
|
const mockedSpawn = spawn as Mock;
|
|
|
|
// ============================================================================
|
|
// Test Fixtures
|
|
// ============================================================================
|
|
|
|
function createTestConfig(overrides: Partial<RuVectorServerConfig> = {}): RuVectorServerConfig {
|
|
return {
|
|
httpPort: 18080, // Use non-standard port for tests
|
|
grpcPort: 50152,
|
|
dataDir: '/tmp/ruvector-test-data',
|
|
autoStart: false, // Don't auto-start by default in tests
|
|
cors: true,
|
|
startTimeout: 5000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createTestPattern(overrides: Partial<QESONAPattern> = {}): QESONAPattern {
|
|
return {
|
|
id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
type: 'test-generation' as QEPatternType,
|
|
domain: 'test-generation' as DomainName,
|
|
stateEmbedding: new Array(384).fill(0).map(() => Math.random()),
|
|
action: {
|
|
type: 'test-action',
|
|
value: 'execute-test',
|
|
},
|
|
outcome: {
|
|
reward: 0.8,
|
|
success: true,
|
|
quality: 0.9,
|
|
},
|
|
confidence: 0.85,
|
|
usageCount: 5,
|
|
createdAt: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createMockProcess(): {
|
|
stdout: { on: Mock };
|
|
stderr: { on: Mock };
|
|
on: Mock;
|
|
kill: Mock;
|
|
} {
|
|
return {
|
|
stdout: { on: vi.fn() },
|
|
stderr: { on: vi.fn() },
|
|
on: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('RuVectorServerClient', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockFetch.mockReset();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await resetSharedServerClient();
|
|
});
|
|
|
|
describe('Vector Operations Capability', () => {
|
|
it('should report vector operations as not supported (API coming soon)', () => {
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
expect(client.supportsVectorOperations()).toBe(false);
|
|
});
|
|
|
|
it('should provide unavailable reason when server not running', () => {
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
const reason = client.getVectorOperationsUnavailableReason();
|
|
expect(reason).toContain('not running');
|
|
});
|
|
|
|
it('should provide unavailable reason when API not available', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
await client.startServer();
|
|
|
|
const reason = client.getVectorOperationsUnavailableReason();
|
|
expect(reason).toContain('not yet available');
|
|
expect(reason).toContain('github.com/ruvnet/ruvector');
|
|
});
|
|
});
|
|
|
|
describe('Configuration', () => {
|
|
it('should use default configuration when none provided', () => {
|
|
const client = new RuVectorServerClient();
|
|
const config = client.getConfig();
|
|
|
|
expect(config.httpPort).toBe(DEFAULT_SERVER_CONFIG.httpPort);
|
|
expect(config.grpcPort).toBe(DEFAULT_SERVER_CONFIG.grpcPort);
|
|
expect(config.dataDir).toBe(DEFAULT_SERVER_CONFIG.dataDir);
|
|
expect(config.autoStart).toBe(DEFAULT_SERVER_CONFIG.autoStart);
|
|
expect(config.cors).toBe(DEFAULT_SERVER_CONFIG.cors);
|
|
expect(config.startTimeout).toBe(DEFAULT_SERVER_CONFIG.startTimeout);
|
|
});
|
|
|
|
it('should merge custom configuration with defaults', () => {
|
|
const config = createTestConfig({ httpPort: 9999, cors: false });
|
|
const client = new RuVectorServerClient(config);
|
|
const resultConfig = client.getConfig();
|
|
|
|
expect(resultConfig.httpPort).toBe(9999);
|
|
expect(resultConfig.cors).toBe(false);
|
|
// Other values should be from test config or defaults
|
|
expect(resultConfig.grpcPort).toBe(50152);
|
|
});
|
|
|
|
it('should return correct endpoints', () => {
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
|
|
expect(client.getHttpEndpoint()).toBe('http://localhost:18080');
|
|
expect(client.getGrpcEndpoint()).toBe('localhost:50152');
|
|
});
|
|
});
|
|
|
|
describe('Health Check', () => {
|
|
it('should return healthy status when server responds OK', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
const health = await client.healthCheck();
|
|
|
|
expect(health.healthy).toBe(true);
|
|
expect(health.status).toBe('running');
|
|
expect(health.httpEndpoint).toBe('http://localhost:18080');
|
|
expect(health.lastChecked).toBeInstanceOf(Date);
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'http://localhost:18080/health',
|
|
expect.objectContaining({ method: 'GET' })
|
|
);
|
|
});
|
|
|
|
it('should return unhealthy status when server returns error', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
const health = await client.healthCheck();
|
|
|
|
expect(health.healthy).toBe(false);
|
|
expect(health.status).toBe('error');
|
|
expect(health.error).toContain('500');
|
|
});
|
|
|
|
it('should return stopped status when fetch fails', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
const health = await client.healthCheck();
|
|
|
|
expect(health.healthy).toBe(false);
|
|
expect(health.status).toBe('stopped');
|
|
expect(health.error).toBe('Connection refused');
|
|
});
|
|
|
|
it('should cache last health check result', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
await client.healthCheck();
|
|
|
|
const cached = client.getLastHealthCheck();
|
|
expect(cached).not.toBeNull();
|
|
expect(cached?.healthy).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Server Lifecycle', () => {
|
|
it('should report not running initially', () => {
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
expect(client.isServerRunning()).toBe(false);
|
|
});
|
|
|
|
it('should start server with correct arguments', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
// Simulate server starting
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started and listening')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
|
|
// Don't await - just trigger start
|
|
const startPromise = client.startServer();
|
|
|
|
// Verify spawn was called with correct args
|
|
expect(mockedSpawn).toHaveBeenCalledWith(
|
|
'npx',
|
|
expect.arrayContaining([
|
|
'ruvector',
|
|
'server',
|
|
'--port', '18080',
|
|
'--grpc-port', '50152',
|
|
'--data-dir', '/tmp/ruvector-test-data',
|
|
'--cors',
|
|
]),
|
|
expect.any(Object)
|
|
);
|
|
|
|
await startPromise;
|
|
expect(client.isServerRunning()).toBe(true);
|
|
});
|
|
|
|
it('should not include --cors when cors is disabled', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig({ cors: false }));
|
|
await client.startServer();
|
|
|
|
const spawnArgs = mockedSpawn.mock.calls[0][1];
|
|
expect(spawnArgs).not.toContain('--cors');
|
|
});
|
|
|
|
it('should handle server start timeout', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
// Never emit 'started' message
|
|
mockProcess.stdout.on.mockImplementation(() => {});
|
|
mockProcess.stderr.on.mockImplementation(() => {});
|
|
mockProcess.on.mockImplementation(() => {});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig({ startTimeout: 100 }));
|
|
|
|
await expect(client.startServer()).rejects.toThrow(/failed to start within/i);
|
|
});
|
|
|
|
it('should handle spawn error', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.on.mockImplementation((event: string, callback: (err: Error) => void) => {
|
|
if (event === 'error') {
|
|
process.nextTick(() => callback(new Error('spawn ENOENT')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
|
|
await expect(client.startServer()).rejects.toThrow(/spawn ENOENT/i);
|
|
});
|
|
|
|
it('should stop server gracefully', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
// Start server
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
await client.startServer();
|
|
expect(client.isServerRunning()).toBe(true);
|
|
|
|
// Stop server
|
|
mockProcess.on.mockImplementation((event: string, callback: () => void) => {
|
|
if (event === 'exit') {
|
|
setTimeout(() => callback(), 10);
|
|
}
|
|
});
|
|
|
|
await client.stopServer();
|
|
expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
|
|
expect(client.isServerRunning()).toBe(false);
|
|
});
|
|
|
|
it('should skip start if already running', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
await client.startServer();
|
|
|
|
// Try to start again
|
|
await client.startServer();
|
|
|
|
// Should only have spawned once
|
|
expect(mockedSpawn).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('ensureServerRunning', () => {
|
|
it('should start server if health check fails and autoStart is true', async () => {
|
|
// First health check fails
|
|
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig({ autoStart: true }));
|
|
await client.ensureServerRunning();
|
|
|
|
expect(mockedSpawn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw if server not running and autoStart is false', async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
|
|
const client = new RuVectorServerClient(createTestConfig({ autoStart: false }));
|
|
|
|
await expect(client.ensureServerRunning()).rejects.toThrow(/autoStart is disabled/);
|
|
});
|
|
|
|
it('should not start server if already healthy', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
|
|
const client = new RuVectorServerClient(createTestConfig({ autoStart: true }));
|
|
await client.ensureServerRunning();
|
|
|
|
expect(mockedSpawn).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Vector Operations (Stubs)', () => {
|
|
let client: RuVectorServerClient;
|
|
|
|
beforeEach(() => {
|
|
// Mock healthy server for all vector operations
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200 });
|
|
client = new RuVectorServerClient(createTestConfig());
|
|
});
|
|
|
|
it('should log stub message for storeVector', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
await client.storeVector('test-namespace', 'vec-1', [1.0, 2.0, 3.0], { key: 'value' });
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('STUB: storeVector'),
|
|
expect.any(String)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should return empty array for searchSimilar stub', async () => {
|
|
const results = await client.searchSimilar('test-namespace', [1.0, 2.0, 3.0], 5);
|
|
|
|
expect(results).toEqual([]);
|
|
});
|
|
|
|
it('should log stub message for deleteVector', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
await client.deleteVector('test-namespace', 'vec-1');
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('STUB: deleteVector')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should return mocked stats', async () => {
|
|
const stats = await client.getServerStats();
|
|
|
|
expect(stats.isMocked).toBe(true);
|
|
expect(stats.totalVectors).toBe(0);
|
|
expect(stats.vectorsByNamespace).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('Pattern Sharing', () => {
|
|
let client: RuVectorServerClient;
|
|
|
|
beforeEach(() => {
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200 });
|
|
client = new RuVectorServerClient(createTestConfig());
|
|
});
|
|
|
|
it('should share pattern by storing its embedding (stubbed)', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
const pattern = createTestPattern();
|
|
|
|
const result = await client.sharePattern(pattern);
|
|
|
|
// Should return false because vector operations are not supported
|
|
expect(result).toBe(false);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('STUB: storeVector'),
|
|
expect.any(String)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should skip sharing pattern without embedding', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const pattern = createTestPattern({ stateEmbedding: [] });
|
|
|
|
const result = await client.sharePattern(pattern);
|
|
|
|
expect(result).toBe(false);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('no embedding')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should find similar patterns (returns empty for stub)', async () => {
|
|
const pattern = createTestPattern();
|
|
const results = await client.findSimilarPatterns(pattern, 5);
|
|
|
|
expect(results).toEqual([]);
|
|
});
|
|
|
|
it('should return empty for patterns without embedding', async () => {
|
|
const pattern = createTestPattern({ stateEmbedding: [] });
|
|
const results = await client.findSimilarPatterns(pattern);
|
|
|
|
expect(results).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Factory Functions', () => {
|
|
it('should create client with async factory', async () => {
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200 });
|
|
|
|
const client = await createRuVectorServerClient(createTestConfig());
|
|
|
|
expect(client).toBeInstanceOf(RuVectorServerClient);
|
|
});
|
|
|
|
it('should handle server start failure gracefully in async factory', async () => {
|
|
mockFetch.mockRejectedValue(new Error('Connection refused'));
|
|
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
// Should not throw, just warn
|
|
const client = await createRuVectorServerClient(createTestConfig({ autoStart: true }));
|
|
|
|
expect(client).toBeInstanceOf(RuVectorServerClient);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to start server'),
|
|
expect.any(String)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should create client with sync factory', () => {
|
|
const client = createRuVectorServerClientSync(createTestConfig());
|
|
|
|
expect(client).toBeInstanceOf(RuVectorServerClient);
|
|
expect(client.isServerRunning()).toBe(false);
|
|
});
|
|
|
|
it('should return same instance for shared client', async () => {
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200 });
|
|
|
|
const client1 = await getSharedServerClient(createTestConfig());
|
|
const client2 = await getSharedServerClient(createTestConfig());
|
|
|
|
expect(client1).toBe(client2);
|
|
});
|
|
|
|
it('should reset shared client', async () => {
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200 });
|
|
|
|
const client1 = await getSharedServerClient(createTestConfig());
|
|
await resetSharedServerClient();
|
|
const client2 = await getSharedServerClient(createTestConfig());
|
|
|
|
expect(client1).not.toBe(client2);
|
|
});
|
|
});
|
|
|
|
describe('Dispose', () => {
|
|
it('should stop server on dispose', async () => {
|
|
const mockProcess = createMockProcess();
|
|
mockedSpawn.mockReturnValueOnce(mockProcess);
|
|
|
|
mockProcess.stdout.on.mockImplementation((event: string, callback: (data: Buffer) => void) => {
|
|
if (event === 'data') {
|
|
process.nextTick(() => callback(Buffer.from('Server started')));
|
|
}
|
|
});
|
|
|
|
mockProcess.on.mockImplementation((event: string, callback: () => void) => {
|
|
if (event === 'exit') {
|
|
setTimeout(() => callback(), 10);
|
|
}
|
|
});
|
|
|
|
const client = new RuVectorServerClient(createTestConfig());
|
|
await client.startServer();
|
|
|
|
await client.dispose();
|
|
|
|
expect(mockProcess.kill).toHaveBeenCalled();
|
|
expect(client.isServerRunning()).toBe(false);
|
|
});
|
|
});
|
|
});
|