- Add --swarm <count> capability to launch up to 100 agents in parallel - Support multiple providers: --claude, --openai, --gemini, --grok, --local - Implement lazy agent spawning for efficient resource usage - Add automatic authentication for all providers - Create parallel file processing with progress tracking - Migrate all tests from Jest to Vitest - Add comprehensive test coverage for swarm functionality - Support idiomatic usage: dev --claude --swarm 5 -p 'edit files...' - Version bump to 2.1.0
340 lines
10 KiB
TypeScript
340 lines
10 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
import { PeerAgentNetwork, AgentConfig } from '../src/lib/peer-agent-network';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
describe('PeerAgentNetwork', () => {
|
|
let network: PeerAgentNetwork;
|
|
let testDir: string;
|
|
|
|
beforeEach(() => {
|
|
network = new PeerAgentNetwork();
|
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'peer-network-test-'));
|
|
|
|
// Create test file structure
|
|
fs.mkdirSync(path.join(testDir, 'src'));
|
|
fs.mkdirSync(path.join(testDir, 'tests'));
|
|
fs.writeFileSync(path.join(testDir, 'src', 'index.js'), 'console.log("Hello");');
|
|
fs.writeFileSync(path.join(testDir, 'src', 'utils.js'), 'export function util() {}');
|
|
fs.writeFileSync(path.join(testDir, 'tests', 'index.test.js'), 'test("sample", () => {});');
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('agent spawning', () => {
|
|
test('should spawn agent with configuration', async () => {
|
|
const config: AgentConfig = {
|
|
id: 'test-agent',
|
|
name: 'Test Agent',
|
|
type: 'claude-code',
|
|
responsibility: 'Test file processing',
|
|
tools: ['edit_file', 'view_file']
|
|
};
|
|
|
|
await network.spawnAgent(config);
|
|
|
|
const agents = network.getActiveAgents();
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0].id).toBe('test-agent');
|
|
expect(agents[0].status).toBe('active');
|
|
});
|
|
|
|
test('should prevent duplicate agent IDs', async () => {
|
|
const config: AgentConfig = {
|
|
id: 'duplicate',
|
|
name: 'Agent 1',
|
|
type: 'claude-code'
|
|
};
|
|
|
|
await network.spawnAgent(config);
|
|
|
|
// Try to spawn with same ID
|
|
await expect(network.spawnAgent({
|
|
...config,
|
|
name: 'Agent 2'
|
|
})).rejects.toThrow('already exists');
|
|
});
|
|
});
|
|
|
|
describe('codebase agent spawning', () => {
|
|
test('should spawn one agent per file', async () => {
|
|
await network.spawnAgentsForCodebase(testDir, 'claude-code', 'one-per-file');
|
|
|
|
const agents = network.getActiveAgents();
|
|
// Should have 3 agents (3 files)
|
|
expect(agents).toHaveLength(3);
|
|
|
|
// Check agent responsibilities
|
|
const responsibilities = agents.map(a => a.responsibility);
|
|
expect(responsibilities).toContain(expect.stringContaining('src/index.js'));
|
|
expect(responsibilities).toContain(expect.stringContaining('src/utils.js'));
|
|
expect(responsibilities).toContain(expect.stringContaining('tests/index.test.js'));
|
|
});
|
|
|
|
test('should spawn one agent per directory', async () => {
|
|
await network.spawnAgentsForCodebase(testDir, 'claude-code', 'one-per-directory');
|
|
|
|
const agents = network.getActiveAgents();
|
|
// Should have 2 agents (src and tests directories)
|
|
expect(agents).toHaveLength(2);
|
|
|
|
const responsibilities = agents.map(a => a.responsibility);
|
|
expect(responsibilities).toContain(expect.stringContaining('src'));
|
|
expect(responsibilities).toContain(expect.stringContaining('tests'));
|
|
});
|
|
|
|
test('should respect file patterns', async () => {
|
|
await network.spawnAgentsForCodebase(
|
|
testDir,
|
|
'claude-code',
|
|
'one-per-file',
|
|
['**/*.test.js'] // Only test files
|
|
);
|
|
|
|
const agents = network.getActiveAgents();
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0].responsibility).toContain('tests/index.test.js');
|
|
});
|
|
});
|
|
|
|
describe('agent communication', () => {
|
|
test('should enable agent-to-agent messaging', async () => {
|
|
// Spawn two agents
|
|
await network.spawnAgent({
|
|
id: 'agent1',
|
|
name: 'Agent 1',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
await network.spawnAgent({
|
|
id: 'agent2',
|
|
name: 'Agent 2',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
// Send message from agent1 to agent2
|
|
const response = await network.sendMessage('agent1', 'agent2', {
|
|
type: 'query',
|
|
content: 'What files are you working on?'
|
|
});
|
|
|
|
expect(response).toBeDefined();
|
|
expect(response.from).toBe('agent2');
|
|
expect(response.to).toBe('agent1');
|
|
});
|
|
|
|
test('should broadcast messages to all agents', async () => {
|
|
// Spawn three agents
|
|
for (let i = 1; i <= 3; i++) {
|
|
await network.spawnAgent({
|
|
id: `agent${i}`,
|
|
name: `Agent ${i}`,
|
|
type: 'claude-code'
|
|
});
|
|
}
|
|
|
|
const responses = await network.broadcast('agent1', {
|
|
type: 'announcement',
|
|
content: 'Starting code review'
|
|
});
|
|
|
|
expect(responses).toHaveLength(2); // Response from agent2 and agent3
|
|
expect(responses.every(r => r.from !== 'agent1')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('MCP tool exposure', () => {
|
|
test('should expose agents as MCP tools to each other', async () => {
|
|
await network.spawnAgent({
|
|
id: 'file-agent',
|
|
name: 'File Agent',
|
|
type: 'claude-code',
|
|
responsibility: 'File operations',
|
|
tools: ['edit_file', 'create_file']
|
|
});
|
|
|
|
await network.spawnAgent({
|
|
id: 'test-agent',
|
|
name: 'Test Agent',
|
|
type: 'aider',
|
|
responsibility: 'Test writing'
|
|
});
|
|
|
|
// Check that each agent can see the other as a tool
|
|
const fileAgentTools = await network.getAgentTools('file-agent');
|
|
expect(fileAgentTools).toContain(expect.objectContaining({
|
|
name: 'ask_test_agent',
|
|
description: expect.stringContaining('Test Agent')
|
|
}));
|
|
|
|
const testAgentTools = await network.getAgentTools('test-agent');
|
|
expect(testAgentTools).toContain(expect.objectContaining({
|
|
name: 'ask_file_agent',
|
|
description: expect.stringContaining('File Agent')
|
|
}));
|
|
});
|
|
|
|
test('should allow recursive agent calls via MCP', async () => {
|
|
// Set up agents
|
|
await network.spawnAgent({
|
|
id: 'coordinator',
|
|
name: 'Coordinator',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
await network.spawnAgent({
|
|
id: 'worker1',
|
|
name: 'Worker 1',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
await network.spawnAgent({
|
|
id: 'worker2',
|
|
name: 'Worker 2',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
// Coordinator delegates to workers
|
|
const result = await network.callAgentTool('coordinator', 'delegate_to_worker1', {
|
|
task: 'Process data'
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('task coordination', () => {
|
|
test('should coordinate parallel tasks across agents', async () => {
|
|
// Create a task that can be parallelized
|
|
const files = [
|
|
'file1.js',
|
|
'file2.js',
|
|
'file3.js',
|
|
'file4.js'
|
|
];
|
|
|
|
// Spawn agents for parallel processing
|
|
const agents = await network.spawnAgentsForTask(
|
|
'Process multiple files',
|
|
files.map(f => ({
|
|
subtask: `Process ${f}`,
|
|
data: { file: f }
|
|
}))
|
|
);
|
|
|
|
expect(agents).toHaveLength(4);
|
|
|
|
// Execute all tasks in parallel
|
|
const results = await network.executeParallelTasks(
|
|
agents.map(a => ({
|
|
agentId: a.id,
|
|
task: a.config.responsibility!
|
|
}))
|
|
);
|
|
|
|
expect(results).toHaveLength(4);
|
|
expect(results.every(r => r.status === 'completed')).toBe(true);
|
|
});
|
|
|
|
test('should handle agent failures gracefully', async () => {
|
|
await network.spawnAgent({
|
|
id: 'failing-agent',
|
|
name: 'Failing Agent',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
// Make agent fail
|
|
(network as any).agents.get('failing-agent').status = 'error';
|
|
|
|
const agents = network.getActiveAgents();
|
|
expect(agents).toHaveLength(0); // Failed agents not in active list
|
|
|
|
const allAgents = network.getAllAgents();
|
|
expect(allAgents).toHaveLength(1);
|
|
expect(allAgents[0].status).toBe('error');
|
|
});
|
|
});
|
|
|
|
describe('swarm optimization', () => {
|
|
test('should optimize agent allocation based on workload', async () => {
|
|
// Create initial agents
|
|
for (let i = 1; i <= 3; i++) {
|
|
await network.spawnAgent({
|
|
id: `agent${i}`,
|
|
name: `Agent ${i}`,
|
|
type: 'claude-code'
|
|
});
|
|
}
|
|
|
|
// Simulate workload
|
|
const metrics = {
|
|
agent1: { tasksCompleted: 10, avgTime: 2.5 },
|
|
agent2: { tasksCompleted: 5, avgTime: 5.0 },
|
|
agent3: { tasksCompleted: 8, avgTime: 3.0 }
|
|
};
|
|
|
|
const optimization = network.optimizeSwarm(metrics);
|
|
|
|
// Should recommend spawning more agents like agent1 (best performance)
|
|
expect(optimization.recommendations).toContain(
|
|
expect.stringContaining('agent1')
|
|
);
|
|
});
|
|
|
|
test('should monitor swarm health', async () => {
|
|
// Spawn multiple agents
|
|
for (let i = 1; i <= 5; i++) {
|
|
await network.spawnAgent({
|
|
id: `agent${i}`,
|
|
name: `Agent ${i}`,
|
|
type: i % 2 === 0 ? 'aider' : 'claude-code'
|
|
});
|
|
}
|
|
|
|
const health = network.getSwarmHealth();
|
|
|
|
expect(health.totalAgents).toBe(5);
|
|
expect(health.activeAgents).toBe(5);
|
|
expect(health.agentTypes).toContain('claude-code');
|
|
expect(health.agentTypes).toContain('aider');
|
|
});
|
|
});
|
|
|
|
describe('cleanup and lifecycle', () => {
|
|
test('should terminate individual agents', async () => {
|
|
await network.spawnAgent({
|
|
id: 'temp-agent',
|
|
name: 'Temporary Agent',
|
|
type: 'claude-code'
|
|
});
|
|
|
|
expect(network.getActiveAgents()).toHaveLength(1);
|
|
|
|
await network.terminateAgent('temp-agent');
|
|
|
|
expect(network.getActiveAgents()).toHaveLength(0);
|
|
});
|
|
|
|
test('should terminate all agents on shutdown', async () => {
|
|
// Spawn multiple agents
|
|
for (let i = 1; i <= 3; i++) {
|
|
await network.spawnAgent({
|
|
id: `agent${i}`,
|
|
name: `Agent ${i}`,
|
|
type: 'claude-code'
|
|
});
|
|
}
|
|
|
|
expect(network.getActiveAgents()).toHaveLength(3);
|
|
|
|
await network.shutdown();
|
|
|
|
expect(network.getActiveAgents()).toHaveLength(0);
|
|
});
|
|
});
|
|
}); |