@hanzo/dev 2.0.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.
@@ -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
+ });
@@ -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
- });