@hanzo/dev 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +25 -0
- package/dist/cli/dev.js +8202 -553
- package/jest.config.js +30 -0
- package/package.json +13 -1
- package/src/cli/dev.ts +456 -106
- package/src/lib/agent-loop.ts +552 -0
- package/src/lib/code-act-agent.ts +378 -0
- package/src/lib/config.ts +163 -0
- package/src/lib/editor.ts +368 -0
- package/src/lib/function-calling.ts +318 -0
- package/src/lib/mcp-client.ts +259 -0
- package/src/lib/peer-agent-network.ts +584 -0
- package/src/lib/unified-workspace.ts +435 -0
- package/tests/browser-integration.test.ts +242 -0
- package/tests/code-act-agent.test.ts +305 -0
- package/tests/editor.test.ts +223 -0
- package/tests/mcp-client.test.ts +238 -0
- package/tests/peer-agent-network.test.ts +340 -0
- package/tests/setup.ts +25 -0
- package/tests/swe-bench.test.ts +357 -0
- package/tsconfig.json +13 -15
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { MCPClient, MCPSession, MCPServerConfig } from '../src/lib/mcp-client';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import * as child_process from 'child_process';
|
|
5
|
+
|
|
6
|
+
// Mock child_process
|
|
7
|
+
jest.mock('child_process');
|
|
8
|
+
|
|
9
|
+
describe('MCPClient', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
let mockProcess: any;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
client = new MCPClient();
|
|
15
|
+
|
|
16
|
+
// Mock spawn to return a fake process
|
|
17
|
+
mockProcess = new EventEmitter();
|
|
18
|
+
mockProcess.stdin = { write: jest.fn() };
|
|
19
|
+
mockProcess.stdout = new EventEmitter();
|
|
20
|
+
mockProcess.stderr = new EventEmitter();
|
|
21
|
+
mockProcess.kill = jest.fn();
|
|
22
|
+
|
|
23
|
+
(child_process.spawn as jest.Mock).mockReturnValue(mockProcess);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('stdio transport', () => {
|
|
31
|
+
test('should connect to MCP server via stdio', async () => {
|
|
32
|
+
const config: MCPServerConfig = {
|
|
33
|
+
name: 'test-server',
|
|
34
|
+
transport: 'stdio',
|
|
35
|
+
command: 'test-mcp-server',
|
|
36
|
+
args: ['--test']
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Start connection in background
|
|
40
|
+
const connectPromise = client.connect(config);
|
|
41
|
+
|
|
42
|
+
// Simulate server sending initialization response
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
mockProcess.stdout.emit('data', JSON.stringify({
|
|
45
|
+
jsonrpc: '2.0',
|
|
46
|
+
id: 1,
|
|
47
|
+
result: {
|
|
48
|
+
protocolVersion: '1.0',
|
|
49
|
+
serverInfo: { name: 'test-server', version: '1.0.0' }
|
|
50
|
+
}
|
|
51
|
+
}) + '\n');
|
|
52
|
+
|
|
53
|
+
// Simulate tools list response
|
|
54
|
+
mockProcess.stdout.emit('data', JSON.stringify({
|
|
55
|
+
jsonrpc: '2.0',
|
|
56
|
+
id: 2,
|
|
57
|
+
result: {
|
|
58
|
+
tools: [
|
|
59
|
+
{
|
|
60
|
+
name: 'test_tool',
|
|
61
|
+
description: 'A test tool',
|
|
62
|
+
parameters: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
input: { type: 'string' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}) + '\n');
|
|
72
|
+
}, 10);
|
|
73
|
+
|
|
74
|
+
const session = await connectPromise;
|
|
75
|
+
expect(session).toBeDefined();
|
|
76
|
+
expect(session.tools).toHaveLength(1);
|
|
77
|
+
expect(session.tools[0].name).toBe('test_tool');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should handle server errors', async () => {
|
|
81
|
+
const config: MCPServerConfig = {
|
|
82
|
+
name: 'error-server',
|
|
83
|
+
transport: 'stdio',
|
|
84
|
+
command: 'failing-server'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const connectPromise = client.connect(config);
|
|
88
|
+
|
|
89
|
+
// Simulate process error
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
mockProcess.emit('error', new Error('Failed to start'));
|
|
92
|
+
}, 10);
|
|
93
|
+
|
|
94
|
+
await expect(connectPromise).rejects.toThrow('Failed to start');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('tool calling', () => {
|
|
99
|
+
test('should call tool on MCP server', async () => {
|
|
100
|
+
const session: MCPSession = {
|
|
101
|
+
serverName: 'test-server',
|
|
102
|
+
tools: [{
|
|
103
|
+
name: 'echo',
|
|
104
|
+
description: 'Echo input',
|
|
105
|
+
parameters: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
message: { type: 'string' }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}],
|
|
112
|
+
prompts: [],
|
|
113
|
+
resources: []
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Mock session in client
|
|
117
|
+
(client as any).sessions.set('test-server', session);
|
|
118
|
+
(client as any).processes.set('test-server', mockProcess);
|
|
119
|
+
|
|
120
|
+
// Start tool call
|
|
121
|
+
const callPromise = client.callTool('test-server', 'echo', { message: 'Hello' });
|
|
122
|
+
|
|
123
|
+
// Simulate server response
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
// Find the request that was sent
|
|
126
|
+
const writeCall = mockProcess.stdin.write.mock.calls[0];
|
|
127
|
+
const request = JSON.parse(writeCall[0]);
|
|
128
|
+
|
|
129
|
+
// Send response with same ID
|
|
130
|
+
mockProcess.stdout.emit('data', JSON.stringify({
|
|
131
|
+
jsonrpc: '2.0',
|
|
132
|
+
id: request.id,
|
|
133
|
+
result: {
|
|
134
|
+
output: 'Echo: Hello'
|
|
135
|
+
}
|
|
136
|
+
}) + '\n');
|
|
137
|
+
}, 10);
|
|
138
|
+
|
|
139
|
+
const result = await callPromise;
|
|
140
|
+
expect(result.output).toBe('Echo: Hello');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('session management', () => {
|
|
145
|
+
test('should list connected sessions', async () => {
|
|
146
|
+
// Mock two sessions
|
|
147
|
+
(client as any).sessions.set('server1', {
|
|
148
|
+
serverName: 'server1',
|
|
149
|
+
tools: [],
|
|
150
|
+
prompts: [],
|
|
151
|
+
resources: []
|
|
152
|
+
});
|
|
153
|
+
(client as any).sessions.set('server2', {
|
|
154
|
+
serverName: 'server2',
|
|
155
|
+
tools: [],
|
|
156
|
+
prompts: [],
|
|
157
|
+
resources: []
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const sessions = client.listSessions();
|
|
161
|
+
expect(sessions).toHaveLength(2);
|
|
162
|
+
expect(sessions.map(s => s.serverName)).toContain('server1');
|
|
163
|
+
expect(sessions.map(s => s.serverName)).toContain('server2');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('should disconnect from server', async () => {
|
|
167
|
+
const serverName = 'test-server';
|
|
168
|
+
|
|
169
|
+
// Mock session and process
|
|
170
|
+
(client as any).sessions.set(serverName, {
|
|
171
|
+
serverName,
|
|
172
|
+
tools: [],
|
|
173
|
+
prompts: [],
|
|
174
|
+
resources: []
|
|
175
|
+
});
|
|
176
|
+
(client as any).processes.set(serverName, mockProcess);
|
|
177
|
+
|
|
178
|
+
await client.disconnect(serverName);
|
|
179
|
+
|
|
180
|
+
expect(mockProcess.kill).toHaveBeenCalled();
|
|
181
|
+
expect(client.listSessions()).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('error handling', () => {
|
|
186
|
+
test('should handle JSON-RPC errors', async () => {
|
|
187
|
+
const session: MCPSession = {
|
|
188
|
+
serverName: 'test-server',
|
|
189
|
+
tools: [{
|
|
190
|
+
name: 'failing_tool',
|
|
191
|
+
description: 'A tool that fails',
|
|
192
|
+
parameters: { type: 'object' }
|
|
193
|
+
}],
|
|
194
|
+
prompts: [],
|
|
195
|
+
resources: []
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
(client as any).sessions.set('test-server', session);
|
|
199
|
+
(client as any).processes.set('test-server', mockProcess);
|
|
200
|
+
|
|
201
|
+
const callPromise = client.callTool('test-server', 'failing_tool', {});
|
|
202
|
+
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
const writeCall = mockProcess.stdin.write.mock.calls[0];
|
|
205
|
+
const request = JSON.parse(writeCall[0]);
|
|
206
|
+
|
|
207
|
+
// Send error response
|
|
208
|
+
mockProcess.stdout.emit('data', JSON.stringify({
|
|
209
|
+
jsonrpc: '2.0',
|
|
210
|
+
id: request.id,
|
|
211
|
+
error: {
|
|
212
|
+
code: -32601,
|
|
213
|
+
message: 'Method not found'
|
|
214
|
+
}
|
|
215
|
+
}) + '\n');
|
|
216
|
+
}, 10);
|
|
217
|
+
|
|
218
|
+
await expect(callPromise).rejects.toThrow('Method not found');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should handle malformed responses', async () => {
|
|
222
|
+
const config: MCPServerConfig = {
|
|
223
|
+
name: 'malformed-server',
|
|
224
|
+
transport: 'stdio',
|
|
225
|
+
command: 'test-server'
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const connectPromise = client.connect(config);
|
|
229
|
+
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
// Send malformed JSON
|
|
232
|
+
mockProcess.stdout.emit('data', 'not valid json\n');
|
|
233
|
+
}, 10);
|
|
234
|
+
|
|
235
|
+
await expect(connectPromise).rejects.toThrow();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { PeerAgentNetwork, AgentConfig } from '../src/lib/peer-agent-network';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('PeerAgentNetwork', () => {
|
|
8
|
+
let network: PeerAgentNetwork;
|
|
9
|
+
let testDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
network = new PeerAgentNetwork();
|
|
13
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'peer-network-test-'));
|
|
14
|
+
|
|
15
|
+
// Create test file structure
|
|
16
|
+
fs.mkdirSync(path.join(testDir, 'src'));
|
|
17
|
+
fs.mkdirSync(path.join(testDir, 'tests'));
|
|
18
|
+
fs.writeFileSync(path.join(testDir, 'src', 'index.js'), 'console.log("Hello");');
|
|
19
|
+
fs.writeFileSync(path.join(testDir, 'src', 'utils.js'), 'export function util() {}');
|
|
20
|
+
fs.writeFileSync(path.join(testDir, 'tests', 'index.test.js'), 'test("sample", () => {});');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Clean up
|
|
25
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('agent spawning', () => {
|
|
29
|
+
test('should spawn agent with configuration', async () => {
|
|
30
|
+
const config: AgentConfig = {
|
|
31
|
+
id: 'test-agent',
|
|
32
|
+
name: 'Test Agent',
|
|
33
|
+
type: 'claude-code',
|
|
34
|
+
responsibility: 'Test file processing',
|
|
35
|
+
tools: ['edit_file', 'view_file']
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await network.spawnAgent(config);
|
|
39
|
+
|
|
40
|
+
const agents = network.getActiveAgents();
|
|
41
|
+
expect(agents).toHaveLength(1);
|
|
42
|
+
expect(agents[0].id).toBe('test-agent');
|
|
43
|
+
expect(agents[0].status).toBe('active');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should prevent duplicate agent IDs', async () => {
|
|
47
|
+
const config: AgentConfig = {
|
|
48
|
+
id: 'duplicate',
|
|
49
|
+
name: 'Agent 1',
|
|
50
|
+
type: 'claude-code'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await network.spawnAgent(config);
|
|
54
|
+
|
|
55
|
+
// Try to spawn with same ID
|
|
56
|
+
await expect(network.spawnAgent({
|
|
57
|
+
...config,
|
|
58
|
+
name: 'Agent 2'
|
|
59
|
+
})).rejects.toThrow('already exists');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('codebase agent spawning', () => {
|
|
64
|
+
test('should spawn one agent per file', async () => {
|
|
65
|
+
await network.spawnAgentsForCodebase(testDir, 'claude-code', 'one-per-file');
|
|
66
|
+
|
|
67
|
+
const agents = network.getActiveAgents();
|
|
68
|
+
// Should have 3 agents (3 files)
|
|
69
|
+
expect(agents).toHaveLength(3);
|
|
70
|
+
|
|
71
|
+
// Check agent responsibilities
|
|
72
|
+
const responsibilities = agents.map(a => a.responsibility);
|
|
73
|
+
expect(responsibilities).toContain(expect.stringContaining('src/index.js'));
|
|
74
|
+
expect(responsibilities).toContain(expect.stringContaining('src/utils.js'));
|
|
75
|
+
expect(responsibilities).toContain(expect.stringContaining('tests/index.test.js'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should spawn one agent per directory', async () => {
|
|
79
|
+
await network.spawnAgentsForCodebase(testDir, 'claude-code', 'one-per-directory');
|
|
80
|
+
|
|
81
|
+
const agents = network.getActiveAgents();
|
|
82
|
+
// Should have 2 agents (src and tests directories)
|
|
83
|
+
expect(agents).toHaveLength(2);
|
|
84
|
+
|
|
85
|
+
const responsibilities = agents.map(a => a.responsibility);
|
|
86
|
+
expect(responsibilities).toContain(expect.stringContaining('src'));
|
|
87
|
+
expect(responsibilities).toContain(expect.stringContaining('tests'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should respect file patterns', async () => {
|
|
91
|
+
await network.spawnAgentsForCodebase(
|
|
92
|
+
testDir,
|
|
93
|
+
'claude-code',
|
|
94
|
+
'one-per-file',
|
|
95
|
+
['**/*.test.js'] // Only test files
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const agents = network.getActiveAgents();
|
|
99
|
+
expect(agents).toHaveLength(1);
|
|
100
|
+
expect(agents[0].responsibility).toContain('tests/index.test.js');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('agent communication', () => {
|
|
105
|
+
test('should enable agent-to-agent messaging', async () => {
|
|
106
|
+
// Spawn two agents
|
|
107
|
+
await network.spawnAgent({
|
|
108
|
+
id: 'agent1',
|
|
109
|
+
name: 'Agent 1',
|
|
110
|
+
type: 'claude-code'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await network.spawnAgent({
|
|
114
|
+
id: 'agent2',
|
|
115
|
+
name: 'Agent 2',
|
|
116
|
+
type: 'claude-code'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Send message from agent1 to agent2
|
|
120
|
+
const response = await network.sendMessage('agent1', 'agent2', {
|
|
121
|
+
type: 'query',
|
|
122
|
+
content: 'What files are you working on?'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(response).toBeDefined();
|
|
126
|
+
expect(response.from).toBe('agent2');
|
|
127
|
+
expect(response.to).toBe('agent1');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should broadcast messages to all agents', async () => {
|
|
131
|
+
// Spawn three agents
|
|
132
|
+
for (let i = 1; i <= 3; i++) {
|
|
133
|
+
await network.spawnAgent({
|
|
134
|
+
id: `agent${i}`,
|
|
135
|
+
name: `Agent ${i}`,
|
|
136
|
+
type: 'claude-code'
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const responses = await network.broadcast('agent1', {
|
|
141
|
+
type: 'announcement',
|
|
142
|
+
content: 'Starting code review'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(responses).toHaveLength(2); // Response from agent2 and agent3
|
|
146
|
+
expect(responses.every(r => r.from !== 'agent1')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('MCP tool exposure', () => {
|
|
151
|
+
test('should expose agents as MCP tools to each other', async () => {
|
|
152
|
+
await network.spawnAgent({
|
|
153
|
+
id: 'file-agent',
|
|
154
|
+
name: 'File Agent',
|
|
155
|
+
type: 'claude-code',
|
|
156
|
+
responsibility: 'File operations',
|
|
157
|
+
tools: ['edit_file', 'create_file']
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await network.spawnAgent({
|
|
161
|
+
id: 'test-agent',
|
|
162
|
+
name: 'Test Agent',
|
|
163
|
+
type: 'aider',
|
|
164
|
+
responsibility: 'Test writing'
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Check that each agent can see the other as a tool
|
|
168
|
+
const fileAgentTools = await network.getAgentTools('file-agent');
|
|
169
|
+
expect(fileAgentTools).toContain(expect.objectContaining({
|
|
170
|
+
name: 'ask_test_agent',
|
|
171
|
+
description: expect.stringContaining('Test Agent')
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
const testAgentTools = await network.getAgentTools('test-agent');
|
|
175
|
+
expect(testAgentTools).toContain(expect.objectContaining({
|
|
176
|
+
name: 'ask_file_agent',
|
|
177
|
+
description: expect.stringContaining('File Agent')
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should allow recursive agent calls via MCP', async () => {
|
|
182
|
+
// Set up agents
|
|
183
|
+
await network.spawnAgent({
|
|
184
|
+
id: 'coordinator',
|
|
185
|
+
name: 'Coordinator',
|
|
186
|
+
type: 'claude-code'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await network.spawnAgent({
|
|
190
|
+
id: 'worker1',
|
|
191
|
+
name: 'Worker 1',
|
|
192
|
+
type: 'claude-code'
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await network.spawnAgent({
|
|
196
|
+
id: 'worker2',
|
|
197
|
+
name: 'Worker 2',
|
|
198
|
+
type: 'claude-code'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Coordinator delegates to workers
|
|
202
|
+
const result = await network.callAgentTool('coordinator', 'delegate_to_worker1', {
|
|
203
|
+
task: 'Process data'
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result).toBeDefined();
|
|
207
|
+
expect(result.success).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('task coordination', () => {
|
|
212
|
+
test('should coordinate parallel tasks across agents', async () => {
|
|
213
|
+
// Create a task that can be parallelized
|
|
214
|
+
const files = [
|
|
215
|
+
'file1.js',
|
|
216
|
+
'file2.js',
|
|
217
|
+
'file3.js',
|
|
218
|
+
'file4.js'
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// Spawn agents for parallel processing
|
|
222
|
+
const agents = await network.spawnAgentsForTask(
|
|
223
|
+
'Process multiple files',
|
|
224
|
+
files.map(f => ({
|
|
225
|
+
subtask: `Process ${f}`,
|
|
226
|
+
data: { file: f }
|
|
227
|
+
}))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(agents).toHaveLength(4);
|
|
231
|
+
|
|
232
|
+
// Execute all tasks in parallel
|
|
233
|
+
const results = await network.executeParallelTasks(
|
|
234
|
+
agents.map(a => ({
|
|
235
|
+
agentId: a.id,
|
|
236
|
+
task: a.config.responsibility!
|
|
237
|
+
}))
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(results).toHaveLength(4);
|
|
241
|
+
expect(results.every(r => r.status === 'completed')).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('should handle agent failures gracefully', async () => {
|
|
245
|
+
await network.spawnAgent({
|
|
246
|
+
id: 'failing-agent',
|
|
247
|
+
name: 'Failing Agent',
|
|
248
|
+
type: 'claude-code'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Make agent fail
|
|
252
|
+
(network as any).agents.get('failing-agent').status = 'error';
|
|
253
|
+
|
|
254
|
+
const agents = network.getActiveAgents();
|
|
255
|
+
expect(agents).toHaveLength(0); // Failed agents not in active list
|
|
256
|
+
|
|
257
|
+
const allAgents = network.getAllAgents();
|
|
258
|
+
expect(allAgents).toHaveLength(1);
|
|
259
|
+
expect(allAgents[0].status).toBe('error');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('swarm optimization', () => {
|
|
264
|
+
test('should optimize agent allocation based on workload', async () => {
|
|
265
|
+
// Create initial agents
|
|
266
|
+
for (let i = 1; i <= 3; i++) {
|
|
267
|
+
await network.spawnAgent({
|
|
268
|
+
id: `agent${i}`,
|
|
269
|
+
name: `Agent ${i}`,
|
|
270
|
+
type: 'claude-code'
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Simulate workload
|
|
275
|
+
const metrics = {
|
|
276
|
+
agent1: { tasksCompleted: 10, avgTime: 2.5 },
|
|
277
|
+
agent2: { tasksCompleted: 5, avgTime: 5.0 },
|
|
278
|
+
agent3: { tasksCompleted: 8, avgTime: 3.0 }
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const optimization = network.optimizeSwarm(metrics);
|
|
282
|
+
|
|
283
|
+
// Should recommend spawning more agents like agent1 (best performance)
|
|
284
|
+
expect(optimization.recommendations).toContain(
|
|
285
|
+
expect.stringContaining('agent1')
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('should monitor swarm health', async () => {
|
|
290
|
+
// Spawn multiple agents
|
|
291
|
+
for (let i = 1; i <= 5; i++) {
|
|
292
|
+
await network.spawnAgent({
|
|
293
|
+
id: `agent${i}`,
|
|
294
|
+
name: `Agent ${i}`,
|
|
295
|
+
type: i % 2 === 0 ? 'aider' : 'claude-code'
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const health = network.getSwarmHealth();
|
|
300
|
+
|
|
301
|
+
expect(health.totalAgents).toBe(5);
|
|
302
|
+
expect(health.activeAgents).toBe(5);
|
|
303
|
+
expect(health.agentTypes).toContain('claude-code');
|
|
304
|
+
expect(health.agentTypes).toContain('aider');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('cleanup and lifecycle', () => {
|
|
309
|
+
test('should terminate individual agents', async () => {
|
|
310
|
+
await network.spawnAgent({
|
|
311
|
+
id: 'temp-agent',
|
|
312
|
+
name: 'Temporary Agent',
|
|
313
|
+
type: 'claude-code'
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(network.getActiveAgents()).toHaveLength(1);
|
|
317
|
+
|
|
318
|
+
await network.terminateAgent('temp-agent');
|
|
319
|
+
|
|
320
|
+
expect(network.getActiveAgents()).toHaveLength(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('should terminate all agents on shutdown', async () => {
|
|
324
|
+
// Spawn multiple agents
|
|
325
|
+
for (let i = 1; i <= 3; i++) {
|
|
326
|
+
await network.spawnAgent({
|
|
327
|
+
id: `agent${i}`,
|
|
328
|
+
name: `Agent ${i}`,
|
|
329
|
+
type: 'claude-code'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
expect(network.getActiveAgents()).toHaveLength(3);
|
|
334
|
+
|
|
335
|
+
await network.shutdown();
|
|
336
|
+
|
|
337
|
+
expect(network.getActiveAgents()).toHaveLength(0);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
// Set up test environment
|
|
4
|
+
process.env.NODE_ENV = 'test';
|
|
5
|
+
|
|
6
|
+
// Mock console methods to reduce noise in tests
|
|
7
|
+
global.console = {
|
|
8
|
+
...console,
|
|
9
|
+
log: jest.fn(),
|
|
10
|
+
debug: jest.fn(),
|
|
11
|
+
info: jest.fn(),
|
|
12
|
+
warn: jest.fn(),
|
|
13
|
+
error: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Mock fetch globally
|
|
17
|
+
global.fetch = jest.fn();
|
|
18
|
+
|
|
19
|
+
// Set test timeout
|
|
20
|
+
jest.setTimeout(30000);
|
|
21
|
+
|
|
22
|
+
// Clean up after each test
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
});
|