aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
592 lines (488 loc) • 19.4 kB
text/typescript
/**
* Tests for MockAgentOrchestrator
* Validates agent registration, execution, error injection, and history tracking
* Target: 80%+ unit test coverage
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
MockAgentOrchestrator,
type MockAgentBehavior,
type AgentRequest,
type AgentResponse
} from '../../../../src/testing/mocks/agent-orchestrator.ts';
describe('MockAgentOrchestrator', () => {
// Timing tolerance for test assertions (ms)
const TIMING_TOLERANCE = 10;
let orchestrator: MockAgentOrchestrator;
beforeEach(() => {
orchestrator = new MockAgentOrchestrator();
});
describe('Agent Registration', () => {
it('should register an agent with behavior', () => {
const behavior: MockAgentBehavior = {
responseGenerator: (prompt: string) => `Response to: ${prompt}`
};
orchestrator.registerAgent('test-agent', behavior);
expect(orchestrator.hasAgent('test-agent')).toBe(true);
expect(orchestrator.getRegisteredAgents()).toContain('test-agent');
});
it('should register multiple agents', () => {
orchestrator.registerAgent('agent-1', {
responseGenerator: () => 'Response 1'
});
orchestrator.registerAgent('agent-2', {
responseGenerator: () => 'Response 2'
});
const agents = orchestrator.getRegisteredAgents();
expect(agents).toHaveLength(2);
expect(agents).toContain('agent-1');
expect(agents).toContain('agent-2');
});
it('should overwrite agent if registered twice', () => {
orchestrator.registerAgent('agent', {
responseGenerator: () => 'First'
});
orchestrator.registerAgent('agent', {
responseGenerator: () => 'Second'
});
expect(orchestrator.getRegisteredAgents()).toHaveLength(1);
});
it('should unregister an agent', () => {
orchestrator.registerAgent('test-agent', {
responseGenerator: () => 'Response'
});
const removed = orchestrator.unregisterAgent('test-agent');
expect(removed).toBe(true);
expect(orchestrator.hasAgent('test-agent')).toBe(false);
});
it('should return false when unregistering non-existent agent', () => {
const removed = orchestrator.unregisterAgent('non-existent');
expect(removed).toBe(false);
});
it('should clear all agents', () => {
orchestrator.registerAgent('agent-1', {
responseGenerator: () => 'Response 1'
});
orchestrator.registerAgent('agent-2', {
responseGenerator: () => 'Response 2'
});
orchestrator.clearAgents();
expect(orchestrator.getRegisteredAgents()).toHaveLength(0);
});
});
describe('Single Agent Execution', () => {
beforeEach(() => {
orchestrator.registerAgent('echo-agent', {
responseGenerator: (prompt: string) => `Echo: ${prompt}`
});
});
it('should execute registered agent', async () => {
const response = await orchestrator.executeAgent('echo-agent', 'Hello');
expect(response.agentType).toBe('echo-agent');
expect(response.output).toBe('Echo: Hello');
expect(response.executionTime).toBeGreaterThanOrEqual(0);
expect(response.error).toBeUndefined();
});
it('should throw error for unregistered agent', async () => {
await expect(
orchestrator.executeAgent('unknown-agent', 'Test')
).rejects.toThrow("Agent type 'unknown-agent' is not registered");
});
it('should pass prompt to response generator', async () => {
orchestrator.registerAgent('prompt-tester', {
responseGenerator: (prompt: string) => {
// Verify prompt contains expected content
return prompt.includes('test-data') ? 'Found' : 'Not found';
}
});
const response = await orchestrator.executeAgent(
'prompt-tester',
'Please process test-data'
);
expect(response.output).toBe('Found');
});
it('should record execution in history', async () => {
await orchestrator.executeAgent('echo-agent', 'Test prompt');
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(1);
expect(history[0].agentType).toBe('echo-agent');
expect(history[0].prompt).toBe('Test prompt');
expect(history[0].response.output).toBe('Echo: Test prompt');
expect(history[0].timestamp).toBeGreaterThan(0);
});
it('should track execution time', async () => {
orchestrator.registerAgent('delayed-agent', {
responseGenerator: () => 'Done',
delay: 50
});
const response = await orchestrator.executeAgent('delayed-agent', 'Test');
expect(response.executionTime).toBeGreaterThanOrEqual(50 - TIMING_TOLERANCE);
});
});
describe('Delay Simulation', () => {
// Parameterized test for basic delay scenarios
it.each([
{
name: 'behavior delay',
behaviorDelay: 100,
globalDelay: 0,
injectedDelay: 0,
expectedMin: 100
},
{
name: 'global delay',
behaviorDelay: 0,
globalDelay: 80,
injectedDelay: 0,
expectedMin: 80
},
{
name: 'combined global and behavior delays',
behaviorDelay: 50,
globalDelay: 50,
injectedDelay: 0,
expectedMin: 100
},
{
name: 'injected delay',
behaviorDelay: 0,
globalDelay: 0,
injectedDelay: 75,
expectedMin: 75
},
{
name: 'all delay sources combined',
behaviorDelay: 30,
globalDelay: 30,
injectedDelay: 30,
expectedMin: 90
}
])('should apply $name correctly', async ({ behaviorDelay, globalDelay, injectedDelay, expectedMin }) => {
orchestrator.registerAgent('agent', {
responseGenerator: () => 'Result',
delay: behaviorDelay
});
if (globalDelay > 0) {
orchestrator.setGlobalDelay(globalDelay);
}
if (injectedDelay > 0) {
orchestrator.injectDelay('agent', injectedDelay);
}
const start = Date.now();
await orchestrator.executeAgent('agent', 'Test');
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(expectedMin - TIMING_TOLERANCE);
});
it('should clear injected delay after first use', async () => {
orchestrator.registerAgent('agent', {
responseGenerator: () => 'Result'
});
orchestrator.injectDelay('agent', 100);
// First execution should be delayed
const start1 = Date.now();
await orchestrator.executeAgent('agent', 'Test 1');
const duration1 = Date.now() - start1;
expect(duration1).toBeGreaterThanOrEqual(100 - TIMING_TOLERANCE);
// Second execution should NOT be delayed
const start2 = Date.now();
await orchestrator.executeAgent('agent', 'Test 2');
const duration2 = Date.now() - start2;
expect(duration2).toBeLessThan(50);
});
// Consolidated negative validation tests
it.each([
{ type: 'global', value: -10, method: 'setGlobalDelay', message: 'Global delay must be non-negative' },
{ type: 'injected', value: -5, method: 'injectDelay', message: 'Injected delay must be non-negative' }
])('should throw error for negative $type delay', ({ value, method, message }) => {
if (method === 'setGlobalDelay') {
expect(() => orchestrator.setGlobalDelay(value)).toThrow(message);
} else {
expect(() => orchestrator.injectDelay('agent', value)).toThrow(message);
}
});
});
describe('Error Injection', () => {
beforeEach(() => {
orchestrator.registerAgent('test-agent', {
responseGenerator: () => 'Success'
});
});
it('should inject and throw error', async () => {
const testError = new Error('Injected failure');
orchestrator.injectError('test-agent', testError);
await expect(
orchestrator.executeAgent('test-agent', 'Test')
).rejects.toThrow('Injected failure');
});
it('should record error in execution history', async () => {
const testError = new Error('Test error');
orchestrator.injectError('test-agent', testError);
try {
await orchestrator.executeAgent('test-agent', 'Test');
} catch {
// Expected
}
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(1);
expect(history[0].response.error).toBeDefined();
expect(history[0].response.error?.message).toBe('Test error');
});
it('should clear injected error after first use', async () => {
orchestrator.injectError('test-agent', new Error('First error'));
// First execution should fail
await expect(
orchestrator.executeAgent('test-agent', 'Test 1')
).rejects.toThrow('First error');
// Second execution should succeed
const response = await orchestrator.executeAgent('test-agent', 'Test 2');
expect(response.output).toBe('Success');
});
it('should respect error rate configuration', async () => {
orchestrator.registerAgent('unreliable-agent', {
responseGenerator: () => 'Success',
errorRate: 1.0 // Always fail
});
await expect(
orchestrator.executeAgent('unreliable-agent', 'Test')
).rejects.toThrow('Simulated random failure');
});
it('should succeed when error rate is 0', async () => {
orchestrator.registerAgent('reliable-agent', {
responseGenerator: () => 'Success',
errorRate: 0
});
const response = await orchestrator.executeAgent('reliable-agent', 'Test');
expect(response.output).toBe('Success');
});
});
describe('Parallel Execution', () => {
beforeEach(() => {
orchestrator.registerAgent('agent-1', {
responseGenerator: (prompt: string) => `Agent 1: ${prompt}`,
delay: 20
});
orchestrator.registerAgent('agent-2', {
responseGenerator: (prompt: string) => `Agent 2: ${prompt}`,
delay: 30
});
orchestrator.registerAgent('agent-3', {
responseGenerator: (prompt: string) => `Agent 3: ${prompt}`,
delay: 10
});
});
it('should execute multiple agents in parallel', async () => {
const requests: AgentRequest[] = [
{ agentType: 'agent-1', prompt: 'Task A' },
{ agentType: 'agent-2', prompt: 'Task B' },
{ agentType: 'agent-3', prompt: 'Task C' }
];
const start = Date.now();
const responses = await orchestrator.executeParallel(requests);
const duration = Date.now() - start;
// Should complete in time of slowest agent (30ms), not sum (60ms)
expect(duration).toBeLessThan(100); // Allow overhead
expect(responses).toHaveLength(3);
});
// Consolidated request order and history tracking
it('should maintain request order in responses and record in history', async () => {
const requests: AgentRequest[] = [
{ agentType: 'agent-1', prompt: 'First' },
{ agentType: 'agent-2', prompt: 'Second' }
];
const responses = await orchestrator.executeParallel(requests);
// Check order
expect(responses[0].agentType).toBe('agent-1');
expect(responses[0].output).toBe('Agent 1: First');
expect(responses[1].agentType).toBe('agent-2');
expect(responses[1].output).toBe('Agent 2: Second');
// Check history
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(2);
});
it('should handle errors in parallel execution', async () => {
orchestrator.injectError('agent-2', new Error('Agent 2 failed'));
const requests: AgentRequest[] = [
{ agentType: 'agent-1', prompt: 'A' },
{ agentType: 'agent-2', prompt: 'B' },
{ agentType: 'agent-3', prompt: 'C' }
];
const responses = await orchestrator.executeParallel(requests);
expect(responses).toHaveLength(3);
expect(responses[0].error).toBeUndefined();
expect(responses[1].error).toBeDefined();
expect(responses[1].error?.message).toBe('Agent 2 failed');
expect(responses[2].error).toBeUndefined();
});
it('should continue execution when one agent fails', async () => {
orchestrator.registerAgent('failing-agent', {
responseGenerator: () => 'Never reached',
errorRate: 1.0
});
const requests: AgentRequest[] = [
{ agentType: 'agent-1', prompt: 'A' },
{ agentType: 'failing-agent', prompt: 'B' },
{ agentType: 'agent-3', prompt: 'C' }
];
const responses = await orchestrator.executeParallel(requests);
expect(responses[0].output).toBe('Agent 1: A');
expect(responses[1].error).toBeDefined();
expect(responses[2].output).toBe('Agent 3: C');
});
});
describe('Execution History', () => {
beforeEach(() => {
orchestrator.registerAgent('test-agent', {
responseGenerator: (prompt: string) => `Result: ${prompt}`
});
});
it('should track multiple executions', async () => {
await orchestrator.executeAgent('test-agent', 'First');
await orchestrator.executeAgent('test-agent', 'Second');
await orchestrator.executeAgent('test-agent', 'Third');
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(3);
expect(history[0].prompt).toBe('First');
expect(history[1].prompt).toBe('Second');
expect(history[2].prompt).toBe('Third');
});
it('should include timestamps in execution records', async () => {
const beforeExecution = Date.now();
await orchestrator.executeAgent('test-agent', 'Test');
const afterExecution = Date.now();
const history = orchestrator.getExecutionHistory();
const timestamp = history[0].timestamp;
expect(timestamp).toBeGreaterThanOrEqual(beforeExecution);
expect(timestamp).toBeLessThanOrEqual(afterExecution);
});
it('should return copy of history to prevent mutation', async () => {
await orchestrator.executeAgent('test-agent', 'Test');
const history1 = orchestrator.getExecutionHistory();
const history2 = orchestrator.getExecutionHistory();
expect(history1).not.toBe(history2); // Different array instances
expect(history1).toEqual(history2); // But equal content
});
it('should track failed executions', async () => {
orchestrator.injectError('test-agent', new Error('Test failure'));
try {
await orchestrator.executeAgent('test-agent', 'Test');
} catch {
// Expected
}
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(1);
expect(history[0].response.error).toBeDefined();
});
});
describe('State Reset', () => {
beforeEach(() => {
orchestrator.registerAgent('test-agent', {
responseGenerator: () => 'Result'
});
});
// Consolidated reset tests using single test with assertions
it('should clear history, errors, delays, and global delay but preserve agents', async () => {
// Setup state
await orchestrator.executeAgent('test-agent', 'Test 1');
await orchestrator.executeAgent('test-agent', 'Test 2');
orchestrator.injectError('test-agent', new Error('Test error'));
orchestrator.injectDelay('test-agent', 100);
orchestrator.setGlobalDelay(100);
// Reset
orchestrator.reset();
// Verify history cleared
expect(orchestrator.getExecutionHistory()).toHaveLength(0);
// Verify agents preserved
expect(orchestrator.hasAgent('test-agent')).toBe(true);
// Verify errors cleared
const response = await orchestrator.executeAgent('test-agent', 'Test');
expect(response.output).toBe('Result');
// Verify delays cleared
const start = Date.now();
await orchestrator.executeAgent('test-agent', 'Test 2');
const duration = Date.now() - start;
expect(duration).toBeLessThan(50);
});
});
describe('Integration Scenarios', () => {
it('should simulate realistic multi-agent workflow', async () => {
// Setup: Register agents for SAD review workflow
orchestrator.registerAgent('architecture-designer', {
responseGenerator: (prompt: string) => {
return `# Software Architecture Document\n\nDraft architecture for: ${prompt}`;
},
delay: 100
});
orchestrator.registerAgent('security-architect', {
responseGenerator: () => {
return 'APPROVED: Security review complete. No major concerns.';
},
delay: 50
});
orchestrator.registerAgent('test-architect', {
responseGenerator: () => {
return 'CONDITIONAL: Add performance test strategy.';
},
delay: 40
});
// Execute: Primary author creates draft
const draft = await orchestrator.executeAgent(
'architecture-designer',
'E-commerce platform'
);
expect(draft.output).toContain('Software Architecture Document');
// Execute: Parallel reviewers
const reviews = await orchestrator.executeParallel([
{ agentType: 'security-architect', prompt: 'Review SAD' },
{ agentType: 'test-architect', prompt: 'Review SAD' }
]);
expect(reviews).toHaveLength(2);
expect(reviews[0].output).toContain('APPROVED');
expect(reviews[1].output).toContain('CONDITIONAL');
// Verify history
const history = orchestrator.getExecutionHistory();
expect(history).toHaveLength(3); // 1 draft + 2 reviews
});
it('should handle partial failures in review cycle', async () => {
orchestrator.registerAgent('reviewer-1', {
responseGenerator: () => 'APPROVED'
});
orchestrator.registerAgent('reviewer-2', {
responseGenerator: () => 'APPROVED'
});
orchestrator.registerAgent('reviewer-3', {
responseGenerator: () => 'APPROVED'
});
// Inject failure in one reviewer
orchestrator.injectError(
'reviewer-2',
new Error('Reviewer unavailable')
);
const reviews = await orchestrator.executeParallel([
{ agentType: 'reviewer-1', prompt: 'Review' },
{ agentType: 'reviewer-2', prompt: 'Review' },
{ agentType: 'reviewer-3', prompt: 'Review' }
]);
const successful = reviews.filter(r => !r.error);
const failed = reviews.filter(r => r.error);
expect(successful).toHaveLength(2);
expect(failed).toHaveLength(1);
});
it('should support retry logic with error injection', async () => {
orchestrator.registerAgent('retry-agent', {
responseGenerator: () => 'Success'
});
// First attempt: inject error
orchestrator.injectError('retry-agent', new Error('Temporary failure'));
let attempt1Failed = false;
try {
await orchestrator.executeAgent('retry-agent', 'Attempt 1');
} catch {
attempt1Failed = true;
}
expect(attempt1Failed).toBe(true);
// Second attempt: should succeed (error cleared)
const response = await orchestrator.executeAgent('retry-agent', 'Attempt 2');
expect(response.output).toBe('Success');
});
});
});