@hanzo/dev 1.2.0 → 2.1.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.json +24 -0
- package/README.md +359 -0
- package/dist/cli/dev.js +21724 -602
- package/package.json +19 -4
- package/src/cli/dev.ts +623 -106
- package/src/lib/agent-loop.ts +552 -0
- package/src/lib/benchmark-runner.ts +431 -0
- package/src/lib/code-act-agent.ts +378 -0
- package/src/lib/config.ts +163 -0
- package/src/lib/editor.ts +395 -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/swarm-runner.ts +379 -0
- package/src/lib/unified-workspace.ts +435 -0
- package/test-swarm/file1.js +6 -0
- package/test-swarm/file2.ts +12 -0
- package/test-swarm/file3.py +15 -0
- package/test-swarm/file4.md +13 -0
- package/test-swarm/file5.json +12 -0
- package/test-swarm-demo.sh +22 -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/fixtures/sample-code.js +13 -0
- package/tests/fixtures/sample-code.py +28 -0
- package/tests/fixtures/sample-code.ts +22 -0
- package/tests/mcp-client.test.ts +238 -0
- package/tests/peer-agent-network.test.ts +340 -0
- package/tests/swarm-runner.test.ts +301 -0
- package/tests/swe-bench.test.ts +357 -0
- package/tsconfig.json +13 -15
- package/vitest.config.ts +37 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { SwarmRunner, SwarmOptions } from '../src/lib/swarm-runner';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import * as child_process from 'child_process';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
|
|
10
|
+
// Mock modules
|
|
11
|
+
vi.mock('child_process');
|
|
12
|
+
vi.mock('glob');
|
|
13
|
+
vi.mock('ora', () => ({
|
|
14
|
+
default: () => ({
|
|
15
|
+
start: vi.fn().mockReturnThis(),
|
|
16
|
+
succeed: vi.fn().mockReturnThis(),
|
|
17
|
+
fail: vi.fn().mockReturnThis(),
|
|
18
|
+
stop: vi.fn().mockReturnThis()
|
|
19
|
+
})
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('SwarmRunner', () => {
|
|
23
|
+
let testDir: string;
|
|
24
|
+
let runner: SwarmRunner;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Create test directory
|
|
28
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarm-test-'));
|
|
29
|
+
|
|
30
|
+
// Create test files
|
|
31
|
+
fs.writeFileSync(path.join(testDir, 'file1.js'), '// Test file 1');
|
|
32
|
+
fs.writeFileSync(path.join(testDir, 'file2.ts'), '// Test file 2');
|
|
33
|
+
fs.writeFileSync(path.join(testDir, 'file3.py'), '# Test file 3');
|
|
34
|
+
|
|
35
|
+
// Reset mocks
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
// Force exit after all tests complete
|
|
47
|
+
setTimeout(() => process.exit(0), 100);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('initialization', () => {
|
|
51
|
+
test('should create swarm runner with options', () => {
|
|
52
|
+
const options: SwarmOptions = {
|
|
53
|
+
provider: 'claude',
|
|
54
|
+
count: 5,
|
|
55
|
+
prompt: 'Add copyright header',
|
|
56
|
+
cwd: testDir
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
runner = new SwarmRunner(options);
|
|
60
|
+
expect(runner).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should limit agent count to 100', () => {
|
|
64
|
+
const options: SwarmOptions = {
|
|
65
|
+
provider: 'claude',
|
|
66
|
+
count: 150,
|
|
67
|
+
prompt: 'Test prompt',
|
|
68
|
+
cwd: testDir
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
runner = new SwarmRunner(options);
|
|
72
|
+
// We can't directly test private properties, but this ensures no crash
|
|
73
|
+
expect(runner).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('file finding', () => {
|
|
78
|
+
test('should find editable files in directory', async () => {
|
|
79
|
+
// Mock glob to return our test files immediately
|
|
80
|
+
vi.mocked(glob).mockImplementation((pattern, options, callback) => {
|
|
81
|
+
if (typeof callback === 'function') {
|
|
82
|
+
// Call callback synchronously
|
|
83
|
+
callback(null, ['file1.js', 'file2.ts', 'file3.py']);
|
|
84
|
+
}
|
|
85
|
+
return undefined as any;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const options: SwarmOptions = {
|
|
89
|
+
provider: 'claude',
|
|
90
|
+
count: 3,
|
|
91
|
+
prompt: 'Test prompt',
|
|
92
|
+
cwd: testDir
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
runner = new SwarmRunner(options);
|
|
96
|
+
|
|
97
|
+
// Mock auth to return true
|
|
98
|
+
vi.spyOn(runner, 'ensureProviderAuth').mockResolvedValue(true);
|
|
99
|
+
|
|
100
|
+
// Mock spawn to return immediately closing processes
|
|
101
|
+
let spawnCount = 0;
|
|
102
|
+
vi.mocked(child_process.spawn).mockImplementation(() => {
|
|
103
|
+
spawnCount++;
|
|
104
|
+
const proc = new EventEmitter();
|
|
105
|
+
proc.stdout = new EventEmitter();
|
|
106
|
+
proc.stderr = new EventEmitter();
|
|
107
|
+
proc.kill = vi.fn();
|
|
108
|
+
|
|
109
|
+
// Close immediately
|
|
110
|
+
process.nextTick(() => proc.emit('close', 0));
|
|
111
|
+
|
|
112
|
+
return proc as any;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await runner.run();
|
|
116
|
+
|
|
117
|
+
// Should have spawned 3 processes (one for each file)
|
|
118
|
+
expect(spawnCount).toBe(3);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('provider authentication', () => {
|
|
123
|
+
test('should check Claude authentication', async () => {
|
|
124
|
+
const options: SwarmOptions = {
|
|
125
|
+
provider: 'claude',
|
|
126
|
+
count: 1,
|
|
127
|
+
prompt: 'Test',
|
|
128
|
+
cwd: testDir
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
runner = new SwarmRunner(options);
|
|
132
|
+
|
|
133
|
+
// Mock environment variable
|
|
134
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
135
|
+
|
|
136
|
+
// Mock successful auth check
|
|
137
|
+
vi.mocked(child_process.spawn).mockImplementationOnce(() => {
|
|
138
|
+
const authCheckProcess = new EventEmitter();
|
|
139
|
+
authCheckProcess.stderr = new EventEmitter();
|
|
140
|
+
authCheckProcess.kill = vi.fn();
|
|
141
|
+
|
|
142
|
+
// Emit close immediately
|
|
143
|
+
process.nextTick(() => authCheckProcess.emit('close', 0));
|
|
144
|
+
|
|
145
|
+
return authCheckProcess as any;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await runner.ensureProviderAuth();
|
|
149
|
+
expect(result).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should return true for local provider', async () => {
|
|
153
|
+
const options: SwarmOptions = {
|
|
154
|
+
provider: 'local',
|
|
155
|
+
count: 1,
|
|
156
|
+
prompt: 'Test',
|
|
157
|
+
cwd: testDir
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
runner = new SwarmRunner(options);
|
|
161
|
+
const result = await runner.ensureProviderAuth();
|
|
162
|
+
expect(result).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should check API key for OpenAI', async () => {
|
|
166
|
+
const options: SwarmOptions = {
|
|
167
|
+
provider: 'openai',
|
|
168
|
+
count: 1,
|
|
169
|
+
prompt: 'Test',
|
|
170
|
+
cwd: testDir
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
runner = new SwarmRunner(options);
|
|
174
|
+
|
|
175
|
+
// Without API key
|
|
176
|
+
delete process.env.OPENAI_API_KEY;
|
|
177
|
+
expect(await runner.ensureProviderAuth()).toBe(false);
|
|
178
|
+
|
|
179
|
+
// With API key
|
|
180
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
181
|
+
expect(await runner.ensureProviderAuth()).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('command building', () => {
|
|
186
|
+
test('should build correct command for Claude', () => {
|
|
187
|
+
const options: SwarmOptions = {
|
|
188
|
+
provider: 'claude',
|
|
189
|
+
count: 1,
|
|
190
|
+
prompt: 'Add header',
|
|
191
|
+
cwd: testDir
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
runner = new SwarmRunner(options);
|
|
195
|
+
const command = (runner as any).buildCommand('test.js');
|
|
196
|
+
|
|
197
|
+
expect(command.cmd).toBe('claude');
|
|
198
|
+
expect(command.args).toContain('-p');
|
|
199
|
+
expect(command.args.join(' ')).toContain('Add header');
|
|
200
|
+
expect(command.args).toContain('--max-turns');
|
|
201
|
+
expect(command.args).toContain('5');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should build correct command for local provider', () => {
|
|
205
|
+
const options: SwarmOptions = {
|
|
206
|
+
provider: 'local',
|
|
207
|
+
count: 1,
|
|
208
|
+
prompt: 'Format code',
|
|
209
|
+
cwd: testDir
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
runner = new SwarmRunner(options);
|
|
213
|
+
const command = (runner as any).buildCommand('test.js');
|
|
214
|
+
|
|
215
|
+
expect(command.cmd).toBe('dev');
|
|
216
|
+
expect(command.args).toContain('agent');
|
|
217
|
+
expect(command.args.join(' ')).toContain('Format code');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('parallel processing', () => {
|
|
222
|
+
test('should process multiple files in parallel', async () => {
|
|
223
|
+
vi.mocked(glob).mockImplementation((pattern, options, callback) => {
|
|
224
|
+
if (typeof callback === 'function') {
|
|
225
|
+
callback(null, ['file1.js', 'file2.js', 'file3.js']);
|
|
226
|
+
}
|
|
227
|
+
return undefined as any;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const options: SwarmOptions = {
|
|
231
|
+
provider: 'local',
|
|
232
|
+
count: 3,
|
|
233
|
+
prompt: 'Add copyright',
|
|
234
|
+
cwd: testDir
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
runner = new SwarmRunner(options);
|
|
238
|
+
|
|
239
|
+
// Mock auth
|
|
240
|
+
vi.spyOn(runner, 'ensureProviderAuth').mockResolvedValue(true);
|
|
241
|
+
|
|
242
|
+
let processCount = 0;
|
|
243
|
+
vi.mocked(child_process.spawn).mockImplementation(() => {
|
|
244
|
+
processCount++;
|
|
245
|
+
const proc = new EventEmitter();
|
|
246
|
+
proc.stdout = new EventEmitter();
|
|
247
|
+
proc.stderr = new EventEmitter();
|
|
248
|
+
proc.kill = vi.fn();
|
|
249
|
+
|
|
250
|
+
// Simulate successful completion
|
|
251
|
+
process.nextTick(() => proc.emit('close', 0));
|
|
252
|
+
|
|
253
|
+
return proc as any;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await runner.run();
|
|
257
|
+
|
|
258
|
+
// Should have spawned 3 processes
|
|
259
|
+
expect(processCount).toBe(3);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('should handle process failures', async () => {
|
|
263
|
+
vi.mocked(glob).mockImplementation((pattern, options, callback) => {
|
|
264
|
+
if (typeof callback === 'function') {
|
|
265
|
+
callback(null, ['file1.js']);
|
|
266
|
+
}
|
|
267
|
+
return undefined as any;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const options: SwarmOptions = {
|
|
271
|
+
provider: 'local',
|
|
272
|
+
count: 1,
|
|
273
|
+
prompt: 'Test',
|
|
274
|
+
cwd: testDir
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
runner = new SwarmRunner(options);
|
|
278
|
+
|
|
279
|
+
// Mock auth
|
|
280
|
+
vi.spyOn(runner, 'ensureProviderAuth').mockResolvedValue(true);
|
|
281
|
+
|
|
282
|
+
vi.mocked(child_process.spawn).mockImplementation(() => {
|
|
283
|
+
const proc = new EventEmitter();
|
|
284
|
+
proc.stdout = new EventEmitter();
|
|
285
|
+
proc.stderr = new EventEmitter();
|
|
286
|
+
proc.kill = vi.fn();
|
|
287
|
+
|
|
288
|
+
// Simulate failure
|
|
289
|
+
process.nextTick(() => {
|
|
290
|
+
proc.stderr!.emit('data', 'Error occurred');
|
|
291
|
+
proc.emit('close', 1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return proc as any;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Should complete without throwing
|
|
298
|
+
await expect(runner.run()).resolves.not.toThrow();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|