@oalacea/demon 1.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,387 @@
1
+ /**
2
+ * Demon - Test Generator Agent
3
+ *
4
+ * Generates test files based on source code analysis.
5
+ * Supports unit, integration, and E2E test generation.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Generate a unit test for a component
13
+ */
14
+ function generateComponentTest(componentPath, componentName, content) {
15
+ const hasProps = /interface Props|type Props|Props:/.test(content);
16
+ const hasOnClick = /onClick|handleClick/.test(content);
17
+ const hasDisabled = /disabled|isDisabled/.test(content);
18
+ const hasLoading = /loading|isLoading/.test(content);
19
+ const hasChildren = /children|{props\.children}/.test(content);
20
+ const hasError = /error|isError/.test(content);
21
+ const hasVariants = /variant=|variants/.test(content);
22
+
23
+ let imports = [
24
+ `import { render, screen } from '@testing-library/react';`,
25
+ `import { describe, it, expect, vi } from 'vitest';`,
26
+ `import { ${componentName} } from '@/components/${componentName}';`,
27
+ ];
28
+
29
+ let tests = [];
30
+
31
+ // Basic render test
32
+ tests.push(`
33
+ it('should render', () => {
34
+ render(<${componentName} />);
35
+ expect(screen.getByRole('button')).toBeInTheDocument();
36
+ });`);
37
+
38
+ // Children test
39
+ if (hasChildren) {
40
+ tests.push(`
41
+ it('should render children', () => {
42
+ render(<${componentName}>Test content</${componentName}>);
43
+ expect(screen.getByText('Test content')).toBeInTheDocument();
44
+ });`);
45
+ }
46
+
47
+ // Disabled test
48
+ if (hasDisabled) {
49
+ tests.push(`
50
+ it('should be disabled when disabled prop is true', () => {
51
+ render(<${componentName} disabled />);
52
+ expect(screen.getByRole('button')).toBeDisabled();
53
+ });`);
54
+ }
55
+
56
+ // Loading test
57
+ if (hasLoading) {
58
+ tests.push(`
59
+ it('should show loading state', () => {
60
+ render(<${componentName} loading />);
61
+ expect(screen.getByTestId(/spinner|loading/i)).toBeInTheDocument();
62
+ });`);
63
+ }
64
+
65
+ // Error test
66
+ if (hasError) {
67
+ tests.push(`
68
+ it('should show error message', () => {
69
+ render(<${componentName} error="Something went wrong" />);
70
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
71
+ });`);
72
+ }
73
+
74
+ // onClick test
75
+ if (hasOnClick) {
76
+ tests.push(`
77
+ it('should call onClick when clicked', async () => {
78
+ const handleClick = vi.fn();
79
+ render(<${componentName} onClick={handleClick} />);
80
+ await screen.getByRole('button').click();
81
+ expect(handleClick).toHaveBeenCalledTimes(1);
82
+ });`);
83
+ }
84
+
85
+ // Variant test
86
+ if (hasVariants) {
87
+ tests.push(`
88
+ it('should apply variant class', () => {
89
+ render(<${componentName} variant="danger" />);
90
+ expect(screen.getByRole('button')).toHaveClass('btn-danger');
91
+ });`);
92
+ }
93
+
94
+ return `${imports.join('\n')}
95
+
96
+ describe('${componentName}', () => {${tests.join('\n')}
97
+ });
98
+ `;
99
+ }
100
+
101
+ /**
102
+ * Generate a unit test for a hook
103
+ */
104
+ function generateHookTest(hookPath, hookName, content) {
105
+ const hasReturn = /return\s*{/.test(content);
106
+ const hasState = /useState|useReducer/.test(content);
107
+ const hasEffect = /useEffect/.test(content);
108
+ const hasCallback = /useCallback/.test(content);
109
+ const hasMemo = /useMemo/.test(content);
110
+
111
+ let imports = [
112
+ `import { renderHook, act, waitFor } from '@testing-library/react';`,
113
+ `import { describe, it, expect, vi } from 'vitest';`,
114
+ `import { ${hookName} } from '@/hooks/${hookName}';`,
115
+ ];
116
+
117
+ let tests = [];
118
+
119
+ // Basic return test
120
+ tests.push(`
121
+ it('should return initial state', () => {
122
+ const { result } = renderHook(() => ${hookName}());
123
+ expect(result.current).toBeDefined();
124
+ });`);
125
+
126
+ // State update test
127
+ if (hasState) {
128
+ tests.push(`
129
+ it('should update state', async () => {
130
+ const { result } = renderHook(() => ${hookName}());
131
+
132
+ act(() => {
133
+ result.current.setValue('test');
134
+ });
135
+
136
+ await waitFor(() => {
137
+ expect(result.current.value).toBe('test');
138
+ });
139
+ });`);
140
+ }
141
+
142
+ // Cleanup test
143
+ if (hasEffect) {
144
+ tests.push(`
145
+ it('should cleanup on unmount', () => {
146
+ const cleanup = vi.fn();
147
+ const { unmount } = renderHook(() => ${hookName}({ cleanup }));
148
+ unmount();
149
+ expect(cleanup).toHaveBeenCalled();
150
+ });`);
151
+ }
152
+
153
+ return `${imports.join('\n')}
154
+
155
+ describe('${hookName}', () => {${tests.join('\n')}
156
+ });
157
+ `;
158
+ }
159
+
160
+ /**
161
+ * Generate a unit test for a utility function
162
+ */
163
+ function generateUtilTest(utilPath, utilName, content) {
164
+ const hasParams = /function\s+\w+\s*\(([^)]+)\)|export\s+const\s+\w+\s*=\s*\(([^)]+)\)/.test(content);
165
+ const isAsync = /async\s+function|=>\s*async/.test(content);
166
+
167
+ let imports = [
168
+ `import { describe, it, expect } from 'vitest';`,
169
+ `import { ${utilName} } from '@/utils/${utilName}';`,
170
+ ];
171
+
172
+ let tests = [];
173
+
174
+ if (isAsync) {
175
+ tests.push(`
176
+ it('should return result', async () => {
177
+ const result = await ${utilName}('test');
178
+ expect(result).toBeDefined();
179
+ });`);
180
+ } else {
181
+ tests.push(`
182
+ it('should return result', () => {
183
+ const result = ${utilName}('test');
184
+ expect(result).toBeDefined();
185
+ });`);
186
+ }
187
+
188
+ return `${imports.join('\n')}
189
+
190
+ describe('${utilName}', () => {${tests.join('\n')}
191
+ });
192
+ `;
193
+ }
194
+
195
+ /**
196
+ * Generate an integration test for an API route
197
+ */
198
+ function generateApiTest(routePath, routeMethod, content) {
199
+ const hasValidation = /zod|validate|schema/.test(content);
200
+ const hasAuth = /auth|requireAuth|getSession/.test(content);
201
+ const hasDb = /prisma|db\./.test(content);
202
+
203
+ let imports = [
204
+ `import { describe, it, expect, beforeEach, afterEach } from 'vitest';`,
205
+ `import { POST, GET } from '@/app${routePath}/route';`,
206
+ ];
207
+
208
+ if (hasDb) {
209
+ imports.push(`import { db } from '@test/db';`);
210
+ }
211
+
212
+ let tests = [];
213
+
214
+ // Success test
215
+ tests.push(`
216
+ it('should return 200 on success', async () => {
217
+ const request = new Request('http://localhost:3000${routePath}', {
218
+ method: '${routeMethod}',
219
+ body: JSON.stringify({ test: 'data' }),
220
+ });
221
+
222
+ const response = await ${routeMethod}(request);
223
+ expect(response.status).toBe(200);
224
+ });`);
225
+
226
+ // Validation test
227
+ if (hasValidation) {
228
+ tests.push(`
229
+ it('should return 400 for invalid data', async () => {
230
+ const request = new Request('http://localhost:3000${routePath}', {
231
+ method: '${routeMethod}',
232
+ body: JSON.stringify({ invalid: 'data' }),
233
+ });
234
+
235
+ const response = await ${routeMethod}(request);
236
+ expect(response.status).toBe(400);
237
+ });`);
238
+ }
239
+
240
+ // Auth test
241
+ if (hasAuth) {
242
+ tests.push(`
243
+ it('should return 401 without auth', async () => {
244
+ const request = new Request('http://localhost:3000${routePath}', {
245
+ method: '${routeMethod}',
246
+ });
247
+
248
+ const response = await ${routeMethod}(request);
249
+ expect(response.status).toBe(401);
250
+ });`);
251
+ }
252
+
253
+ // DB transaction test
254
+ if (hasDb) {
255
+ tests = [
256
+ `
257
+ beforeEach(async () => {
258
+ await db.begin();
259
+ });`,
260
+ `
261
+ afterEach(async () => {
262
+ await db.rollback();
263
+ });`,
264
+ ...tests,
265
+ ];
266
+ }
267
+
268
+ return `${imports.join('\n')}
269
+
270
+ describe('${routeMethod} ${routePath}', () => {${tests.join('\n')}
271
+ });
272
+ `;
273
+ }
274
+
275
+ /**
276
+ * Generate an E2E test with Playwright
277
+ */
278
+ function generateE2ETest(pageName, actions) {
279
+ let imports = [
280
+ `import { test, expect } from '@playwright/test';`,
281
+ ];
282
+
283
+ let testContent = '';
284
+
285
+ if (actions.includes('login')) {
286
+ testContent += `
287
+ test.beforeEach(async ({ page }) => {
288
+ await page.goto('/login');
289
+ });`;
290
+ }
291
+
292
+ testContent += `
293
+
294
+ test('should complete flow', async ({ page }) => {
295
+ await page.goto('/${pageName}');`;
296
+
297
+ if (actions.includes('fill')) {
298
+ testContent += `
299
+ await page.fill('input[name="email"]', 'test@example.com');`;
300
+ }
301
+
302
+ if (actions.includes('click')) {
303
+ testContent += `
304
+ await page.click('button[type="submit"]');`;
305
+ }
306
+
307
+ if (actions.includes('navigate')) {
308
+ testContent += `
309
+ await expect(page).toHaveURL('/dashboard');`;
310
+ }
311
+
312
+ testContent += `
313
+ });`;
314
+
315
+ return `${imports.join('\n')}
316
+
317
+ test.describe('${pageName}', () => {${testContent}
318
+ });
319
+ `;
320
+ }
321
+
322
+ /**
323
+ * Main generator function
324
+ */
325
+ function generateTest(type, filePath, options = {}) {
326
+ if (!fs.existsSync(filePath)) {
327
+ throw new Error(`File not found: ${filePath}`);
328
+ }
329
+
330
+ const content = fs.readFileSync(filePath, 'utf-8');
331
+ const filename = path.basename(filePath, path.extname(filePath));
332
+
333
+ switch (type) {
334
+ case 'component':
335
+ return generateComponentTest(filePath, filename, content);
336
+ case 'hook':
337
+ return generateHookTest(filePath, filename, content);
338
+ case 'util':
339
+ return generateUtilTest(filePath, filename, content);
340
+ case 'api':
341
+ return generateApiTest(options.route, options.method, content);
342
+ case 'e2e':
343
+ return generateE2ETest(filename, options.actions || []);
344
+ default:
345
+ throw new Error(`Unknown test type: ${type}`);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Find files without tests
351
+ */
352
+ function findFilesWithoutTests(projectDir, pattern) {
353
+ const files = [];
354
+
355
+ function findInDir(dir) {
356
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
357
+
358
+ for (const entry of entries) {
359
+ const fullPath = path.join(dir, entry.name);
360
+
361
+ if (entry.isDirectory()) {
362
+ if (['node_modules', '.next', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
363
+ continue;
364
+ }
365
+ findInDir(fullPath);
366
+ } else if (entry.isFile() && fullPath.match(pattern)) {
367
+ const testPath = fullPath.replace(/\.(tsx?)$/, '.test$1');
368
+ if (!fs.existsSync(testPath)) {
369
+ files.push(fullPath);
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ findInDir(projectDir);
376
+ return files;
377
+ }
378
+
379
+ module.exports = {
380
+ generateTest,
381
+ generateComponentTest,
382
+ generateHookTest,
383
+ generateUtilTest,
384
+ generateApiTest,
385
+ generateE2ETest,
386
+ findFilesWithoutTests,
387
+ };
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Demon - Test Runner Agent
3
+ *
4
+ * Executes tests and parses results.
5
+ * Supports Vitest, Jest, and Playwright.
6
+ */
7
+
8
+ const { execSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ // Configuration
13
+ const CONFIG = {
14
+ container: 'demon-tools',
15
+ docker: 'docker exec',
16
+ };
17
+
18
+ /**
19
+ * Run tests inside the Docker container
20
+ */
21
+ function runTests(command, options = {}) {
22
+ const dockerCmd = `${CONFIG.docker} ${CONFIG.container} ${command}`;
23
+
24
+ try {
25
+ const output = execSync(dockerCmd, {
26
+ encoding: 'utf-8',
27
+ stdio: options.silent ? 'pipe' : 'inherit',
28
+ timeout: options.timeout || 120000,
29
+ cwd: options.cwd || process.cwd(),
30
+ });
31
+
32
+ return {
33
+ success: true,
34
+ output,
35
+ exitCode: 0,
36
+ };
37
+ } catch (error) {
38
+ return {
39
+ success: false,
40
+ output: error.stdout || '',
41
+ error: error.stderr || error.message,
42
+ exitCode: error.status || 1,
43
+ };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Run unit tests
49
+ */
50
+ function runUnitTests(testFile = null) {
51
+ const cmd = testFile
52
+ ? `npm test -- ${testFile}`
53
+ : `npm test`;
54
+
55
+ return runTests(cmd);
56
+ }
57
+
58
+ /**
59
+ * Run integration tests
60
+ */
61
+ function runIntegrationTests(testFile = null) {
62
+ const cmd = testFile
63
+ ? `npm test -- ${testFile}`
64
+ : `npm test -- tests/integration`;
65
+
66
+ return runTests(cmd);
67
+ }
68
+
69
+ /**
70
+ * Run E2E tests
71
+ */
72
+ function runE2ETests(testFile = null) {
73
+ const cmd = testFile
74
+ ? `npx playwright test ${testFile}`
75
+ : `npx playwright test`;
76
+
77
+ return runTests(cmd);
78
+ }
79
+
80
+ /**
81
+ * Run performance tests
82
+ */
83
+ function runPerformanceTests(testFile = null) {
84
+ const cmd = testFile
85
+ ? `k6 run ${testFile}`
86
+ : `k6 run tests/performance`;
87
+
88
+ return runTests(cmd);
89
+ }
90
+
91
+ /**
92
+ * Parse Vitest/Jest output
93
+ */
94
+ function parseTestOutput(output) {
95
+ const results = {
96
+ total: 0,
97
+ passed: 0,
98
+ failed: 0,
99
+ skipped: 0,
100
+ failures: [],
101
+ };
102
+
103
+ // Parse Vitest output
104
+ const testFiles = output.match(/Test Files\s+\d+ passed/g);
105
+ if (testFiles) {
106
+ results.total += parseInt(testFiles[0].match(/\d+/)[0]);
107
+ }
108
+
109
+ const passed = output.match(/(\d+) passed/);
110
+ if (passed) {
111
+ results.passed += parseInt(passed[1]);
112
+ }
113
+
114
+ const failed = output.match(/(\d+) failed/);
115
+ if (failed) {
116
+ results.failed += parseInt(failed[1]);
117
+ // Parse failure details
118
+ const failOutput = output.match(/FAIL\s+(.+)/g);
119
+ if (failOutput) {
120
+ results.failures = failOutput.map((f) => {
121
+ const match = f.match(/FAIL\s+(.+?)\s+/);
122
+ return match ? match[1] : f;
123
+ });
124
+ }
125
+ }
126
+
127
+ const skipped = output.match(/(\d+) skipped/);
128
+ if (skipped) {
129
+ results.skipped += parseInt(skipped[1]);
130
+ }
131
+
132
+ return results;
133
+ }
134
+
135
+ /**
136
+ * Parse Playwright output
137
+ */
138
+ function parseE2EOutput(output) {
139
+ const results = {
140
+ total: 0,
141
+ passed: 0,
142
+ failed: 0,
143
+ skipped: 0,
144
+ failures: [],
145
+ };
146
+
147
+ const passed = output.match(/passed\s+(\d+)/);
148
+ if (passed) results.passed += parseInt(passed[1]);
149
+
150
+ const failed = output.match(/failed\s+(\d+)/);
151
+ if (failed) {
152
+ results.failed += parseInt(failed[1]);
153
+ // Parse failure details
154
+ const failLines = output.match(/^.*?\[ERROR\].*$/gm);
155
+ if (failLines) {
156
+ results.failures = failLines;
157
+ }
158
+ }
159
+
160
+ const skipped = output.match(/skipped\s+(\d+)/);
161
+ if (skipped) results.skipped += parseInt(skipped[1]);
162
+
163
+ results.total = results.passed + results.failed + results.skipped;
164
+
165
+ return results;
166
+ }
167
+
168
+ /**
169
+ * Parse k6 output
170
+ */
171
+ function parsePerfOutput(output) {
172
+ const results = {
173
+ requests: 0,
174
+ duration: 0,
175
+ avgResponseTime: 0,
176
+ p95: 0,
177
+ p99: 0,
178
+ rps: 0,
179
+ failureRate: 0,
180
+ };
181
+
182
+ // Parse k6 summary output
183
+ const checks = output.match(/checks.*?:\s+([\d.]+)%/);
184
+ if (checks) results.failureRate = 100 - parseFloat(checks[1]);
185
+
186
+ const dataReceived = output.match(/data_received\s+([\d.]+\s+\w+)/);
187
+ if (dataReceived) results.dataReceived = dataReceived[1];
188
+
189
+ const httpReqDuration = output.match(/http_req_duration.*?avg=\s*([\d.]+)ms.*?p\(95\)=\s*([\d.]+)ms/);
190
+ if (httpReqDuration) {
191
+ results.avgResponseTime = parseFloat(httpReqDuration[1]);
192
+ results.p95 = parseFloat(httpReqDuration[2]);
193
+ }
194
+
195
+ const httpReqs = output.match(/http_reqs\s+([\d.]+)/);
196
+ if (httpReqs) results.requests = parseInt(httpReqs[1]);
197
+
198
+ const iterationDuration = output.match(/iteration_duration.*?avg=\s*([\d.]+)/);
199
+ if (iterationDuration) results.duration = parseFloat(iterationDuration[1]);
200
+
201
+ return results;
202
+ }
203
+
204
+ /**
205
+ * Get test coverage
206
+ */
207
+ function getCoverage() {
208
+ const result = runTests('npm test -- --coverage');
209
+
210
+ if (!result.success) {
211
+ return null;
212
+ }
213
+
214
+ const coverage = {
215
+ lines: 0,
216
+ functions: 0,
217
+ branches: 0,
218
+ statements: 0,
219
+ };
220
+
221
+ // Parse coverage output
222
+ const lines = result.output.match(/All files[^]*?(\d+)%/);
223
+ if (lines) coverage.lines = parseInt(lines[1]);
224
+
225
+ const functions = result.output.match(/functions[^]*?(\d+)%/);
226
+ if (functions) coverage.functions = parseInt(functions[1]);
227
+
228
+ const branches = result.output.match(/branches[^]*?(\d+)%/);
229
+ if (branches) coverage.branches = parseInt(branches[1]);
230
+
231
+ const statements = result.output.match(/statements[^]*?(\d+)%/);
232
+ if (statements) coverage.statements = parseInt(statements[1]);
233
+
234
+ return coverage;
235
+ }
236
+
237
+ /**
238
+ * Run all tests and return summary
239
+ */
240
+ async function runAllTests() {
241
+ const summary = {
242
+ unit: null,
243
+ integration: null,
244
+ e2e: null,
245
+ performance: null,
246
+ coverage: null,
247
+ };
248
+
249
+ // Unit tests
250
+ const unitResult = runUnitTests();
251
+ summary.unit = {
252
+ ...parseTestOutput(unitResult.output),
253
+ success: unitResult.success,
254
+ };
255
+
256
+ // Integration tests
257
+ const integrationResult = runIntegrationTests();
258
+ summary.integration = {
259
+ ...parseTestOutput(integrationResult.output),
260
+ success: integrationResult.success,
261
+ };
262
+
263
+ // E2E tests
264
+ const e2eResult = runE2ETests();
265
+ summary.e2e = {
266
+ ...parseE2EOutput(e2eResult.output),
267
+ success: e2eResult.success,
268
+ };
269
+
270
+ // Performance tests
271
+ const perfResult = runPerformanceTests();
272
+ summary.performance = {
273
+ ...parsePerfOutput(perfResult.output),
274
+ success: perfResult.success,
275
+ };
276
+
277
+ // Coverage
278
+ summary.coverage = getCoverage();
279
+
280
+ return summary;
281
+ }
282
+
283
+ /**
284
+ * Run a single test file
285
+ */
286
+ function runSingleTest(testPath) {
287
+ return runTests(`npm test -- ${testPath}`);
288
+ }
289
+
290
+ /**
291
+ * Watch tests
292
+ */
293
+ function watchTests() {
294
+ return runTests('npm test -- --watch');
295
+ }
296
+
297
+ /**
298
+ * Debug a failing test
299
+ */
300
+ function debugTest(testPath) {
301
+ return runTests(`npm test -- ${testPath} --reporter=verbose --no-coverage`);
302
+ }
303
+
304
+ module.exports = {
305
+ runTests,
306
+ runUnitTests,
307
+ runIntegrationTests,
308
+ runE2ETests,
309
+ runPerformanceTests,
310
+ parseTestOutput,
311
+ parseE2EOutput,
312
+ parsePerfOutput,
313
+ getCoverage,
314
+ runAllTests,
315
+ runSingleTest,
316
+ watchTests,
317
+ debugTest,
318
+ };