@hanzo/dev 2.0.0 → 2.1.1
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 +15577 -2097
- package/package.json +13 -10
- package/src/cli/dev.ts +168 -1
- package/src/lib/benchmark-runner.ts +431 -0
- package/src/lib/editor.ts +27 -0
- package/src/lib/swarm-runner.ts +389 -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/editor.test.ts +7 -7
- 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 +6 -6
- package/tests/swarm-runner.test.ts +301 -0
- package/vitest.config.ts +37 -0
- package/.eslintrc.js +0 -25
- package/jest.config.js +0 -30
- package/tests/setup.ts +0 -25
|
@@ -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
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: 'node',
|
|
8
|
+
include: ['tests/**/*.test.ts'],
|
|
9
|
+
exclude: ['node_modules', 'dist', 'build'],
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: 'v8',
|
|
12
|
+
reporter: ['text', 'json', 'html'],
|
|
13
|
+
exclude: [
|
|
14
|
+
'node_modules',
|
|
15
|
+
'tests',
|
|
16
|
+
'dist',
|
|
17
|
+
'**/*.d.ts',
|
|
18
|
+
'**/*.config.*',
|
|
19
|
+
'**/mockData.ts'
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
testTimeout: 5000,
|
|
23
|
+
hookTimeout: 5000,
|
|
24
|
+
pool: 'threads',
|
|
25
|
+
poolOptions: {
|
|
26
|
+
threads: {
|
|
27
|
+
singleThread: true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
forceRerunTriggers: ['**/*.test.ts']
|
|
31
|
+
},
|
|
32
|
+
resolve: {
|
|
33
|
+
alias: {
|
|
34
|
+
'@': path.resolve(__dirname, './src')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
package/.eslintrc.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
parser: '@typescript-eslint/parser',
|
|
3
|
-
extends: [
|
|
4
|
-
'eslint:recommended',
|
|
5
|
-
'plugin:@typescript-eslint/recommended',
|
|
6
|
-
],
|
|
7
|
-
parserOptions: {
|
|
8
|
-
ecmaVersion: 2020,
|
|
9
|
-
sourceType: 'module',
|
|
10
|
-
},
|
|
11
|
-
env: {
|
|
12
|
-
node: true,
|
|
13
|
-
jest: true,
|
|
14
|
-
es2020: true,
|
|
15
|
-
},
|
|
16
|
-
rules: {
|
|
17
|
-
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
18
|
-
'@typescript-eslint/no-explicit-any': 'warn',
|
|
19
|
-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
20
|
-
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
21
|
-
'prefer-const': 'error',
|
|
22
|
-
'no-var': 'error',
|
|
23
|
-
},
|
|
24
|
-
ignorePatterns: ['dist/', 'coverage/', 'node_modules/', '*.js'],
|
|
25
|
-
};
|
package/jest.config.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
preset: 'ts-jest',
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
|
6
|
-
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
|
7
|
-
transform: {
|
|
8
|
-
'^.+\\.ts$': 'ts-jest',
|
|
9
|
-
},
|
|
10
|
-
collectCoverageFrom: [
|
|
11
|
-
'src/**/*.ts',
|
|
12
|
-
'!src/**/*.d.ts',
|
|
13
|
-
'!src/**/index.ts',
|
|
14
|
-
],
|
|
15
|
-
coverageDirectory: 'coverage',
|
|
16
|
-
coverageReporters: ['text', 'lcov', 'html'],
|
|
17
|
-
moduleNameMapper: {
|
|
18
|
-
'^@/(.*)$': '<rootDir>/src/$1',
|
|
19
|
-
},
|
|
20
|
-
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
|
21
|
-
testTimeout: 30000,
|
|
22
|
-
globals: {
|
|
23
|
-
'ts-jest': {
|
|
24
|
-
tsconfig: {
|
|
25
|
-
esModuleInterop: true,
|
|
26
|
-
allowSyntheticDefaultImports: true,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
};
|
package/tests/setup.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
});
|