@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.
@@ -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
+ });