UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

794 lines (669 loc) 20.6 kB
/** * @claude-flow/testing - Mock Services * * Comprehensive mock implementations of V3 core services. * Provides realistic behavior for testing with full state tracking. */ import { vi, type Mock } from 'vitest'; import type { V3AgentType } from '../fixtures/agent-fixtures.js'; /** * Mock AgentDB - Vector database mock with HNSW simulation */ export class MockAgentDB { private vectors = new Map<string, { embedding: number[]; metadata: Record<string, unknown> }>(); private indexConfig = { M: 16, efConstruction: 200, efSearch: 50, dimensions: 384, }; // Mock methods for verification insert = vi.fn(async (id: string, embedding: number[], metadata?: Record<string, unknown>) => { if (embedding.length !== this.indexConfig.dimensions) { throw new Error(`Invalid embedding dimensions: expected ${this.indexConfig.dimensions}, got ${embedding.length}`); } this.vectors.set(id, { embedding, metadata: metadata ?? {} }); }); search = vi.fn(async (embedding: number[], k: number, threshold?: number) => { const results: Array<{ id: string; score: number; metadata: Record<string, unknown> }> = []; for (const [id, data] of this.vectors) { const score = this.cosineSimilarity(embedding, data.embedding); if (threshold === undefined || score >= threshold) { results.push({ id, score, metadata: data.metadata }); } } return results .sort((a, b) => b.score - a.score) .slice(0, k); }); delete = vi.fn(async (id: string) => { this.vectors.delete(id); }); update = vi.fn(async (id: string, embedding: number[], metadata?: Record<string, unknown>) => { const existing = this.vectors.get(id); if (!existing) { throw new Error(`Vector not found: ${id}`); } this.vectors.set(id, { embedding, metadata: metadata ?? existing.metadata }); }); getVector = vi.fn(async (id: string) => { return this.vectors.get(id) ?? null; }); getStats = vi.fn(() => ({ vectorCount: this.vectors.size, indexSize: this.vectors.size * this.indexConfig.dimensions * 4, // 4 bytes per float dimensions: this.indexConfig.dimensions, M: this.indexConfig.M, efConstruction: this.indexConfig.efConstruction, })); rebuildIndex = vi.fn(async () => { // Simulate index rebuild await new Promise(resolve => setTimeout(resolve, 10)); }); clear = vi.fn(() => { this.vectors.clear(); }); private cosineSimilarity(a: number[], b: number[]): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } /** * Configure the mock index */ configure(config: Partial<typeof this.indexConfig>): void { Object.assign(this.indexConfig, config); } /** * Get all stored vectors (for testing) */ getAllVectors(): Map<string, { embedding: number[]; metadata: Record<string, unknown> }> { return new Map(this.vectors); } } /** * Mock Unified Swarm Coordinator */ export class MockSwarmCoordinator { private agents = new Map<string, MockSwarmAgent>(); private state: SwarmState = { id: `swarm-${Date.now()}`, topology: 'hierarchical-mesh', status: 'idle', agentCount: 0, activeAgentCount: 0, leaderId: undefined, createdAt: new Date(), }; private messageQueue: SwarmMessage[] = []; private taskQueue: SwarmTask[] = []; initialize = vi.fn(async (config: SwarmInitConfig) => { this.state = { ...this.state, topology: config.topology ?? 'hierarchical-mesh', status: 'active', }; return this.state; }); shutdown = vi.fn(async (graceful: boolean = true) => { if (graceful) { // Complete pending tasks await Promise.all( Array.from(this.agents.values()).map(agent => agent.terminate()) ); } this.state.status = 'shutdown'; this.agents.clear(); }); addAgent = vi.fn(async (config: AgentConfig) => { const id = `agent-${config.type}-${Date.now()}`; const agent = new MockSwarmAgent(id, config); this.agents.set(id, agent); this.state.agentCount++; this.state.activeAgentCount++; if (config.type === 'queen-coordinator' && !this.state.leaderId) { this.state.leaderId = id; } return agent; }); removeAgent = vi.fn(async (agentId: string) => { const agent = this.agents.get(agentId); if (agent) { await agent.terminate(); this.agents.delete(agentId); this.state.agentCount--; this.state.activeAgentCount--; if (this.state.leaderId === agentId) { this.electNewLeader(); } } }); coordinate = vi.fn(async (task: SwarmTask) => { this.taskQueue.push(task); // Find suitable agents const suitableAgents = Array.from(this.agents.values()) .filter(agent => agent.canHandle(task.type)) .sort((a, b) => b.priority - a.priority); if (suitableAgents.length === 0) { return { success: false, error: 'No suitable agents available', taskId: task.id, duration: 0, }; } const startTime = Date.now(); const results: TaskResult[] = []; for (const agent of suitableAgents.slice(0, task.maxAgents ?? 1)) { const result = await agent.execute(task); results.push(result); } return { success: results.every(r => r.success), taskId: task.id, duration: Date.now() - startTime, results, }; }); broadcast = vi.fn(async (message: Omit<SwarmMessage, 'id' | 'timestamp'>) => { const fullMessage: SwarmMessage = { ...message, id: `msg-${Date.now()}`, timestamp: new Date(), to: 'broadcast', }; this.messageQueue.push(fullMessage); for (const agent of this.agents.values()) { await agent.receive(fullMessage); } }); sendMessage = vi.fn(async (message: SwarmMessage) => { this.messageQueue.push(message); if (message.to === 'broadcast') { for (const agent of this.agents.values()) { await agent.receive(message); } } else { const agent = this.agents.get(message.to); if (agent) { await agent.receive(message); } } }); requestConsensus = vi.fn(async <T>(request: ConsensusRequest<T>): Promise<ConsensusResponse<T>> => { const voters = request.voters ?? Array.from(this.agents.keys()); const votes = new Map<string, T>(); for (const voterId of voters) { const agent = this.agents.get(voterId); if (agent) { // Simulate voting - random selection const vote = request.options[Math.floor(Math.random() * request.options.length)]; votes.set(voterId, vote); } } const voteCounts = new Map<string, number>(); for (const vote of votes.values()) { const key = JSON.stringify(vote); voteCounts.set(key, (voteCounts.get(key) ?? 0) + 1); } const majority = Math.floor(voters.length / 2) + 1; let decision: T | null = null; let consensus = false; for (const [key, count] of voteCounts) { if (count >= majority) { decision = JSON.parse(key); consensus = true; break; } } return { topic: request.topic, decision, votes, consensus, votingDuration: 100, participatingAgents: Array.from(votes.keys()), }; }); getState = vi.fn(() => ({ ...this.state })); getAgent = vi.fn((id: string) => this.agents.get(id)); getAgents = vi.fn(() => Array.from(this.agents.values())); getMessageQueue = vi.fn(() => [...this.messageQueue]); getTaskQueue = vi.fn(() => [...this.taskQueue]); private electNewLeader(): void { const candidates = Array.from(this.agents.values()) .filter(a => a.config.type === 'queen-coordinator') .sort((a, b) => b.priority - a.priority); this.state.leaderId = candidates[0]?.id; } reset(): void { this.agents.clear(); this.messageQueue = []; this.taskQueue = []; this.state = { id: `swarm-${Date.now()}`, topology: 'hierarchical-mesh', status: 'idle', agentCount: 0, activeAgentCount: 0, leaderId: undefined, createdAt: new Date(), }; vi.clearAllMocks(); } } /** * Mock Swarm Agent */ export class MockSwarmAgent { readonly id: string; readonly config: AgentConfig; status: 'idle' | 'busy' | 'terminated' = 'idle'; priority: number; private messages: SwarmMessage[] = []; private taskResults: TaskResult[] = []; execute: Mock = vi.fn(); receive: Mock = vi.fn(); send: Mock = vi.fn(); terminate: Mock = vi.fn(); constructor(id: string, config: AgentConfig) { this.id = id; this.config = config; this.priority = config.priority ?? 50; this.execute.mockImplementation(async (task: SwarmTask) => { this.status = 'busy'; await new Promise(resolve => setTimeout(resolve, 10)); this.status = 'idle'; const result: TaskResult = { taskId: task.id, agentId: this.id, success: true, duration: Math.random() * 100 + 10, }; this.taskResults.push(result); return result; }); this.receive.mockImplementation(async (message: SwarmMessage) => { this.messages.push(message); }); this.send.mockImplementation(async () => {}); this.terminate.mockImplementation(async () => { this.status = 'terminated'; }); } canHandle(taskType: string): boolean { const capabilities = agentCapabilities[this.config.type] ?? []; return capabilities.some(cap => cap.includes(taskType) || taskType.includes(cap) ); } getMessages(): SwarmMessage[] { return [...this.messages]; } getTaskResults(): TaskResult[] { return [...this.taskResults]; } } /** * Mock Memory Service with caching */ export class MockMemoryService { private store = new Map<string, { value: unknown; metadata: MemoryMetadata; expiresAt?: Date }>(); private cache = new Map<string, { value: unknown; accessCount: number }>(); private cacheHits = 0; private cacheMisses = 0; set = vi.fn(async (key: string, value: unknown, metadata?: MemoryMetadata) => { const expiresAt = metadata?.ttl ? new Date(Date.now() + metadata.ttl) : undefined; this.store.set(key, { value, metadata: metadata ?? { type: 'short-term', tags: [] }, expiresAt }); this.cache.delete(key); // Invalidate cache }); get = vi.fn(async (key: string) => { // Check cache first const cached = this.cache.get(key); if (cached) { this.cacheHits++; cached.accessCount++; return cached.value; } this.cacheMisses++; const entry = this.store.get(key); if (!entry) { return null; } // Check expiration if (entry.expiresAt && entry.expiresAt < new Date()) { this.store.delete(key); return null; } // Add to cache this.cache.set(key, { value: entry.value, accessCount: 1 }); return entry.value; }); delete = vi.fn(async (key: string) => { this.store.delete(key); this.cache.delete(key); }); search = vi.fn(async (query: VectorSearchQuery) => { // Simulate vector search with filtering const results: SearchResult[] = []; for (const [key, entry] of this.store) { if (query.filters) { const matches = Object.entries(query.filters).every(([k, v]) => entry.metadata[k as keyof MemoryMetadata] === v ); if (!matches) continue; } results.push({ key, value: entry.value, score: Math.random() * 0.3 + 0.7, // Random score 0.7-1.0 metadata: entry.metadata, }); } return results .filter(r => !query.threshold || r.score >= query.threshold) .sort((a, b) => b.score - a.score) .slice(0, query.topK); }); clear = vi.fn(async () => { this.store.clear(); this.cache.clear(); }); getStats = vi.fn(() => ({ totalEntries: this.store.size, cacheSize: this.cache.size, cacheHitRate: this.cacheHits / (this.cacheHits + this.cacheMisses) || 0, cacheHits: this.cacheHits, cacheMisses: this.cacheMisses, })); prune = vi.fn(async () => { const now = new Date(); let pruned = 0; for (const [key, entry] of this.store) { if (entry.expiresAt && entry.expiresAt < now) { this.store.delete(key); this.cache.delete(key); pruned++; } } return pruned; }); reset(): void { this.store.clear(); this.cache.clear(); this.cacheHits = 0; this.cacheMisses = 0; vi.clearAllMocks(); } } /** * Mock Event Bus with history tracking */ export class MockEventBus { private subscribers = new Map<string, Set<EventHandler>>(); private history: DomainEvent[] = []; private maxHistorySize = 1000; publish = vi.fn(async (event: DomainEvent) => { this.history.push(event); if (this.history.length > this.maxHistorySize) { this.history.shift(); } const handlers = this.subscribers.get(event.type) ?? new Set(); const wildcardHandlers = this.subscribers.get('*') ?? new Set(); const allHandlers = [...handlers, ...wildcardHandlers]; await Promise.all(allHandlers.map(handler => handler(event))); }); subscribe = vi.fn((eventType: string, handler: EventHandler) => { if (!this.subscribers.has(eventType)) { this.subscribers.set(eventType, new Set()); } this.subscribers.get(eventType)!.add(handler); return () => this.unsubscribe(eventType, handler); }); unsubscribe = vi.fn((eventType: string, handler: EventHandler) => { this.subscribers.get(eventType)?.delete(handler); }); getHistory(eventType?: string): DomainEvent[] { if (eventType) { return this.history.filter(e => e.type === eventType); } return [...this.history]; } getSubscriberCount(eventType: string): number { return this.subscribers.get(eventType)?.size ?? 0; } clear(): void { this.history = []; vi.clearAllMocks(); } reset(): void { this.subscribers.clear(); this.history = []; vi.clearAllMocks(); } } /** * Mock Security Service */ export class MockSecurityService { private blockedPaths = ['../', '~/', '/etc/', '/tmp/', '/var/', '/root/']; private allowedCommands = ['npm', 'npx', 'node', 'git']; private tokens = new Map<string, { payload: Record<string, unknown>; expiresAt: Date }>(); validatePath = vi.fn((path: string) => { return !this.blockedPaths.some(blocked => path.includes(blocked)); }); validateInput = vi.fn((input: string, options?: InputValidationOptions) => { const errors: string[] = []; if (options?.maxLength && input.length > options.maxLength) { errors.push(`Input exceeds maximum length of ${options.maxLength}`); } if (options?.allowedChars && !options.allowedChars.test(input)) { errors.push('Input contains disallowed characters'); } return { valid: errors.length === 0, sanitized: options?.sanitize ? this.sanitize(input) : input, errors: errors.length > 0 ? errors : undefined, }; }); hashPassword = vi.fn(async (password: string) => { // Simulate argon2 hash format return `$argon2id$v=19$m=65536,t=3,p=4$${Buffer.from(password).toString('base64')}`; }); verifyPassword = vi.fn(async (password: string, hash: string) => { const parts = hash.split('$'); if (parts.length < 5) return false; return Buffer.from(parts[4], 'base64').toString() === password; }); generateToken = vi.fn(async (payload: Record<string, unknown>, expiresIn: number = 3600000) => { const token = `token_${Date.now()}_${Math.random().toString(36).slice(2)}`; this.tokens.set(token, { payload, expiresAt: new Date(Date.now() + expiresIn), }); return token; }); verifyToken = vi.fn(async (token: string) => { const entry = this.tokens.get(token); if (!entry) { throw new Error('Invalid token'); } if (entry.expiresAt < new Date()) { this.tokens.delete(token); throw new Error('Token expired'); } return entry.payload; }); executeSecurely = vi.fn(async (command: string, options?: ExecuteOptions) => { const [cmd] = command.split(' '); if (!this.allowedCommands.includes(cmd)) { throw new Error(`Command not allowed: ${cmd}`); } return { stdout: '', stderr: '', exitCode: 0, duration: Math.random() * 100, }; }); private sanitize(input: string): string { return input .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } reset(): void { this.tokens.clear(); vi.clearAllMocks(); } } // Supporting types interface SwarmState { id: string; topology: string; status: string; agentCount: number; activeAgentCount: number; leaderId?: string; createdAt: Date; } interface SwarmInitConfig { topology?: string; maxAgents?: number; } interface AgentConfig { type: V3AgentType; name?: string; capabilities?: string[]; priority?: number; } interface SwarmTask { id: string; type: string; payload: unknown; priority?: number; maxAgents?: number; } interface SwarmMessage { id: string; from: string; to: string; type: string; payload: unknown; timestamp: Date; } interface TaskResult { taskId: string; agentId: string; success: boolean; output?: unknown; error?: Error; duration: number; } interface ConsensusRequest<T> { topic: string; options: T[]; voters?: string[]; timeout?: number; } interface ConsensusResponse<T> { topic: string; decision: T | null; votes: Map<string, T>; consensus: boolean; votingDuration: number; participatingAgents: string[]; } interface MemoryMetadata { type: 'short-term' | 'long-term' | 'semantic' | 'episodic'; tags: string[]; ttl?: number; [key: string]: unknown; } interface VectorSearchQuery { embedding?: number[]; topK: number; threshold?: number; filters?: Record<string, unknown>; } interface SearchResult { key: string; value: unknown; score: number; metadata: MemoryMetadata; } interface DomainEvent { id: string; type: string; payload: unknown; timestamp: Date; correlationId?: string; } type EventHandler = (event: DomainEvent) => Promise<void>; interface InputValidationOptions { maxLength?: number; allowedChars?: RegExp; sanitize?: boolean; } interface ExecuteOptions { timeout?: number; cwd?: string; shell?: boolean; } // Agent capabilities mapping const agentCapabilities: Record<V3AgentType, string[]> = { 'queen-coordinator': ['orchestration', 'coordination', 'task-distribution'], 'security-architect': ['security', 'design', 'threat-modeling'], 'security-auditor': ['security', 'audit', 'vulnerability'], 'memory-specialist': ['memory', 'optimization', 'caching'], 'swarm-specialist': ['coordination', 'consensus', 'communication'], 'integration-architect': ['integration', 'api', 'compatibility'], 'performance-engineer': ['performance', 'optimization', 'benchmarking'], 'core-architect': ['architecture', 'design', 'domain'], 'test-architect': ['testing', 'tdd', 'quality'], 'project-coordinator': ['project', 'planning', 'scheduling'], 'coder': ['coding', 'implementation', 'debugging'], 'reviewer': ['review', 'quality', 'suggestions'], 'tester': ['testing', 'execution', 'coverage'], 'planner': ['planning', 'estimation', 'roadmap'], 'researcher': ['research', 'analysis', 'documentation'], }; /** * Create all mock services as a bundle */ export function createMockServices(): MockServiceBundle { return { agentDB: new MockAgentDB(), swarmCoordinator: new MockSwarmCoordinator(), memoryService: new MockMemoryService(), eventBus: new MockEventBus(), securityService: new MockSecurityService(), }; } /** * Mock service bundle interface */ export interface MockServiceBundle { agentDB: MockAgentDB; swarmCoordinator: MockSwarmCoordinator; memoryService: MockMemoryService; eventBus: MockEventBus; securityService: MockSecurityService; } /** * Reset all mock services */ export function resetMockServices(services: MockServiceBundle): void { services.agentDB.clear(); services.swarmCoordinator.reset(); services.memoryService.reset(); services.eventBus.reset(); services.securityService.reset(); }