UNPKG

claude-code-collective

Version:

Sub-agent collective framework for Claude Code with TDD validation, hub-spoke coordination, and automated handoffs

674 lines (561 loc) 22 kB
/** * Claude Code Sub-Agent Collective - Agent Lifecycle Tests * * Phase 7: Dynamic Agent Creation * * Comprehensive test suite covering complete agent lifecycle from spawn to cleanup. * Tests error scenarios, resource management, and concurrent operations. * * @author Claude Code Sub-Agent Collective * @version 1.0.0 */ const fs = require('fs-extra'); const path = require('path'); const AgentTemplateSystem = require('../lib/AgentTemplateSystem'); const AgentSpawner = require('../lib/AgentSpawner'); const AgentRegistry = require('../lib/AgentRegistry'); const AgentLifecycleManager = require('../lib/AgentLifecycleManager'); const AgentSpawnCommand = require('../lib/AgentSpawnCommand'); describe('Agent Lifecycle Tests', () => { let templateSystem; let spawner; let registry; let lifecycle; let spawnCommand; let testDir; beforeAll(async () => { // Create test directory testDir = path.join(__dirname, 'temp-lifecycle-test'); await fs.ensureDir(testDir); // Initialize components with test configuration const testConfig = { agentsDir: path.join(testDir, 'agents'), testsDir: path.join(testDir, 'tests'), archiveDir: path.join(testDir, 'archive'), registryDir: path.join(testDir, 'state'), templatesDir: path.join(testDir, 'templates') }; templateSystem = new AgentTemplateSystem({ templatesDir: testConfig.templatesDir }); spawner = new AgentSpawner({ agentsDir: testConfig.agentsDir, testsDir: testConfig.testsDir, archiveDir: testConfig.archiveDir, templateSystem: { templatesDir: testConfig.templatesDir } }); registry = new AgentRegistry({ registryDir: testConfig.registryDir }); lifecycle = new AgentLifecycleManager(spawner, registry, { autoCleanupEnabled: true, idleTimeout: 1000, // 1 second for testing maxAge: 5000, // 5 seconds for testing healthCheckInterval: 500 // 500ms for testing }); spawnCommand = new AgentSpawnCommand({ spawner: testConfig, registry: testConfig, lifecycle: { autoCleanupEnabled: true, idleTimeout: 1000, maxAge: 5000 } }); // Initialize all systems await templateSystem.initialize(); await spawner.initialize(); await registry.initialize(); await lifecycle.initialize(); await spawnCommand.initialize(); }); afterAll(async () => { // Cleanup test directory await lifecycle.shutdown(); await registry.shutdown(); await fs.remove(testDir); }); beforeEach(async () => { // Clear any existing agents between tests const agents = registry.query(); for (const agent of agents) { await registry.unregister(agent.id, { cleanup: true }); } }); describe('Basic Agent Spawning', () => { test('should spawn agent with base template', async () => { const config = { name: 'test-base-agent', agentName: 'test-base-agent', purpose: 'Testing base template functionality', template: 'base' }; const result = await spawner.spawn(config); expect(result.success).toBe(true); expect(result.agent.id).toBeDefined(); expect(result.agent.name).toBe('test-base-agent'); expect(result.agent.template).toBe('base'); expect(result.agent.path).toBeDefined(); // Verify agent file was created expect(await fs.pathExists(result.agent.path)).toBe(true); }); test('should spawn agent with research template', async () => { const config = { name: 'test-research-agent', agentName: 'test-research-agent', purpose: 'Testing research template functionality', template: 'base', // Use base template for simplicity testType: 'integration' }; const result = await spawner.spawn(config); expect(result.success).toBe(true); expect(result.agent.template).toBe('base'); expect(result.agent.metadata.tools).toBeDefined(); }); test('should spawn agent with implementation template', async () => { const config = { name: 'test-implementation-agent', agentName: 'test-implementation-agent', purpose: 'Testing implementation template functionality', template: 'base', // Use base template for simplicity testType: 'integration' }; const result = await spawner.spawn(config); expect(result.success).toBe(true); expect(result.agent.template).toBe('base'); expect(result.agent.metadata.tools).toBeDefined(); }); }); describe('Agent Registration', () => { test('should register agent in registry after spawn', async () => { const config = { name: 'registry-test-agent', agentName: 'registry-test-agent', purpose: 'Testing registry integration', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); const registeredAgent = registry.getAgent(result.agent.id); expect(registeredAgent).toBeDefined(); expect(registeredAgent.id).toBe(result.agent.id); expect(registeredAgent.status).toBe('active'); expect(registeredAgent.activity).toBeDefined(); expect(registeredAgent.performance).toBeDefined(); }); test('should prevent duplicate agent registration', async () => { const config = { name: 'duplicate-test-agent', agentName: 'duplicate-test-agent', purpose: 'Testing duplicate prevention', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); const agentInfo = { id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }; await registry.register(agentInfo); // Attempt duplicate registration await expect(registry.register(agentInfo)).rejects.toThrow('already registered'); }); }); describe('Error Handling', () => { test('should handle invalid template gracefully', async () => { const config = { name: 'invalid-template-agent', agentName: 'invalid-template-agent', purpose: 'Testing invalid template handling', template: 'non-existent-template' }; await expect(spawner.spawn(config)).rejects.toThrow('Specified template'); }); test('should handle missing required parameters', async () => { const config = { agentName: 'missing-params-agent' // Missing required parameter: purpose // This test intentionally omits required parameters }; await expect(spawner.spawn(config)).rejects.toThrow('purpose or description is required'); }); test('should handle spawn failures gracefully', async () => { const config = { name: '', // Invalid name agentName: '', purpose: 'Testing spawn failure handling' }; await expect(spawner.spawn(config)).rejects.toThrow(); }); }); describe('Concurrent Operations', () => { test('should handle concurrent spawning', async () => { const configs = Array.from({ length: 5 }, (_, i) => ({ name: `concurrent-agent-${i}`, agentName: `concurrent-agent-${i}`, purpose: `Testing concurrent spawning ${i}`, template: 'base', testType: 'integration' })); const promises = configs.map(config => spawner.spawn(config)); const results = await Promise.allSettled(promises); const successful = results.filter(r => r.status === 'fulfilled'); expect(successful.length).toBe(5); // Verify all agents have unique IDs const ids = successful.map(r => r.value.agent.id); const uniqueIds = new Set(ids); expect(uniqueIds.size).toBe(5); }); test('should handle race conditions in registry', async () => { const config = { name: 'race-condition-agent', agentName: 'race-condition-agent', purpose: 'Testing race conditions', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); const agentInfo = { id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }; // Attempt concurrent registration and status updates const promises = [ registry.register(agentInfo), new Promise(resolve => setTimeout(() => resolve(registry.updateStatus(result.agent.id, 'processing')), 10)), new Promise(resolve => setTimeout(() => resolve(registry.updateActivity(result.agent.id, { processingTime: 100 })), 20)) ]; const results = await Promise.allSettled(promises); // First should succeed, others may fail but shouldn't crash expect(results[0].status).toBe('fulfilled'); }); }); describe('Agent Cleanup and Despawning', () => { test('should cleanup agent through registry', async () => { const config = { name: 'cleanup-test-agent', agentName: 'cleanup-test-agent', purpose: 'Testing cleanup functionality', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); // Verify agent exists expect(registry.getAgent(result.agent.id)).toBeDefined(); expect(await fs.pathExists(result.agent.path)).toBe(true); // Cleanup agent await registry.unregister(result.agent.id, { cleanup: true }); // Verify agent is removed expect(registry.getAgent(result.agent.id)).toBeUndefined(); }); test('should handle cleanup of non-existent agent', async () => { await expect(registry.unregister('non-existent-agent')).rejects.toThrow('not found'); }); }); describe('Lifecycle Management', () => { test('should automatically cleanup idle agents', async () => { const config = { name: 'idle-cleanup-agent', agentName: 'idle-cleanup-agent', purpose: 'Testing idle cleanup', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); // Wait for idle timeout await new Promise(resolve => setTimeout(resolve, 1200)); // Run cleanup const cleanupResults = await lifecycle.runCleanup(); // Agent should be marked for cleanup due to idle timeout const cleanedAgent = cleanupResults.find(r => r.agentId === result.agent.id); expect(cleanedAgent).toBeDefined(); }, 10000); test('should handle health checks', async () => { const config = { name: 'health-check-agent', agentName: 'health-check-agent', purpose: 'Testing health checks', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); // Run health check const healthResult = await registry.checkHealth(result.agent.id); expect(healthResult.agentId).toBe(result.agent.id); expect(healthResult.status).toBeDefined(); expect(healthResult.timestamp).toBeDefined(); }); }); describe('Resource Management', () => { test('should track resource usage', async () => { const config = { name: 'resource-tracking-agent', agentName: 'resource-tracking-agent', purpose: 'Testing resource tracking', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); // Update activity with resource usage await registry.updateActivity(result.agent.id, { processingTime: 500, success: true, resources: { memoryUsage: 1024 * 1024, // 1MB cpuUsage: 25, diskUsage: 512 * 1024 // 512KB } }); const agent = registry.getAgent(result.agent.id); expect(agent.resources.memoryUsage).toBe(1024 * 1024); expect(agent.performance.avgResponseTime).toBe(500); expect(agent.performance.successRate).toBe(1.0); }); test('should cleanup based on resource policies', async () => { const config = { name: 'resource-policy-agent', agentName: 'resource-policy-agent', purpose: 'Testing resource-based cleanup', template: 'base', testType: 'integration' }; const result = await spawner.spawn(config); await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); // Update with high resource usage await registry.updateActivity(result.agent.id, { resources: { memoryUsage: 200 * 1024 * 1024, // 200MB - exceeds limit cpuUsage: 90 // Exceeds limit } }); // Run cleanup const cleanupResults = await lifecycle.runCleanup(); // Should be flagged for high resource usage const flaggedAgent = cleanupResults.find(r => r.agentId === result.agent.id); expect(flaggedAgent).toBeDefined(); }); }); describe('Command Interface Integration', () => { test('should spawn agent through command interface', async () => { const result = await spawnCommand.execute('quick base "Command interface test"'); expect(result.success).toBe(true); expect(result.agent).toBeDefined(); expect(result.invocation).toBeDefined(); }); test('should list templates through command interface', async () => { const result = await spawnCommand.execute('list-templates'); expect(result.success).toBe(true); expect(result.templates).toBeDefined(); expect(Array.isArray(result.templates)).toBe(true); expect(result.templates.length).toBeGreaterThan(0); }); test('should get status through command interface', async () => { const result = await spawnCommand.execute('status'); expect(result.success).toBe(true); expect(result.stats).toBeDefined(); expect(result.stats.registry).toBeDefined(); expect(result.stats.spawner).toBeDefined(); expect(result.stats.lifecycle).toBeDefined(); }); test('should handle help command', async () => { const result = await spawnCommand.execute('help'); expect(result.success).toBe(true); expect(result.type).toBe('help'); }); }); describe('Performance and Scalability', () => { test('should handle burst spawning within limits', async () => { const startTime = Date.now(); const configs = Array.from({ length: 10 }, (_, i) => ({ name: `burst-agent-${i}`, agentName: `burst-agent-${i}`, purpose: `Burst test agent ${i}`, template: 'base', testType: 'integration' })); const promises = configs.map(config => spawner.spawn(config)); const results = await Promise.allSettled(promises); const duration = Date.now() - startTime; const successful = results.filter(r => r.status === 'fulfilled'); expect(successful.length).toBe(10); expect(duration).toBeLessThan(5000); // Should complete within 5 seconds }, 10000); test('should maintain registry consistency under load', async () => { const agentCount = 20; const operations = []; // Create multiple agents for (let i = 0; i < agentCount; i++) { operations.push(spawner.spawn({ name: `load-test-agent-${i}`, agentName: `load-test-agent-${i}`, purpose: `Load testing ${i}`, template: 'base', testType: 'integration' })); } const spawnResults = await Promise.allSettled(operations); const successful = spawnResults.filter(r => r.status === 'fulfilled'); // Register all spawned agents const registrations = successful.map(async (result) => { const agent = result.value.agent; return registry.register({ id: agent.id, name: agent.name, template: agent.template, path: agent.path, metadata: agent.metadata }); }); await Promise.allSettled(registrations); // Verify registry consistency const allAgents = registry.query(); expect(allAgents.length).toBe(successful.length); // Verify all agents have unique IDs const ids = allAgents.map(a => a.id); const uniqueIds = new Set(ids); expect(uniqueIds.size).toBe(allAgents.length); }, 15000); }); describe('Edge Cases and Error Recovery', () => { test('should recover from corrupted agent file', async () => { const config = { name: 'corruption-test-agent', agentName: 'corruption-test-agent', purpose: 'Testing corruption recovery', template: 'base', testType: 'integration' // Add missing parameter }; const result = await spawner.spawn(config); // Corrupt the agent file await fs.writeFile(result.agent.path, 'corrupted content'); // Health check should detect the issue await registry.register({ id: result.agent.id, name: result.agent.name, template: result.agent.template, path: result.agent.path, metadata: result.agent.metadata }); const healthResult = await registry.checkHealth(result.agent.id); expect(healthResult.status).toBe('unhealthy'); }); test('should handle spawner shutdown gracefully', async () => { const config = { name: 'shutdown-test-agent', agentName: 'shutdown-test-agent', purpose: 'Testing shutdown handling', template: 'base', testType: 'integration' // Add missing parameter }; // Spawn agent const result = await spawner.spawn(config); expect(result.success).toBe(true); // Verify statistics are available const stats = spawner.getStatistics(); expect(stats.totalSpawns).toBeGreaterThan(0); }); }); }); describe('Integration Tests', () => { test('should integrate all systems for complete workflow', async () => { const testDir = path.join(__dirname, 'temp-integration-test'); await fs.ensureDir(testDir); try { // Initialize complete system const config = { agentsDir: path.join(testDir, 'agents'), testsDir: path.join(testDir, 'tests'), archiveDir: path.join(testDir, 'archive'), registryDir: path.join(testDir, 'state'), templatesDir: path.join(testDir, 'templates') }; const system = { templateSystem: new AgentTemplateSystem({ templatesDir: config.templatesDir }), spawner: new AgentSpawner(config), registry: new AgentRegistry({ registryDir: config.registryDir }), }; system.lifecycle = new AgentLifecycleManager(system.spawner, system.registry); system.command = new AgentSpawnCommand(system); // Initialize all components await system.templateSystem.initialize(); await system.spawner.initialize(); await system.registry.initialize(); await system.lifecycle.initialize(); await system.command.initialize(); // Complete workflow: spawn -> register -> monitor -> cleanup const spawnResult = await system.command.execute('quick base "Integration test workflow"'); expect(spawnResult.success).toBe(true); // Register the spawned agent in the registry await system.registry.register({ id: spawnResult.agent.id, name: spawnResult.agent.name, template: spawnResult.agent.template, path: spawnResult.agent.path, metadata: spawnResult.agent.metadata }); // Verify agent is registered const agent = system.registry.getAgent(spawnResult.agent.id); expect(agent).toBeDefined(); // Run health check const healthResult = await system.registry.checkHealth(spawnResult.agent.id); expect(healthResult.status).toBeDefined(); // Update activity await system.registry.updateActivity(spawnResult.agent.id, { processingTime: 200, success: true }); // Get system status const statusResult = await system.command.execute('status'); expect(statusResult.success).toBe(true); expect(statusResult.stats.registry.currentStats.totalAgents).toBeGreaterThan(0); // Cleanup await system.lifecycle.shutdown(); await system.registry.shutdown(); } finally { await fs.remove(testDir); } }, 15000); });