neuro-cli 4.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1200 @@
1
+ // ============================================================
2
+ // NeuroCLI - Testing Integration
3
+ // Automatic test framework detection, execution, and coverage
4
+ // Supports Jest, Vitest, pytest, Go test, cargo test,
5
+ // JUnit, Playwright, Cypress, and standard test runners
6
+ // ============================================================
7
+ import { join, resolve, dirname, extname, basename, relative } from 'path';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
9
+ import { execSync, spawn } from 'child_process';
10
+ // -----------------------------------------------------------
11
+ // Test framework definitions
12
+ // -----------------------------------------------------------
13
+ const TEST_FRAMEWORKS = [
14
+ // Jest
15
+ {
16
+ name: 'jest',
17
+ language: 'javascript',
18
+ configFiles: [
19
+ 'jest.config.js',
20
+ 'jest.config.cjs',
21
+ 'jest.config.mjs',
22
+ 'jest.config.ts',
23
+ 'jest.config.mts',
24
+ 'jest.config.cts',
25
+ ],
26
+ commands: {
27
+ run: ['npx', 'jest', '--json', '--outputFile=/dev/stdout'],
28
+ watch: ['npx', 'jest', '--watch'],
29
+ coverage: ['npx', 'jest', '--coverage', '--json', '--outputFile=/dev/stdout'],
30
+ },
31
+ parseOutput: (stdout, _stderr, rootDir) => {
32
+ const result = {
33
+ total: 0,
34
+ passed: 0,
35
+ failed: 0,
36
+ skipped: 0,
37
+ duration: 0,
38
+ failures: [],
39
+ };
40
+ try {
41
+ // Try to find JSON in the output (Jest mixes JSON with other output)
42
+ const jsonMatch = stdout.match(/\{[\s\S]*"numPassedTests"[\s\S]*\}/);
43
+ if (!jsonMatch)
44
+ return result;
45
+ const data = JSON.parse(jsonMatch[0]);
46
+ result.total = data.numTotalTests;
47
+ result.passed = data.numPassedTests;
48
+ result.failed = data.numFailedTests;
49
+ result.skipped = data.numPendingTests;
50
+ result.duration = Date.now();
51
+ for (const testFile of data.testResults) {
52
+ for (const assertion of testFile.assertionResults) {
53
+ if (assertion.status === 'failed') {
54
+ const errorBody = assertion.failureMessages[0] ?? 'Unknown error';
55
+ const { message, expected, actual, line } = parseJestError(errorBody);
56
+ result.failures.push({
57
+ testName: assertion.fullName,
58
+ file: relative(rootDir, testFile.name),
59
+ line: assertion.location?.line ?? line,
60
+ error: message,
61
+ expected,
62
+ actual,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ // Coverage
68
+ if (data.coverageMap) {
69
+ result.coverage = parseJestCoverage(data.coverageMap, rootDir);
70
+ }
71
+ }
72
+ catch {
73
+ // Fallback: try to parse text output
74
+ result.total = (stdout.match(/\d+ test(s)? passed/g) ?? []).length > 0
75
+ ? parseInt(stdout.match(/Tests:\s+(\d+)/)?.[1] ?? '0', 10)
76
+ : 0;
77
+ }
78
+ return result;
79
+ },
80
+ },
81
+ // Vitest
82
+ {
83
+ name: 'vitest',
84
+ language: 'javascript',
85
+ configFiles: [
86
+ 'vitest.config.js',
87
+ 'vitest.config.cjs',
88
+ 'vitest.config.mjs',
89
+ 'vitest.config.ts',
90
+ 'vitest.config.mts',
91
+ ],
92
+ commands: {
93
+ run: ['npx', 'vitest', 'run', '--reporter=json'],
94
+ watch: ['npx', 'vitest', '--watch'],
95
+ coverage: ['npx', 'vitest', 'run', '--coverage', '--reporter=json'],
96
+ },
97
+ parseOutput: (stdout, _stderr, rootDir) => {
98
+ const result = {
99
+ total: 0,
100
+ passed: 0,
101
+ failed: 0,
102
+ skipped: 0,
103
+ duration: 0,
104
+ failures: [],
105
+ };
106
+ try {
107
+ const data = JSON.parse(stdout);
108
+ result.total = data.numTotalTests;
109
+ result.passed = data.numPassedTests;
110
+ result.failed = data.numFailedTests;
111
+ result.skipped = data.numPendingTests;
112
+ result.duration = Date.now();
113
+ for (const testFile of data.testResults) {
114
+ for (const assertion of testFile.assertionResults) {
115
+ if (assertion.status === 'failed') {
116
+ result.failures.push({
117
+ testName: assertion.fullName,
118
+ file: relative(rootDir, testFile.name),
119
+ line: assertion.location?.line,
120
+ error: assertion.failureMessages[0] ?? 'Unknown error',
121
+ });
122
+ }
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Parse error
128
+ }
129
+ return result;
130
+ },
131
+ },
132
+ // pytest
133
+ {
134
+ name: 'pytest',
135
+ language: 'python',
136
+ configFiles: [
137
+ 'pytest.ini',
138
+ 'pyproject.toml',
139
+ 'tox.ini',
140
+ 'setup.cfg',
141
+ 'conftest.py',
142
+ ],
143
+ commands: {
144
+ run: ['pytest', '--json-report', '--json-report-file=/dev/stdout'],
145
+ coverage: ['pytest', '--cov', '--cov-report=term-missing'],
146
+ },
147
+ parseOutput: (stdout, _stderr, rootDir) => {
148
+ const result = {
149
+ total: 0,
150
+ passed: 0,
151
+ failed: 0,
152
+ skipped: 0,
153
+ duration: 0,
154
+ failures: [],
155
+ };
156
+ try {
157
+ // Try JSON report first
158
+ const data = JSON.parse(stdout);
159
+ result.total = data.summary.total;
160
+ result.passed = data.summary.passed;
161
+ result.failed = data.summary.failed;
162
+ result.skipped = data.summary.skipped;
163
+ result.duration = Math.round(data.summary.duration * 1000);
164
+ if (data.tests) {
165
+ for (const test of data.tests) {
166
+ if (test.outcome === 'failed') {
167
+ result.failures.push({
168
+ testName: test.name,
169
+ file: relative(rootDir, test.filepath),
170
+ line: test.lineno,
171
+ error: test.call?.longrepr ?? 'Unknown error',
172
+ });
173
+ }
174
+ }
175
+ }
176
+ }
177
+ catch {
178
+ // Fallback: parse text output
179
+ const passedMatch = stdout.match(/(\d+) passed/);
180
+ const failedMatch = stdout.match(/(\d+) failed/);
181
+ const skippedMatch = stdout.match(/(\d+) skipped/);
182
+ result.passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
183
+ result.failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
184
+ result.skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
185
+ result.total = result.passed + result.failed + result.skipped;
186
+ // Parse FAILED lines
187
+ const failedLinePattern = /FAILED\s+(\S+)/g;
188
+ let match;
189
+ while ((match = failedLinePattern.exec(stdout)) !== null) {
190
+ result.failures.push({
191
+ testName: match[1],
192
+ file: match[1],
193
+ error: 'Test failed',
194
+ });
195
+ }
196
+ }
197
+ return result;
198
+ },
199
+ },
200
+ // Go test
201
+ {
202
+ name: 'go-test',
203
+ language: 'go',
204
+ configFiles: ['go.mod'],
205
+ commands: {
206
+ run: ['go', 'test', '-json', './...'],
207
+ coverage: ['go', 'test', '-coverprofile=coverage.out', './...'],
208
+ },
209
+ parseOutput: (stdout, _stderr, rootDir) => {
210
+ const result = {
211
+ total: 0,
212
+ passed: 0,
213
+ failed: 0,
214
+ skipped: 0,
215
+ duration: 0,
216
+ failures: [],
217
+ };
218
+ const lines = stdout.split('\n');
219
+ for (const line of lines) {
220
+ try {
221
+ const entry = JSON.parse(line);
222
+ if (entry.Action === 'pass' && entry.Test) {
223
+ result.total++;
224
+ result.passed++;
225
+ }
226
+ else if (entry.Action === 'fail' && entry.Test) {
227
+ result.total++;
228
+ result.failed++;
229
+ result.failures.push({
230
+ testName: entry.Test,
231
+ file: entry.Package,
232
+ error: 'Test failed',
233
+ });
234
+ }
235
+ else if (entry.Action === 'skip' && entry.Test) {
236
+ result.total++;
237
+ result.skipped++;
238
+ }
239
+ if (entry.Elapsed) {
240
+ result.duration += Math.round(entry.Elapsed * 1000);
241
+ }
242
+ }
243
+ catch {
244
+ // Not a JSON line — skip
245
+ }
246
+ }
247
+ return result;
248
+ },
249
+ },
250
+ // cargo test (Rust)
251
+ {
252
+ name: 'cargo-test',
253
+ language: 'rust',
254
+ configFiles: ['Cargo.toml'],
255
+ commands: {
256
+ run: ['cargo', 'test', '--message-format=json'],
257
+ coverage: ['cargo', 'tarpaulin', '--out=Stdout'],
258
+ },
259
+ parseOutput: (stdout, _stderr, rootDir) => {
260
+ const result = {
261
+ total: 0,
262
+ passed: 0,
263
+ failed: 0,
264
+ skipped: 0,
265
+ duration: 0,
266
+ failures: [],
267
+ };
268
+ const lines = stdout.split('\n');
269
+ for (const line of lines) {
270
+ try {
271
+ const entry = JSON.parse(line);
272
+ if (entry.type === 'test') {
273
+ result.total++;
274
+ if (entry.event === 'ok') {
275
+ result.passed++;
276
+ }
277
+ else if (entry.event === 'failed') {
278
+ result.failed++;
279
+ result.failures.push({
280
+ testName: entry.name ?? 'unknown',
281
+ file: 'unknown',
282
+ error: 'Test failed',
283
+ });
284
+ }
285
+ else if (entry.event === 'ignored') {
286
+ result.skipped++;
287
+ }
288
+ }
289
+ }
290
+ catch {
291
+ // Not a JSON line — try text parsing
292
+ if (line.includes('test result:')) {
293
+ const passedMatch = line.match(/(\d+) passed/);
294
+ const failedMatch = line.match(/(\d+) failed/);
295
+ const ignoredMatch = line.match(/(\d+) ignored/);
296
+ result.passed += passedMatch ? parseInt(passedMatch[1], 10) : 0;
297
+ result.failed += failedMatch ? parseInt(failedMatch[1], 10) : 0;
298
+ result.skipped += ignoredMatch ? parseInt(ignoredMatch[1], 10) : 0;
299
+ result.total = result.passed + result.failed + result.skipped;
300
+ }
301
+ }
302
+ }
303
+ return result;
304
+ },
305
+ },
306
+ // JUnit (Java)
307
+ {
308
+ name: 'junit',
309
+ language: 'java',
310
+ configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
311
+ commands: {
312
+ run: ['mvn', 'test'],
313
+ coverage: ['mvn', 'test', 'jacoco:report'],
314
+ },
315
+ parseOutput: (stdout, _stderr, rootDir) => {
316
+ const result = {
317
+ total: 0,
318
+ passed: 0,
319
+ failed: 0,
320
+ skipped: 0,
321
+ duration: 0,
322
+ failures: [],
323
+ };
324
+ // Parse Maven test output
325
+ const testsRunMatch = stdout.match(/Tests run:\s*(\d+)/);
326
+ const failuresMatch = stdout.match(/Failures:\s*(\d+)/);
327
+ const errorsMatch = stdout.match(/Errors:\s*(\d+)/);
328
+ const skippedMatch = stdout.match(/Skipped:\s*(\d+)/);
329
+ if (testsRunMatch) {
330
+ result.total = parseInt(testsRunMatch[1], 10);
331
+ const failCount = failuresMatch ? parseInt(failuresMatch[1], 10) : 0;
332
+ const errCount = errorsMatch ? parseInt(errorsMatch[1], 10) : 0;
333
+ result.failed = failCount + errCount;
334
+ result.skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
335
+ result.passed = result.total - result.failed - result.skipped;
336
+ }
337
+ // Parse failure details
338
+ const failurePattern = /Failed tests:\s*([\s\S]*?)(?=\n\n|Tests run:|BUILD)/;
339
+ const failureBlock = stdout.match(failurePattern);
340
+ if (failureBlock) {
341
+ const testFailurePattern = /\s*(\S+)\((\S+)\):\s*(.+)/g;
342
+ let match;
343
+ while ((match = testFailurePattern.exec(failureBlock[1])) !== null) {
344
+ result.failures.push({
345
+ testName: match[1],
346
+ file: match[2],
347
+ error: match[3],
348
+ });
349
+ }
350
+ }
351
+ return result;
352
+ },
353
+ },
354
+ // Playwright (E2E)
355
+ {
356
+ name: 'playwright',
357
+ language: 'javascript',
358
+ configFiles: [
359
+ 'playwright.config.js',
360
+ 'playwright.config.cjs',
361
+ 'playwright.config.mjs',
362
+ 'playwright.config.ts',
363
+ 'playwright.config.mts',
364
+ ],
365
+ commands: {
366
+ run: ['npx', 'playwright', 'test', '--reporter=json'],
367
+ watch: ['npx', 'playwright', 'test', '--ui'],
368
+ coverage: ['npx', 'playwright', 'test', '--coverage'],
369
+ },
370
+ parseOutput: (stdout, _stderr, rootDir) => {
371
+ const result = {
372
+ total: 0,
373
+ passed: 0,
374
+ failed: 0,
375
+ skipped: 0,
376
+ duration: 0,
377
+ failures: [],
378
+ };
379
+ try {
380
+ const data = JSON.parse(stdout);
381
+ if (data.stats) {
382
+ result.total = data.stats.total;
383
+ result.passed = data.stats.expected;
384
+ result.failed = data.stats.unexpected;
385
+ result.skipped = data.stats.skipped;
386
+ result.duration = data.stats.duration;
387
+ }
388
+ }
389
+ catch {
390
+ // Fallback text parse
391
+ const passedMatch = stdout.match(/(\d+) passed/);
392
+ const failedMatch = stdout.match(/(\d+) failed/);
393
+ const skippedMatch = stdout.match(/(\d+) skipped/);
394
+ result.passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
395
+ result.failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
396
+ result.skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
397
+ result.total = result.passed + result.failed + result.skipped;
398
+ }
399
+ return result;
400
+ },
401
+ },
402
+ // Cypress (E2E)
403
+ {
404
+ name: 'cypress',
405
+ language: 'javascript',
406
+ configFiles: [
407
+ 'cypress.config.js',
408
+ 'cypress.config.cjs',
409
+ 'cypress.config.mjs',
410
+ 'cypress.config.ts',
411
+ 'cypress.json',
412
+ ],
413
+ commands: {
414
+ run: ['npx', 'cypress', 'run', '--reporter=json'],
415
+ watch: ['npx', 'cypress', 'open'],
416
+ coverage: ['npx', 'cypress', 'run', '--coverage'],
417
+ },
418
+ parseOutput: (stdout, _stderr, rootDir) => {
419
+ const result = {
420
+ total: 0,
421
+ passed: 0,
422
+ failed: 0,
423
+ skipped: 0,
424
+ duration: 0,
425
+ failures: [],
426
+ };
427
+ try {
428
+ const data = JSON.parse(stdout);
429
+ result.total = data.totalTests ?? 0;
430
+ result.passed = data.totalPassed ?? 0;
431
+ result.failed = data.totalFailed ?? 0;
432
+ result.skipped = data.totalPending ?? 0;
433
+ result.duration = data.totalDuration ?? 0;
434
+ if (data.runs) {
435
+ for (const run of data.runs) {
436
+ for (const test of run.tests) {
437
+ if (test.state === 'failed') {
438
+ result.failures.push({
439
+ testName: test.title.join(' > '),
440
+ file: run.spec.name,
441
+ error: test.displayError ?? 'Test failed',
442
+ });
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ catch {
449
+ // Parse error
450
+ }
451
+ return result;
452
+ },
453
+ },
454
+ ];
455
+ // Default configuration
456
+ const DEFAULT_TESTING_CONFIG = {
457
+ enabled: true,
458
+ autoRunOnChange: false,
459
+ runOnSave: false,
460
+ coverageThreshold: 80,
461
+ timeout: 120_000,
462
+ relatedTestsOnly: false,
463
+ };
464
+ // Test file naming conventions
465
+ const TEST_FILE_PATTERNS = {
466
+ javascript: [
467
+ /\.test\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$/,
468
+ /\.spec\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$/,
469
+ /__tests__\/.*\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$/,
470
+ ],
471
+ python: [
472
+ /test_.*\.py$/,
473
+ /.*_test\.py$/,
474
+ ],
475
+ go: [
476
+ /_test\.go$/,
477
+ ],
478
+ rust: [], // Rust tests are inline
479
+ java: [
480
+ /Test\.java$/,
481
+ /Tests\.java$/,
482
+ /IT\.java$/, // Integration test
483
+ ],
484
+ };
485
+ // Source file → test file mapping conventions
486
+ const SOURCE_TO_TEST_MAP = {
487
+ javascript: [
488
+ (src) => {
489
+ const base = src.replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
490
+ return [
491
+ `${base}.test.${src.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/)?.[1] ?? 'ts'}`,
492
+ `${base}.spec.${src.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/)?.[1] ?? 'ts'}`,
493
+ ];
494
+ },
495
+ (src) => {
496
+ const base = basename(src).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
497
+ const dir = dirname(src);
498
+ return [
499
+ join(dir, '__tests__', `${base}.test.ts`),
500
+ join(dir, '__tests__', `${base}.spec.ts`),
501
+ ];
502
+ },
503
+ ],
504
+ python: [
505
+ (src) => {
506
+ const base = src.replace(/\.py$/, '');
507
+ return [`test_${base}.py`, `${base}_test.py`];
508
+ },
509
+ ],
510
+ go: [
511
+ (src) => {
512
+ const base = src.replace(/\.go$/, '');
513
+ return [`${base}_test.go`];
514
+ },
515
+ ],
516
+ rust: [], // Inline tests
517
+ java: [
518
+ (src) => {
519
+ const base = src.replace(/\.java$/, '');
520
+ return [`${base}Test.java`, `${base}Tests.java`];
521
+ },
522
+ ],
523
+ };
524
+ // -----------------------------------------------------------
525
+ // TestingIntegration class
526
+ // -----------------------------------------------------------
527
+ export class TestingIntegration {
528
+ config;
529
+ projectRoot;
530
+ detectedFramework = null;
531
+ frameworkDetected = false;
532
+ cachedResults = null;
533
+ callbacks = [];
534
+ watchProcess = null;
535
+ constructor(projectRoot, config) {
536
+ this.projectRoot = projectRoot ?? process.cwd();
537
+ this.config = { ...DEFAULT_TESTING_CONFIG, ...config };
538
+ }
539
+ // ----------------------------------------------------------
540
+ // Public API
541
+ // ----------------------------------------------------------
542
+ /**
543
+ * Run tests with auto-detection of the test framework.
544
+ * Optionally target a specific test path or pass run options.
545
+ */
546
+ async runTests(testPath, options) {
547
+ if (!this.config.enabled) {
548
+ return this.emptyResult('disabled');
549
+ }
550
+ this.ensureDetected();
551
+ if (!this.detectedFramework) {
552
+ return this.emptyResult('none');
553
+ }
554
+ const framework = this.detectedFramework;
555
+ const timeout = options?.timeout ?? this.config.timeout;
556
+ const startTime = Date.now();
557
+ // Build command
558
+ let commandParts;
559
+ if (options?.coverage) {
560
+ commandParts = [...framework.commands.coverage];
561
+ }
562
+ else {
563
+ commandParts = [...framework.commands.run];
564
+ }
565
+ if (testPath) {
566
+ commandParts.push(resolve(this.projectRoot, testPath));
567
+ }
568
+ if (options?.filter) {
569
+ commandParts.push(options.filter);
570
+ }
571
+ if (options?.verbose && framework.name === 'jest') {
572
+ commandParts.push('--verbose');
573
+ }
574
+ try {
575
+ const { stdout, stderr } = await this.execCommand(commandParts, this.projectRoot, timeout);
576
+ const parsed = framework.parseOutput(stdout, stderr, this.projectRoot);
577
+ const duration = Date.now() - startTime;
578
+ const result = {
579
+ success: parsed.failed === 0,
580
+ totalTests: parsed.total,
581
+ passed: parsed.passed,
582
+ failed: parsed.failed,
583
+ skipped: parsed.skipped,
584
+ duration: duration || parsed.duration,
585
+ framework: framework.name,
586
+ failures: parsed.failures,
587
+ coverage: parsed.coverage,
588
+ };
589
+ // Check coverage threshold
590
+ if (result.coverage && result.coverage.lines < this.config.coverageThreshold) {
591
+ result.success = false;
592
+ }
593
+ this.cachedResults = result;
594
+ this.emit(result);
595
+ return result;
596
+ }
597
+ catch (error) {
598
+ const duration = Date.now() - startTime;
599
+ const errMsg = error instanceof Error ? error.message : String(error);
600
+ const result = {
601
+ success: false,
602
+ totalTests: 0,
603
+ passed: 0,
604
+ failed: 0,
605
+ skipped: 0,
606
+ duration,
607
+ framework: framework.name,
608
+ failures: [{
609
+ testName: 'Framework execution',
610
+ file: '',
611
+ error: `Failed to run tests: ${errMsg}`,
612
+ }],
613
+ };
614
+ this.cachedResults = result;
615
+ this.emit(result);
616
+ return result;
617
+ }
618
+ }
619
+ /**
620
+ * Detect which test framework is configured in the project.
621
+ * Returns the name of the detected framework, or null if none found.
622
+ */
623
+ detectTestFramework(projectRoot) {
624
+ const root = projectRoot ?? this.projectRoot;
625
+ // Check package.json for test scripts
626
+ const pkgJsonPath = join(root, 'package.json');
627
+ if (existsSync(pkgJsonPath)) {
628
+ try {
629
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
630
+ const allDeps = {
631
+ ...pkgJson.dependencies,
632
+ ...pkgJson.devDependencies,
633
+ };
634
+ // Check for vitest first (takes priority over jest if both present)
635
+ if (allDeps['vitest']) {
636
+ const vitestFramework = TEST_FRAMEWORKS.find(f => f.name === 'vitest');
637
+ this.detectedFramework = vitestFramework;
638
+ this.frameworkDetected = true;
639
+ return 'vitest';
640
+ }
641
+ if (allDeps['jest']) {
642
+ const jestFramework = TEST_FRAMEWORKS.find(f => f.name === 'jest');
643
+ this.detectedFramework = jestFramework;
644
+ this.frameworkDetected = true;
645
+ return 'jest';
646
+ }
647
+ // Check for Playwright / Cypress
648
+ if (allDeps['@playwright/test']) {
649
+ const pwFramework = TEST_FRAMEWORKS.find(f => f.name === 'playwright');
650
+ this.detectedFramework = pwFramework;
651
+ this.frameworkDetected = true;
652
+ return 'playwright';
653
+ }
654
+ if (allDeps['cypress']) {
655
+ const cypressFramework = TEST_FRAMEWORKS.find(f => f.name === 'cypress');
656
+ this.detectedFramework = cypressFramework;
657
+ this.frameworkDetected = true;
658
+ return 'cypress';
659
+ }
660
+ }
661
+ catch {
662
+ // Ignore parse errors
663
+ }
664
+ }
665
+ // Check config files for all frameworks
666
+ for (const framework of TEST_FRAMEWORKS) {
667
+ const hasConfig = framework.configFiles.some(cfg => existsSync(join(root, cfg)));
668
+ if (hasConfig) {
669
+ this.detectedFramework = framework;
670
+ this.frameworkDetected = true;
671
+ return framework.name;
672
+ }
673
+ }
674
+ this.frameworkDetected = true;
675
+ return null;
676
+ }
677
+ /**
678
+ * Run tests related to a changed file.
679
+ * Attempts to find and run only the test files that cover the changed source.
680
+ */
681
+ async runRelatedTests(filePath) {
682
+ this.ensureDetected();
683
+ if (!this.detectedFramework) {
684
+ return this.emptyResult('none');
685
+ }
686
+ const lang = this.getLanguageForFile(filePath);
687
+ if (!lang) {
688
+ // If we can't determine language, run all tests
689
+ return this.runTests();
690
+ }
691
+ // Find related test files
692
+ const relatedTestFiles = this.findRelatedTestFiles(filePath, lang);
693
+ if (relatedTestFiles.length === 0) {
694
+ // No related tests found — run the full suite if not in related-only mode
695
+ if (!this.config.relatedTestsOnly) {
696
+ return this.runTests();
697
+ }
698
+ return this.emptyResult('no-related-tests');
699
+ }
700
+ // Run each related test file
701
+ const allFailures = [];
702
+ let totalTests = 0;
703
+ let totalPassed = 0;
704
+ let totalFailed = 0;
705
+ let totalSkipped = 0;
706
+ let totalDuration = 0;
707
+ for (const testFile of relatedTestFiles) {
708
+ const absTestPath = resolve(this.projectRoot, testFile);
709
+ if (!existsSync(absTestPath))
710
+ continue;
711
+ const result = await this.runTests(testFile);
712
+ totalTests += result.totalTests;
713
+ totalPassed += result.passed;
714
+ totalFailed += result.failed;
715
+ totalSkipped += result.skipped;
716
+ totalDuration += result.duration;
717
+ allFailures.push(...result.failures);
718
+ }
719
+ const combinedResult = {
720
+ success: totalFailed === 0,
721
+ totalTests,
722
+ passed: totalPassed,
723
+ failed: totalFailed,
724
+ skipped: totalSkipped,
725
+ duration: totalDuration,
726
+ framework: this.detectedFramework.name,
727
+ failures: allFailures,
728
+ };
729
+ return combinedResult;
730
+ }
731
+ /**
732
+ * Get test coverage report.
733
+ */
734
+ async getCoverage(options) {
735
+ return this.runTests(undefined, { ...options, coverage: true });
736
+ }
737
+ /**
738
+ * Start watch mode for continuous testing.
739
+ * Returns a function that stops the watcher when called.
740
+ */
741
+ async watchTests(testPath) {
742
+ this.ensureDetected();
743
+ if (!this.detectedFramework) {
744
+ throw new Error('No test framework detected');
745
+ }
746
+ const framework = this.detectedFramework;
747
+ if (!framework.commands.watch) {
748
+ throw new Error(`${framework.name} does not support watch mode`);
749
+ }
750
+ const commandParts = [...framework.commands.watch];
751
+ if (testPath) {
752
+ commandParts.push(resolve(this.projectRoot, testPath));
753
+ }
754
+ // Stop any existing watcher
755
+ this.stopWatcher();
756
+ const [cmd, ...args] = commandParts;
757
+ this.watchProcess = spawn(cmd, args, {
758
+ cwd: this.projectRoot,
759
+ stdio: 'pipe',
760
+ shell: true,
761
+ });
762
+ this.watchProcess.stdout?.on('data', (data) => {
763
+ const output = data.toString('utf-8');
764
+ console.log(output);
765
+ });
766
+ this.watchProcess.stderr?.on('data', (data) => {
767
+ const output = data.toString('utf-8');
768
+ console.error(output);
769
+ });
770
+ // Return stop function
771
+ return () => this.stopWatcher();
772
+ }
773
+ /**
774
+ * Generate a test file for a source file based on conventions.
775
+ * Returns the path of the generated test file, or null if generation failed.
776
+ */
777
+ async generateTest(filePath) {
778
+ const lang = this.getLanguageForFile(filePath);
779
+ if (!lang)
780
+ return null;
781
+ const absPath = resolve(this.projectRoot, filePath);
782
+ if (!existsSync(absPath))
783
+ return null;
784
+ // Determine test file path
785
+ const testFilePaths = this.findRelatedTestFilePaths(filePath, lang);
786
+ if (testFilePaths.length === 0)
787
+ return null;
788
+ const testFilePath = testFilePaths[0];
789
+ const fullTestPath = resolve(this.projectRoot, testFilePath);
790
+ // Check if test file already exists
791
+ if (existsSync(fullTestPath)) {
792
+ return fullTestPath;
793
+ }
794
+ // Generate test file content
795
+ const content = this.generateTestContent(filePath, lang, testFilePath);
796
+ if (!content)
797
+ return null;
798
+ try {
799
+ const testDir = dirname(fullTestPath);
800
+ if (!existsSync(testDir)) {
801
+ mkdirSync(testDir, { recursive: true });
802
+ }
803
+ writeFileSync(fullTestPath, content, 'utf-8');
804
+ return fullTestPath;
805
+ }
806
+ catch {
807
+ return null;
808
+ }
809
+ }
810
+ /**
811
+ * Register a callback for test results.
812
+ */
813
+ onTestResult(callback) {
814
+ this.callbacks.push(callback);
815
+ }
816
+ /**
817
+ * Remove a previously registered callback.
818
+ */
819
+ offTestResult(callback) {
820
+ this.callbacks = this.callbacks.filter(cb => cb !== callback);
821
+ }
822
+ /**
823
+ * Get the current testing configuration.
824
+ */
825
+ getConfig() {
826
+ return { ...this.config };
827
+ }
828
+ /**
829
+ * Update the testing configuration.
830
+ */
831
+ updateConfig(updates) {
832
+ Object.assign(this.config, updates);
833
+ }
834
+ /**
835
+ * Get the last cached test result.
836
+ */
837
+ getLastResult() {
838
+ return this.cachedResults;
839
+ }
840
+ /**
841
+ * Get the name of the detected test framework.
842
+ */
843
+ getDetectedFramework() {
844
+ this.ensureDetected();
845
+ return this.detectedFramework?.name ?? null;
846
+ }
847
+ /**
848
+ * Determine the language of a file based on its extension.
849
+ */
850
+ getLanguageForFile(filePath) {
851
+ const ext = extname(filePath).toLowerCase();
852
+ const extMap = {
853
+ '.js': 'javascript',
854
+ '.jsx': 'javascript',
855
+ '.mjs': 'javascript',
856
+ '.cjs': 'javascript',
857
+ '.ts': 'javascript',
858
+ '.tsx': 'javascript',
859
+ '.mts': 'javascript',
860
+ '.cts': 'javascript',
861
+ '.py': 'python',
862
+ '.pyi': 'python',
863
+ '.go': 'go',
864
+ '.rs': 'rust',
865
+ '.java': 'java',
866
+ };
867
+ return extMap[ext] ?? null;
868
+ }
869
+ /**
870
+ * Check if a file is a test file based on naming conventions.
871
+ */
872
+ isTestFile(filePath) {
873
+ const lang = this.getLanguageForFile(filePath);
874
+ if (!lang)
875
+ return false;
876
+ const patterns = TEST_FILE_PATTERNS[lang] ?? [];
877
+ return patterns.some(pattern => pattern.test(basename(filePath)));
878
+ }
879
+ /**
880
+ * Find all test files in the project.
881
+ */
882
+ findTestFiles(directory) {
883
+ const root = directory ?? this.projectRoot;
884
+ const testFiles = [];
885
+ this.walkDir(root, (filePath) => {
886
+ if (this.isTestFile(filePath)) {
887
+ testFiles.push(relative(this.projectRoot, filePath));
888
+ }
889
+ });
890
+ return testFiles;
891
+ }
892
+ /**
893
+ * Print a summary of test results.
894
+ */
895
+ printSummary(result) {
896
+ console.log('');
897
+ console.log('--- Test Summary ---');
898
+ console.log(` Framework: ${result.framework}`);
899
+ console.log(` Duration: ${result.duration}ms`);
900
+ console.log(` Total: ${result.totalTests}`);
901
+ console.log(` Passed: ${result.passed}`);
902
+ console.log(` Failed: ${result.failed}`);
903
+ console.log(` Skipped: ${result.skipped}`);
904
+ console.log(` Status: ${result.success ? 'PASS' : 'FAIL'}`);
905
+ if (result.coverage) {
906
+ console.log('');
907
+ console.log(' Coverage:');
908
+ console.log(` Lines: ${result.coverage.lines.toFixed(1)}%`);
909
+ console.log(` Branches: ${result.coverage.branches.toFixed(1)}%`);
910
+ console.log(` Functions: ${result.coverage.functions.toFixed(1)}%`);
911
+ console.log(` Statements: ${result.coverage.statements.toFixed(1)}%`);
912
+ if (result.coverage.lines < this.config.coverageThreshold) {
913
+ console.log(` ⚠ Below threshold (${this.config.coverageThreshold}%)`);
914
+ }
915
+ }
916
+ if (result.failures.length > 0) {
917
+ console.log('');
918
+ console.log(' Failures:');
919
+ for (const failure of result.failures.slice(0, 20)) {
920
+ console.log(` ✗ ${failure.testName}`);
921
+ if (failure.file)
922
+ console.log(` File: ${failure.file}${failure.line ? `:${failure.line}` : ''}`);
923
+ console.log(` ${failure.error.split('\n')[0]}`);
924
+ if (failure.expected)
925
+ console.log(` Expected: ${failure.expected}`);
926
+ if (failure.actual)
927
+ console.log(` Actual: ${failure.actual}`);
928
+ }
929
+ if (result.failures.length > 20) {
930
+ console.log(` ... and ${result.failures.length - 20} more`);
931
+ }
932
+ }
933
+ console.log('');
934
+ }
935
+ // ----------------------------------------------------------
936
+ // Private helpers
937
+ // ----------------------------------------------------------
938
+ ensureDetected() {
939
+ if (!this.frameworkDetected) {
940
+ this.detectTestFramework(this.projectRoot);
941
+ }
942
+ }
943
+ emit(result) {
944
+ for (const cb of this.callbacks) {
945
+ try {
946
+ cb(result);
947
+ }
948
+ catch {
949
+ // Callback errors should not interrupt the flow
950
+ }
951
+ }
952
+ }
953
+ emptyResult(reason) {
954
+ return {
955
+ success: reason !== 'fail',
956
+ totalTests: 0,
957
+ passed: 0,
958
+ failed: 0,
959
+ skipped: 0,
960
+ duration: 0,
961
+ framework: reason,
962
+ failures: [],
963
+ };
964
+ }
965
+ stopWatcher() {
966
+ if (this.watchProcess) {
967
+ this.watchProcess.kill('SIGTERM');
968
+ this.watchProcess = null;
969
+ }
970
+ }
971
+ findRelatedTestFiles(filePath, lang) {
972
+ const testPaths = [];
973
+ // First check if the file itself is a test file
974
+ if (this.isTestFile(filePath)) {
975
+ testPaths.push(filePath);
976
+ return testPaths;
977
+ }
978
+ // Use source-to-test mapping
979
+ const mappers = SOURCE_TO_TEST_MAP[lang] ?? [];
980
+ for (const mapper of mappers) {
981
+ const candidates = mapper(filePath);
982
+ for (const candidate of candidates) {
983
+ const absCandidate = resolve(this.projectRoot, candidate);
984
+ if (existsSync(absCandidate)) {
985
+ testPaths.push(candidate);
986
+ }
987
+ }
988
+ }
989
+ return testPaths;
990
+ }
991
+ findRelatedTestFilePaths(filePath, lang) {
992
+ const mappers = SOURCE_TO_TEST_MAP[lang] ?? [];
993
+ const paths = [];
994
+ for (const mapper of mappers) {
995
+ paths.push(...mapper(filePath));
996
+ }
997
+ return paths;
998
+ }
999
+ generateTestContent(sourceFilePath, lang, _testFilePath) {
1000
+ const sourceBase = basename(sourceFilePath, extname(sourceFilePath));
1001
+ switch (lang) {
1002
+ case 'javascript':
1003
+ return this.generateJSTestContent(sourceBase, sourceFilePath);
1004
+ case 'python':
1005
+ return this.generatePythonTestContent(sourceBase, sourceFilePath);
1006
+ case 'go':
1007
+ return this.generateGoTestContent(sourceBase, sourceFilePath);
1008
+ case 'rust':
1009
+ return null; // Rust tests are inline
1010
+ case 'java':
1011
+ return this.generateJavaTestContent(sourceBase, sourceFilePath);
1012
+ default:
1013
+ return null;
1014
+ }
1015
+ }
1016
+ generateJSTestContent(sourceBase, sourceFilePath) {
1017
+ const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(sourceFilePath);
1018
+ const ext = isTypeScript ? 'ts' : 'js';
1019
+ const importExt = isTypeScript ? '' : '.js';
1020
+ // Detect if using vitest or jest
1021
+ const framework = this.detectedFramework?.name ?? 'jest';
1022
+ return `// Test file for ${sourceFilePath}
1023
+ // Generated by NeuroCLI Testing Integration
1024
+
1025
+ import { describe, it, expect${framework === 'vitest' ? ', vi' : ''} } from '${framework === 'vitest' ? 'vitest' : '@jest/globals'}';
1026
+
1027
+ describe('${sourceBase}', () => {
1028
+ it('should work correctly', () => {
1029
+ // TODO: Implement test
1030
+ expect(true).toBe(true);
1031
+ });
1032
+ });
1033
+ `;
1034
+ }
1035
+ generatePythonTestContent(sourceBase, _sourceFilePath) {
1036
+ return `# Test file for ${sourceBase}
1037
+ # Generated by NeuroCLI Testing Integration
1038
+
1039
+ import pytest
1040
+
1041
+
1042
+ class Test${this.pascalCase(sourceBase)}:
1043
+ """Tests for ${sourceBase} module."""
1044
+
1045
+ def test_placeholder(self):
1046
+ """TODO: Implement test."""
1047
+ assert True
1048
+ `;
1049
+ }
1050
+ generateGoTestContent(sourceBase, sourceFilePath) {
1051
+ const packageName = dirname(sourceFilePath).split('/').pop() ?? 'main';
1052
+ return `package ${packageName}
1053
+
1054
+ // Test file for ${sourceFilePath}
1055
+ // Generated by NeuroCLI Testing Integration
1056
+
1057
+ import "testing"
1058
+
1059
+ func Test${this.pascalCase(sourceBase)}(t *testing.T) {
1060
+ // TODO: Implement test
1061
+ if true != true {
1062
+ t.Fatal("placeholder test failed")
1063
+ }
1064
+ }
1065
+ `;
1066
+ }
1067
+ generateJavaTestContent(sourceBase, _sourceFilePath) {
1068
+ const className = this.pascalCase(sourceBase);
1069
+ return `// Test file for ${className}
1070
+ // Generated by NeuroCLI Testing Integration
1071
+
1072
+ import org.junit.jupiter.api.Test;
1073
+ import static org.junit.jupiter.api.Assertions.*;
1074
+
1075
+ class ${className}Test {
1076
+
1077
+ @Test
1078
+ void testPlaceholder() {
1079
+ // TODO: Implement test
1080
+ assertTrue(true);
1081
+ }
1082
+ }
1083
+ `;
1084
+ }
1085
+ pascalCase(str) {
1086
+ return str
1087
+ .split(/[-_]/)
1088
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
1089
+ .join('');
1090
+ }
1091
+ walkDir(dir, callback) {
1092
+ if (!existsSync(dir))
1093
+ return;
1094
+ const entries = readdirSync(dir, { withFileTypes: true });
1095
+ for (const entry of entries) {
1096
+ const fullPath = join(dir, entry.name);
1097
+ // Skip common non-source directories
1098
+ if (entry.isDirectory()) {
1099
+ if (['node_modules', '.git', 'dist', 'build', '__pycache__', 'target', '.next', 'vendor', '.venv', 'venv'].includes(entry.name)) {
1100
+ continue;
1101
+ }
1102
+ this.walkDir(fullPath, callback);
1103
+ }
1104
+ else if (entry.isFile()) {
1105
+ callback(fullPath);
1106
+ }
1107
+ }
1108
+ }
1109
+ execCommand(commandParts, cwd, timeout) {
1110
+ return new Promise((resolve, reject) => {
1111
+ const [cmd, ...args] = commandParts;
1112
+ try {
1113
+ const result = execSync(`${cmd} ${args.map(a => `"${a}"`).join(' ')}`, {
1114
+ cwd,
1115
+ timeout,
1116
+ encoding: 'utf-8',
1117
+ maxBuffer: 50 * 1024 * 1024,
1118
+ stdio: ['pipe', 'pipe', 'pipe'],
1119
+ });
1120
+ resolve({ stdout: result, stderr: '', exitCode: 0 });
1121
+ }
1122
+ catch (error) {
1123
+ const execError = error;
1124
+ if (execError.stdout || execError.stderr) {
1125
+ resolve({
1126
+ stdout: typeof execError.stdout === 'string' ? execError.stdout : (execError.stdout?.toString('utf-8') ?? ''),
1127
+ stderr: typeof execError.stderr === 'string' ? execError.stderr : (execError.stderr?.toString('utf-8') ?? ''),
1128
+ exitCode: execError.status ?? 1,
1129
+ });
1130
+ }
1131
+ else {
1132
+ reject(error);
1133
+ }
1134
+ }
1135
+ });
1136
+ }
1137
+ }
1138
+ // -----------------------------------------------------------
1139
+ // Standalone helper functions
1140
+ // -----------------------------------------------------------
1141
+ /**
1142
+ * Parse a Jest error message to extract expected/actual values.
1143
+ */
1144
+ function parseJestError(errorMsg) {
1145
+ const result = {
1146
+ message: errorMsg.split('\n')[0],
1147
+ };
1148
+ // Try to extract expected/actual from Jest assertion errors
1149
+ const expectedMatch = errorMsg.match(/Expected:\s*(.+)/);
1150
+ const receivedMatch = errorMsg.match(/Received:\s*(.+)/);
1151
+ if (expectedMatch)
1152
+ result.expected = expectedMatch[1].trim();
1153
+ if (receivedMatch)
1154
+ result.actual = receivedMatch[1].trim();
1155
+ // Try to extract line number
1156
+ const lineMatch = errorMsg.match(/:(\d+):\d+/);
1157
+ if (lineMatch)
1158
+ result.line = parseInt(lineMatch[1], 10);
1159
+ return result;
1160
+ }
1161
+ /**
1162
+ * Parse Jest coverage map into a CoverageReport.
1163
+ */
1164
+ function parseJestCoverage(coverageMap, rootDir) {
1165
+ let totalStatements = 0;
1166
+ let coveredStatements = 0;
1167
+ let totalBranches = 0;
1168
+ let coveredBranches = 0;
1169
+ let totalFunctions = 0;
1170
+ let coveredFunctions = 0;
1171
+ let totalLines = 0;
1172
+ let coveredLines = 0;
1173
+ const files = [];
1174
+ for (const [filePath, coverage] of Object.entries(coverageMap)) {
1175
+ totalStatements += coverage.statements.total;
1176
+ coveredStatements += coverage.statements.covered;
1177
+ totalBranches += coverage.branches.total;
1178
+ coveredBranches += coverage.branches.covered;
1179
+ totalFunctions += coverage.functions.total;
1180
+ coveredFunctions += coverage.functions.covered;
1181
+ totalLines += coverage.lines.total;
1182
+ coveredLines += coverage.lines.covered;
1183
+ files.push({
1184
+ path: relative(rootDir, filePath),
1185
+ lines: coverage.lines.total > 0 ? (coverage.lines.covered / coverage.lines.total) * 100 : 100,
1186
+ branches: coverage.branches.total > 0 ? (coverage.branches.covered / coverage.branches.total) * 100 : 100,
1187
+ functions: coverage.functions.total > 0 ? (coverage.functions.covered / coverage.functions.total) * 100 : 100,
1188
+ statements: coverage.statements.total > 0 ? (coverage.statements.covered / coverage.statements.total) * 100 : 100,
1189
+ uncoveredLines: [],
1190
+ });
1191
+ }
1192
+ return {
1193
+ lines: totalLines > 0 ? (coveredLines / totalLines) * 100 : 100,
1194
+ branches: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 100,
1195
+ functions: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 100,
1196
+ statements: totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 100,
1197
+ files,
1198
+ };
1199
+ }
1200
+ //# sourceMappingURL=testing.js.map