mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
108676e602
Phase 2 Implementation (95% complete): - All 12 planned deliverables implemented - 5/5 validation criteria passing (VC1-VC5) - 0 TypeScript errors, 0 critical bugs - Comprehensive documentation (14 files) Core Infrastructure: - Agent lifecycle instrumentation (agent.ts, task.ts) - Memory operation instrumentation (memory.ts) with SwarmMemoryManager integration - Token tracking and cost calculation (CostTracker) - 4 clause evaluators (AST, metric, pattern, semantic) - Multi-agent voting protocol with consensus algorithms - Distributed trace propagation with OTEL CLI Commands (Phase 3/4 completed early): - Telemetry: status, metrics, trace, export-prometheus - Constitution: validate, list, show, evaluate Validation: - VC1: Agent trace retrieval ✅ - VC2: Token tracking ✅ - VC3: Clause evaluation ✅ - VC4: Multi-agent voting ✅ - VC5: Memory operation instrumentation ✅ Tests: - 5 validation scripts (790+ LOC) - 13+ unit tests - Integration tests for SwarmMemoryManager - All tests passing Documentation: - MEMORY-TS-ANALYSIS.md (10+ pages deep analysis) - PHASE2-FINAL-STATUS.md (comprehensive status) - BRUTAL-HONESTY-REVIEW-PHASE2.md (technical review) - SESSION-SUMMARY.md (achievements summary) - CLI documentation (telemetry + constitution) - Memory instrumentation guides (integration + examples) Files Created (60+): - Instrumentation: 4 files (agent, task, memory, index) - Evaluators: 6 files (AST, metric, pattern, semantic, base, index) - Voting: 6 files (protocol, panel, consensus, orchestrator, types, index) - Validation: 5 scripts - Documentation: 14+ files - Tests: 10+ test files - CLI: 2 command files What's Left (5%): - CLI integration testing against real OTEL collector - Real-world validation with production agent workflows Ready for Phase 3: ✅ Dashboards & Visualization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
/**
|
|
* Tests for voting orchestrator
|
|
* Validates parallel coordination, timeout handling, and result aggregation
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
VotingOrchestrator,
|
|
DefaultAgentPool,
|
|
DefaultVotingStrategy,
|
|
ConsensusFactory
|
|
} from '../../src/voting/index.js';
|
|
import type {
|
|
VotingAgent,
|
|
VotingTask,
|
|
Vote,
|
|
VotingPanelConfig
|
|
} from '../../src/voting/types.js';
|
|
|
|
describe('VotingOrchestrator', () => {
|
|
let orchestrator: VotingOrchestrator;
|
|
let pool: DefaultAgentPool;
|
|
let strategy: DefaultVotingStrategy;
|
|
let mockVoteExecutor: ReturnType<typeof vi.fn>;
|
|
|
|
const createMockAgents = (): VotingAgent[] => [
|
|
{
|
|
id: 'agent-1',
|
|
type: 'test-generator',
|
|
expertise: ['unit-testing', 'integration-testing'],
|
|
weight: 1.0
|
|
},
|
|
{
|
|
id: 'agent-2',
|
|
type: 'coverage-analyzer',
|
|
expertise: ['coverage-analysis', 'gap-detection'],
|
|
weight: 1.2
|
|
},
|
|
{
|
|
id: 'agent-3',
|
|
type: 'quality-gate',
|
|
expertise: ['quality-metrics', 'policy-enforcement'],
|
|
weight: 1.5
|
|
},
|
|
{
|
|
id: 'agent-4',
|
|
type: 'performance-tester',
|
|
expertise: ['performance', 'benchmarking'],
|
|
weight: 0.9
|
|
},
|
|
{
|
|
id: 'agent-5',
|
|
type: 'security-scanner',
|
|
expertise: ['security', 'vulnerability-detection'],
|
|
weight: 1.3
|
|
}
|
|
];
|
|
|
|
const createMockTask = (): VotingTask => ({
|
|
id: 'task-1',
|
|
type: 'test-generation',
|
|
description: 'Generate comprehensive test suite',
|
|
context: { sourceFile: 'user-service.ts' },
|
|
priority: 'high',
|
|
requiredExpertise: ['unit-testing', 'coverage-analysis']
|
|
});
|
|
|
|
beforeEach(() => {
|
|
const agents = createMockAgents();
|
|
pool = new DefaultAgentPool(agents);
|
|
strategy = new DefaultVotingStrategy();
|
|
|
|
mockVoteExecutor = vi.fn().mockImplementation(
|
|
async (agent: VotingAgent, task: VotingTask): Promise<Vote> => {
|
|
// Simulate voting delay
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
return {
|
|
agentId: agent.id,
|
|
taskId: task.id,
|
|
score: 0.8 + Math.random() * 0.2,
|
|
confidence: 0.85 + Math.random() * 0.15,
|
|
reasoning: `Agent ${agent.id} evaluated task ${task.id}`,
|
|
timestamp: new Date()
|
|
};
|
|
}
|
|
);
|
|
|
|
orchestrator = new VotingOrchestrator(pool, strategy, mockVoteExecutor);
|
|
});
|
|
|
|
describe('Panel Assembly', () => {
|
|
it('should assemble panel with required size', async () => {
|
|
const config: VotingPanelConfig = {
|
|
minPanelSize: 3,
|
|
maxPanelSize: 5,
|
|
consensusMethod: 'weighted-average',
|
|
timeoutMs: 5000,
|
|
maxRetries: 3,
|
|
retryDelayMs: 100,
|
|
parallelExecution: true
|
|
};
|
|
|
|
const result = await orchestrator.assemblePanel(config);
|
|
|
|
expect(result.panel.length).toBeGreaterThanOrEqual(config.minPanelSize);
|
|
expect(result.panel.length).toBeLessThanOrEqual(config.maxPanelSize);
|
|
expect(result.assemblyTime).toBeGreaterThan(0);
|
|
expect(result.coverage.expertise.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should select agents with matching expertise', async () => {
|
|
const config: VotingPanelConfig = {
|
|
minPanelSize: 2,
|
|
maxPanelSize: 4,
|
|
requiredExpertise: ['unit-testing', 'coverage-analysis'],
|
|
consensusMethod: 'quorum',
|
|
quorumThreshold: 0.67,
|
|
timeoutMs: 5000,
|
|
maxRetries: 3,
|
|
retryDelayMs: 100,
|
|
parallelExecution: true
|
|
};
|
|
|
|
const result = await orchestrator.assemblePanel(config);
|
|
|
|
const hasRequiredExpertise = result.panel.every(agent =>
|
|
config.requiredExpertise?.some(e => agent.expertise.includes(e))
|
|
);
|
|
|
|
expect(hasRequiredExpertise).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Parallel Voting', () => {
|
|
it('should distribute task and collect votes in parallel', async () => {
|
|
const task = createMockTask();
|
|
const config: VotingPanelConfig = {
|
|
minPanelSize: 3,
|
|
maxPanelSize: 5,
|
|
consensusMethod: 'weighted-average',
|
|
timeoutMs: 5000,
|
|
maxRetries: 3,
|
|
retryDelayMs: 100,
|
|
parallelExecution: true
|
|
};
|
|
|
|
const panelResult = await orchestrator.assemblePanel(config);
|
|
await orchestrator.distributeTask(task, panelResult.panel);
|
|
|
|
const votes = await orchestrator.collectVotes(task.id, 5000);
|
|
|
|
expect(votes.length).toBeGreaterThan(0);
|
|
expect(votes.length).toBeLessThanOrEqual(panelResult.panel.length);
|
|
expect(mockVoteExecutor).toHaveBeenCalledTimes(panelResult.panel.length);
|
|
});
|
|
|
|
it('should handle concurrent voting for multiple agents', async () => {
|
|
const task = createMockTask();
|
|
const agents = createMockAgents();
|
|
|
|
await orchestrator.distributeTask(task, agents);
|
|
const votes = await orchestrator.collectVotes(task.id, 5000);
|
|
|
|
// Verify all agents voted
|
|
const agentIds = votes.map(v => v.agentId);
|
|
expect(agentIds.length).toBe(agents.length);
|
|
expect(new Set(agentIds).size).toBe(agents.length); // All unique
|
|
});
|
|
});
|
|
|
|
describe('Timeout Handling', () => {
|
|
it('should handle vote timeout gracefully', async () => {
|
|
// Create slow vote executor
|
|
const slowExecutor = vi.fn().mockImplementation(
|
|
async (agent: VotingAgent, task: VotingTask): Promise<Vote> => {
|
|
if (agent.id === 'agent-3') {
|
|
// Simulate timeout for one agent
|
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
return {
|
|
agentId: agent.id,
|
|
taskId: task.id,
|
|
score: 0.8,
|
|
confidence: 0.85,
|
|
reasoning: `Vote from ${agent.id}`,
|
|
timestamp: new Date()
|
|
};
|
|
}
|
|
);
|
|
|
|
const slowOrchestrator = new VotingOrchestrator(
|
|
pool,
|
|
strategy,
|
|
slowExecutor
|
|
);
|
|
|
|
const task = createMockTask();
|
|
const agents = createMockAgents().slice(0, 3); // Use 3 agents
|
|
|
|
await slowOrchestrator.distributeTask(task, agents);
|
|
const votes = await slowOrchestrator.collectVotes(task.id, 1000); // Short timeout
|
|
|
|
// Should collect votes from agents that completed in time
|
|
expect(votes.length).toBeGreaterThan(0);
|
|
expect(votes.length).toBeLessThan(agents.length);
|
|
|
|
const metrics = slowOrchestrator.getMetrics();
|
|
expect(metrics.timeoutVotes).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Retry Logic', () => {
|
|
it('should retry failed votes', async () => {
|
|
let attemptCount = 0;
|
|
|
|
const flakyExecutor = vi.fn().mockImplementation(
|
|
async (agent: VotingAgent, task: VotingTask): Promise<Vote> => {
|
|
attemptCount++;
|
|
|
|
// Fail first attempt, succeed on retry
|
|
if (agent.id === 'agent-2' && attemptCount === 1) {
|
|
throw new Error('ECONNRESET: Connection reset');
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
return {
|
|
agentId: agent.id,
|
|
taskId: task.id,
|
|
score: 0.8,
|
|
confidence: 0.85,
|
|
reasoning: `Vote from ${agent.id}`,
|
|
timestamp: new Date()
|
|
};
|
|
}
|
|
);
|
|
|
|
const retryOrchestrator = new VotingOrchestrator(
|
|
pool,
|
|
strategy,
|
|
flakyExecutor
|
|
);
|
|
|
|
const task = createMockTask();
|
|
const agent = createMockAgents()[1]; // agent-2
|
|
|
|
const vote = await retryOrchestrator.retry(agent.id, task.id, 1);
|
|
|
|
expect(vote).not.toBeNull();
|
|
expect(vote?.agentId).toBe(agent.id);
|
|
|
|
const metrics = retryOrchestrator.getMetrics();
|
|
expect(metrics.retryRate).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should not retry after max attempts', async () => {
|
|
const failingExecutor = vi.fn().mockRejectedValue(
|
|
new Error('Persistent failure')
|
|
);
|
|
|
|
const retryOrchestrator = new VotingOrchestrator(
|
|
pool,
|
|
strategy,
|
|
failingExecutor
|
|
);
|
|
|
|
const task = createMockTask();
|
|
const agent = createMockAgents()[0];
|
|
|
|
const vote = await retryOrchestrator.retry(agent.id, task.id, 3);
|
|
|
|
expect(vote).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Result Aggregation', () => {
|
|
it('should aggregate votes using weighted average', async () => {
|
|
const votes: Vote[] = [
|
|
{
|
|
agentId: 'agent-1',
|
|
taskId: 'task-1',
|
|
score: 0.8,
|
|
confidence: 0.9,
|
|
reasoning: 'Good coverage',
|
|
timestamp: new Date()
|
|
},
|
|
{
|
|
agentId: 'agent-2',
|
|
taskId: 'task-1',
|
|
score: 0.85,
|
|
confidence: 0.95,
|
|
reasoning: 'Excellent patterns',
|
|
timestamp: new Date()
|
|
},
|
|
{
|
|
agentId: 'agent-3',
|
|
taskId: 'task-1',
|
|
score: 0.9,
|
|
confidence: 0.85,
|
|
reasoning: 'Strong quality',
|
|
timestamp: new Date()
|
|
}
|
|
];
|
|
|
|
const result = orchestrator.aggregateResults(votes, 'weighted-average');
|
|
|
|
expect(result.consensusReached).toBe(true);
|
|
expect(result.finalScore).toBeGreaterThan(0.8);
|
|
expect(result.finalScore).toBeLessThan(0.9);
|
|
expect(result.votes.length).toBe(3);
|
|
expect(result.aggregationMethod).toBe('weighted-average');
|
|
});
|
|
|
|
it('should detect consensus with majority method', async () => {
|
|
const votes: Vote[] = [
|
|
{
|
|
agentId: 'agent-1',
|
|
taskId: 'task-1',
|
|
score: 0.9,
|
|
confidence: 0.9,
|
|
reasoning: 'Excellent',
|
|
timestamp: new Date()
|
|
},
|
|
{
|
|
agentId: 'agent-2',
|
|
taskId: 'task-1',
|
|
score: 0.9,
|
|
confidence: 0.95,
|
|
reasoning: 'Excellent',
|
|
timestamp: new Date()
|
|
},
|
|
{
|
|
agentId: 'agent-3',
|
|
taskId: 'task-1',
|
|
score: 0.5,
|
|
confidence: 0.8,
|
|
reasoning: 'Needs work',
|
|
timestamp: new Date()
|
|
}
|
|
];
|
|
|
|
const result = orchestrator.aggregateResults(votes, 'majority');
|
|
|
|
expect(result.finalScore).toBe(0.9);
|
|
expect(result.consensusReached).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Orchestration Metrics', () => {
|
|
it('should track orchestration metrics', async () => {
|
|
const task = createMockTask();
|
|
const agents = createMockAgents().slice(0, 3);
|
|
|
|
await orchestrator.distributeTask(task, agents);
|
|
await orchestrator.collectVotes(task.id, 5000);
|
|
|
|
const metrics = orchestrator.getMetrics();
|
|
|
|
expect(metrics.totalTasks).toBe(1);
|
|
expect(metrics.successfulVotes).toBeGreaterThan(0);
|
|
expect(metrics.averageExecutionTime).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Orchestration Logging', () => {
|
|
it('should log orchestration events', async () => {
|
|
const task = createMockTask();
|
|
const config: VotingPanelConfig = {
|
|
minPanelSize: 2,
|
|
maxPanelSize: 3,
|
|
consensusMethod: 'quorum',
|
|
quorumThreshold: 0.67,
|
|
timeoutMs: 5000,
|
|
maxRetries: 3,
|
|
retryDelayMs: 100,
|
|
parallelExecution: true
|
|
};
|
|
|
|
await orchestrator.assemblePanel(config);
|
|
const logs = orchestrator.getLogs();
|
|
|
|
expect(logs.length).toBeGreaterThan(0);
|
|
expect(logs[0].event).toBe('panel-assembled');
|
|
expect(logs[0].taskId).toBeDefined();
|
|
});
|
|
|
|
it('should filter logs by task ID', async () => {
|
|
const task1 = { ...createMockTask(), id: 'task-1' };
|
|
const task2 = { ...createMockTask(), id: 'task-2' };
|
|
const agents = createMockAgents().slice(0, 2);
|
|
|
|
await orchestrator.distributeTask(task1, agents);
|
|
await orchestrator.distributeTask(task2, agents);
|
|
|
|
const task1Logs = orchestrator.getLogs('task-1');
|
|
const task2Logs = orchestrator.getLogs('task-2');
|
|
|
|
expect(task1Logs.every(log => log.taskId === 'task-1')).toBe(true);
|
|
expect(task2Logs.every(log => log.taskId === 'task-2')).toBe(true);
|
|
});
|
|
});
|
|
});
|