qa360 1.4.5 → 2.0.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.
Files changed (209) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/ai.d.ts +41 -0
  3. package/dist/commands/ai.js +499 -0
  4. package/dist/commands/ask.js +12 -12
  5. package/dist/commands/coverage.d.ts +8 -0
  6. package/dist/commands/coverage.js +252 -0
  7. package/dist/commands/explain.d.ts +27 -0
  8. package/dist/commands/explain.js +630 -0
  9. package/dist/commands/flakiness.d.ts +73 -0
  10. package/dist/commands/flakiness.js +435 -0
  11. package/dist/commands/generate.d.ts +66 -0
  12. package/dist/commands/generate.js +438 -0
  13. package/dist/commands/init.d.ts +56 -9
  14. package/dist/commands/init.js +217 -10
  15. package/dist/commands/monitor.d.ts +27 -0
  16. package/dist/commands/monitor.js +225 -0
  17. package/dist/commands/ollama.d.ts +40 -0
  18. package/dist/commands/ollama.js +301 -0
  19. package/dist/commands/pack.d.ts +37 -9
  20. package/dist/commands/pack.js +240 -141
  21. package/dist/commands/regression.d.ts +8 -0
  22. package/dist/commands/regression.js +340 -0
  23. package/dist/commands/repair.d.ts +26 -0
  24. package/dist/commands/repair.js +307 -0
  25. package/dist/commands/retry.d.ts +43 -0
  26. package/dist/commands/retry.js +275 -0
  27. package/dist/commands/run.d.ts +8 -3
  28. package/dist/commands/run.js +45 -31
  29. package/dist/commands/slo.d.ts +8 -0
  30. package/dist/commands/slo.js +327 -0
  31. package/dist/core/adapters/playwright-native-api.d.ts +183 -0
  32. package/dist/core/adapters/playwright-native-api.js +461 -0
  33. package/dist/core/adapters/playwright-ui.d.ts +7 -0
  34. package/dist/core/adapters/playwright-ui.js +29 -1
  35. package/dist/core/ai/anthropic-provider.d.ts +50 -0
  36. package/dist/core/ai/anthropic-provider.js +211 -0
  37. package/dist/core/ai/deepseek-provider.d.ts +81 -0
  38. package/dist/core/ai/deepseek-provider.js +254 -0
  39. package/dist/core/ai/index.d.ts +60 -0
  40. package/dist/core/ai/index.js +18 -0
  41. package/dist/core/ai/llm-client.d.ts +45 -0
  42. package/dist/core/ai/llm-client.js +7 -0
  43. package/dist/core/ai/mock-provider.d.ts +49 -0
  44. package/dist/core/ai/mock-provider.js +121 -0
  45. package/dist/core/ai/ollama-provider.d.ts +78 -0
  46. package/dist/core/ai/ollama-provider.js +192 -0
  47. package/dist/core/ai/openai-provider.d.ts +48 -0
  48. package/dist/core/ai/openai-provider.js +188 -0
  49. package/dist/core/ai/provider-factory.d.ts +160 -0
  50. package/dist/core/ai/provider-factory.js +269 -0
  51. package/dist/core/auth/api-key-provider.d.ts +16 -0
  52. package/dist/core/auth/api-key-provider.js +63 -0
  53. package/dist/core/auth/aws-iam-provider.d.ts +35 -0
  54. package/dist/core/auth/aws-iam-provider.js +177 -0
  55. package/dist/core/auth/azure-ad-provider.d.ts +15 -0
  56. package/dist/core/auth/azure-ad-provider.js +99 -0
  57. package/dist/core/auth/basic-auth-provider.d.ts +26 -0
  58. package/dist/core/auth/basic-auth-provider.js +111 -0
  59. package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
  60. package/dist/core/auth/gcp-adc-provider.js +126 -0
  61. package/dist/core/auth/index.d.ts +238 -0
  62. package/dist/core/auth/index.js +82 -0
  63. package/dist/core/auth/jwt-provider.d.ts +19 -0
  64. package/dist/core/auth/jwt-provider.js +160 -0
  65. package/dist/core/auth/manager.d.ts +84 -0
  66. package/dist/core/auth/manager.js +230 -0
  67. package/dist/core/auth/oauth2-provider.d.ts +17 -0
  68. package/dist/core/auth/oauth2-provider.js +114 -0
  69. package/dist/core/auth/totp-provider.d.ts +31 -0
  70. package/dist/core/auth/totp-provider.js +134 -0
  71. package/dist/core/auth/ui-login-provider.d.ts +26 -0
  72. package/dist/core/auth/ui-login-provider.js +198 -0
  73. package/dist/core/cache/index.d.ts +7 -0
  74. package/dist/core/cache/index.js +6 -0
  75. package/dist/core/cache/lru-cache.d.ts +203 -0
  76. package/dist/core/cache/lru-cache.js +397 -0
  77. package/dist/core/coverage/analyzer.d.ts +101 -0
  78. package/dist/core/coverage/analyzer.js +415 -0
  79. package/dist/core/coverage/collector.d.ts +74 -0
  80. package/dist/core/coverage/collector.js +459 -0
  81. package/dist/core/coverage/config.d.ts +37 -0
  82. package/dist/core/coverage/config.js +156 -0
  83. package/dist/core/coverage/index.d.ts +11 -0
  84. package/dist/core/coverage/index.js +15 -0
  85. package/dist/core/coverage/types.d.ts +267 -0
  86. package/dist/core/coverage/types.js +6 -0
  87. package/dist/core/coverage/vault.d.ts +95 -0
  88. package/dist/core/coverage/vault.js +405 -0
  89. package/dist/core/dashboard/assets.d.ts +6 -0
  90. package/dist/core/dashboard/assets.js +690 -0
  91. package/dist/core/dashboard/index.d.ts +6 -0
  92. package/dist/core/dashboard/index.js +5 -0
  93. package/dist/core/dashboard/server.d.ts +72 -0
  94. package/dist/core/dashboard/server.js +354 -0
  95. package/dist/core/dashboard/types.d.ts +70 -0
  96. package/dist/core/dashboard/types.js +5 -0
  97. package/dist/core/discoverer/index.d.ts +115 -0
  98. package/dist/core/discoverer/index.js +250 -0
  99. package/dist/core/flakiness/index.d.ts +228 -0
  100. package/dist/core/flakiness/index.js +384 -0
  101. package/dist/core/generation/code-formatter.d.ts +111 -0
  102. package/dist/core/generation/code-formatter.js +307 -0
  103. package/dist/core/generation/code-generator.d.ts +144 -0
  104. package/dist/core/generation/code-generator.js +293 -0
  105. package/dist/core/generation/generator.d.ts +40 -0
  106. package/dist/core/generation/generator.js +76 -0
  107. package/dist/core/generation/index.d.ts +30 -0
  108. package/dist/core/generation/index.js +28 -0
  109. package/dist/core/generation/pack-generator.d.ts +107 -0
  110. package/dist/core/generation/pack-generator.js +416 -0
  111. package/dist/core/generation/prompt-builder.d.ts +132 -0
  112. package/dist/core/generation/prompt-builder.js +672 -0
  113. package/dist/core/generation/source-analyzer.d.ts +213 -0
  114. package/dist/core/generation/source-analyzer.js +657 -0
  115. package/dist/core/generation/test-optimizer.d.ts +117 -0
  116. package/dist/core/generation/test-optimizer.js +328 -0
  117. package/dist/core/generation/types.d.ts +214 -0
  118. package/dist/core/generation/types.js +4 -0
  119. package/dist/core/index.d.ts +23 -1
  120. package/dist/core/index.js +39 -0
  121. package/dist/core/pack/validator.js +31 -1
  122. package/dist/core/pack-v2/index.d.ts +9 -0
  123. package/dist/core/pack-v2/index.js +8 -0
  124. package/dist/core/pack-v2/loader.d.ts +62 -0
  125. package/dist/core/pack-v2/loader.js +231 -0
  126. package/dist/core/pack-v2/migrator.d.ts +56 -0
  127. package/dist/core/pack-v2/migrator.js +455 -0
  128. package/dist/core/pack-v2/validator.d.ts +61 -0
  129. package/dist/core/pack-v2/validator.js +577 -0
  130. package/dist/core/regression/detector.d.ts +107 -0
  131. package/dist/core/regression/detector.js +497 -0
  132. package/dist/core/regression/index.d.ts +9 -0
  133. package/dist/core/regression/index.js +11 -0
  134. package/dist/core/regression/trend-analyzer.d.ts +102 -0
  135. package/dist/core/regression/trend-analyzer.js +345 -0
  136. package/dist/core/regression/types.d.ts +222 -0
  137. package/dist/core/regression/types.js +7 -0
  138. package/dist/core/regression/vault.d.ts +87 -0
  139. package/dist/core/regression/vault.js +289 -0
  140. package/dist/core/repair/engine/fixer.d.ts +24 -0
  141. package/dist/core/repair/engine/fixer.js +226 -0
  142. package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
  143. package/dist/core/repair/engine/suggestion-engine.js +187 -0
  144. package/dist/core/repair/index.d.ts +10 -0
  145. package/dist/core/repair/index.js +13 -0
  146. package/dist/core/repair/repairer.d.ts +90 -0
  147. package/dist/core/repair/repairer.js +284 -0
  148. package/dist/core/repair/types.d.ts +91 -0
  149. package/dist/core/repair/types.js +6 -0
  150. package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
  151. package/dist/core/repair/utils/error-analyzer.js +264 -0
  152. package/dist/core/retry/flakiness-integration.d.ts +60 -0
  153. package/dist/core/retry/flakiness-integration.js +228 -0
  154. package/dist/core/retry/index.d.ts +14 -0
  155. package/dist/core/retry/index.js +16 -0
  156. package/dist/core/retry/retry-engine.d.ts +80 -0
  157. package/dist/core/retry/retry-engine.js +296 -0
  158. package/dist/core/retry/types.d.ts +178 -0
  159. package/dist/core/retry/types.js +52 -0
  160. package/dist/core/retry/vault.d.ts +77 -0
  161. package/dist/core/retry/vault.js +304 -0
  162. package/dist/core/runner/e2e-helpers.d.ts +102 -0
  163. package/dist/core/runner/e2e-helpers.js +153 -0
  164. package/dist/core/runner/phase3-runner.d.ts +101 -2
  165. package/dist/core/runner/phase3-runner.js +559 -24
  166. package/dist/core/self-healing/assertion-healer.d.ts +97 -0
  167. package/dist/core/self-healing/assertion-healer.js +371 -0
  168. package/dist/core/self-healing/engine.d.ts +122 -0
  169. package/dist/core/self-healing/engine.js +538 -0
  170. package/dist/core/self-healing/index.d.ts +10 -0
  171. package/dist/core/self-healing/index.js +11 -0
  172. package/dist/core/self-healing/selector-healer.d.ts +103 -0
  173. package/dist/core/self-healing/selector-healer.js +372 -0
  174. package/dist/core/self-healing/types.d.ts +152 -0
  175. package/dist/core/self-healing/types.js +6 -0
  176. package/dist/core/slo/config.d.ts +107 -0
  177. package/dist/core/slo/config.js +360 -0
  178. package/dist/core/slo/index.d.ts +11 -0
  179. package/dist/core/slo/index.js +15 -0
  180. package/dist/core/slo/sli-calculator.d.ts +92 -0
  181. package/dist/core/slo/sli-calculator.js +364 -0
  182. package/dist/core/slo/slo-tracker.d.ts +148 -0
  183. package/dist/core/slo/slo-tracker.js +379 -0
  184. package/dist/core/slo/types.d.ts +281 -0
  185. package/dist/core/slo/types.js +7 -0
  186. package/dist/core/slo/vault.d.ts +102 -0
  187. package/dist/core/slo/vault.js +427 -0
  188. package/dist/core/tui/index.d.ts +7 -0
  189. package/dist/core/tui/index.js +6 -0
  190. package/dist/core/tui/monitor.d.ts +92 -0
  191. package/dist/core/tui/monitor.js +271 -0
  192. package/dist/core/tui/renderer.d.ts +33 -0
  193. package/dist/core/tui/renderer.js +218 -0
  194. package/dist/core/tui/types.d.ts +63 -0
  195. package/dist/core/tui/types.js +5 -0
  196. package/dist/core/types/pack-v2.d.ts +425 -0
  197. package/dist/core/types/pack-v2.js +8 -0
  198. package/dist/core/vault/index.d.ts +116 -0
  199. package/dist/core/vault/index.js +400 -5
  200. package/dist/core/watch/index.d.ts +7 -0
  201. package/dist/core/watch/index.js +6 -0
  202. package/dist/core/watch/watch-mode.d.ts +213 -0
  203. package/dist/core/watch/watch-mode.js +389 -0
  204. package/dist/index.js +68 -68
  205. package/dist/utils/config.d.ts +5 -0
  206. package/dist/utils/config.js +136 -0
  207. package/package.json +5 -1
  208. package/dist/core/adapters/playwright-api.d.ts +0 -82
  209. package/dist/core/adapters/playwright-api.js +0 -264
@@ -0,0 +1,657 @@
1
+ /**
2
+ * QA360 Source Code Analyzer
3
+ *
4
+ * Analyzes source code to extract test requirements and generate test specifications.
5
+ * Supports TypeScript, JavaScript, Python, and Go source code.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const analyzer = new SourceAnalyzer();
10
+ * const spec = await analyzer.analyzeFile('./src/userService.ts');
11
+ * const tests = await analyzer.generateTests(spec);
12
+ * ```
13
+ */
14
+ import { readFileSync, existsSync, readdirSync } from 'fs';
15
+ import { join, extname, basename } from 'path';
16
+ /**
17
+ * Source Analyzer
18
+ *
19
+ * Analyzes source code to extract testable elements and suggest tests.
20
+ */
21
+ export class SourceAnalyzer {
22
+ options;
23
+ defaultOptions = {
24
+ includePrivate: false,
25
+ maxComplexity: 10,
26
+ exclude: ['node_modules', 'dist', 'build', '.git', 'coverage'],
27
+ followImports: false
28
+ };
29
+ constructor(options = {}) {
30
+ this.options = { ...this.defaultOptions, ...options };
31
+ }
32
+ /**
33
+ * Analyze a single source file
34
+ */
35
+ analyzeFile(filePath) {
36
+ if (!existsSync(filePath)) {
37
+ throw new Error(`Source file not found: ${filePath}`);
38
+ }
39
+ const content = readFileSync(filePath, 'utf-8');
40
+ const ext = extname(filePath);
41
+ const language = this.getLanguage(ext);
42
+ const analysis = {
43
+ filePath,
44
+ language,
45
+ exports: [],
46
+ functions: [],
47
+ classes: [],
48
+ imports: [],
49
+ hasTests: false,
50
+ complexity: 0,
51
+ linesOfCode: content.split('\n').length
52
+ };
53
+ switch (language) {
54
+ case 'typescript':
55
+ case 'javascript':
56
+ this.analyzeTypeScriptScript(content, analysis);
57
+ break;
58
+ case 'python':
59
+ this.analyzePython(content, analysis);
60
+ break;
61
+ case 'go':
62
+ this.analyzeGo(content, analysis);
63
+ break;
64
+ }
65
+ // Calculate overall complexity
66
+ analysis.complexity = this.calculateComplexity(analysis);
67
+ // Check if test file exists
68
+ analysis.testFilePath = this.findTestFile(filePath);
69
+ analysis.hasTests = analysis.testFilePath !== undefined;
70
+ return analysis;
71
+ }
72
+ /**
73
+ * Analyze a directory recursively
74
+ */
75
+ analyzeDirectory(dirPath) {
76
+ const results = [];
77
+ if (!existsSync(dirPath)) {
78
+ throw new Error(`Directory not found: ${dirPath}`);
79
+ }
80
+ this.traverseDirectory(dirPath, (filePath) => {
81
+ try {
82
+ const ext = extname(filePath);
83
+ if (this.isSourceFile(ext)) {
84
+ results.push(this.analyzeFile(filePath));
85
+ }
86
+ }
87
+ catch (error) {
88
+ // Skip files that fail to analyze
89
+ console.warn(`Warning: Failed to analyze ${filePath}:`, error);
90
+ }
91
+ });
92
+ return results;
93
+ }
94
+ /**
95
+ * Generate test specification from analysis
96
+ */
97
+ generateSpec(analysis) {
98
+ const functionsToTest = analysis.exports.filter(e => {
99
+ const fn = analysis.functions.find(f => f.name === e);
100
+ return fn && fn.isPublic && fn.complexity <= this.options.maxComplexity;
101
+ });
102
+ return {
103
+ type: 'unit',
104
+ name: this.generateTestName(analysis.filePath),
105
+ source: {
106
+ language: analysis.language,
107
+ filePath: analysis.filePath,
108
+ exports: functionsToTest
109
+ },
110
+ framework: this.recommendFramework(analysis.language)
111
+ };
112
+ }
113
+ /**
114
+ * Generate test suggestions
115
+ */
116
+ generateSuggestions(analysis) {
117
+ const suggestions = [];
118
+ // Suggest unit tests for each public function
119
+ for (const fn of analysis.functions) {
120
+ if (!fn.isPublic)
121
+ continue;
122
+ const complexity = fn.complexity;
123
+ let priority = 'low';
124
+ if (complexity > this.options.maxComplexity) {
125
+ priority = 'high';
126
+ }
127
+ else if (complexity > 5) {
128
+ priority = 'medium';
129
+ }
130
+ suggestions.push({
131
+ type: 'unit',
132
+ priority,
133
+ description: `Test function ${fn.name}`,
134
+ functionName: fn.name,
135
+ assertions: this.generateAssertions(fn),
136
+ mocks: this.generateMocks(fn, analysis.imports)
137
+ });
138
+ }
139
+ // Suggest integration tests for classes with multiple methods
140
+ for (const cls of analysis.classes) {
141
+ if (cls.methods.length >= 3) {
142
+ suggestions.push({
143
+ type: 'integration',
144
+ priority: 'medium',
145
+ description: `Test class ${cls.name} interactions`,
146
+ assertions: ['class instance created', 'methods work together'],
147
+ mocks: this.generateMocksForClass(cls, analysis.imports)
148
+ });
149
+ }
150
+ }
151
+ // Suggest E2E tests if external APIs are used
152
+ const hasExternalCalls = analysis.imports.some(i => i.isExternal && this.isHttpModule(i.module));
153
+ if (hasExternalCalls) {
154
+ suggestions.push({
155
+ type: 'e2e',
156
+ priority: 'low',
157
+ description: 'Test external API integrations',
158
+ assertions: ['API calls succeed', 'error handling works'],
159
+ mocks: []
160
+ });
161
+ }
162
+ return suggestions.sort((a, b) => {
163
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
164
+ return priorityOrder[a.priority] - priorityOrder[b.priority];
165
+ });
166
+ }
167
+ /**
168
+ * Generate API spec from Express/HTTP handlers
169
+ */
170
+ generateApiSpec(analysis) {
171
+ const endpoints = [];
172
+ const baseUrl = this.extractBaseUrl(analysis);
173
+ for (const fn of analysis.functions) {
174
+ const endpoint = this.extractEndpoint(fn, analysis);
175
+ if (endpoint) {
176
+ endpoints.push(endpoint);
177
+ }
178
+ }
179
+ for (const cls of analysis.classes) {
180
+ for (const method of cls.methods) {
181
+ const endpoint = this.extractEndpoint(method, analysis);
182
+ if (endpoint) {
183
+ endpoints.push(endpoint);
184
+ }
185
+ }
186
+ }
187
+ if (endpoints.length === 0) {
188
+ return null;
189
+ }
190
+ return {
191
+ type: 'api',
192
+ name: `${basename(analysis.filePath)} API`,
193
+ baseUrl,
194
+ endpoints
195
+ };
196
+ }
197
+ /**
198
+ * Analyze TypeScript/JavaScript code
199
+ */
200
+ analyzeTypeScriptScript(content, analysis) {
201
+ const lines = content.split('\n');
202
+ // Extract exports
203
+ const exportRegex = /export\s+(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|function))/g;
204
+ for (const match of content.matchAll(exportRegex)) {
205
+ const name = match[1] || match[2];
206
+ if (name && !analysis.exports.includes(name)) {
207
+ analysis.exports.push(name);
208
+ }
209
+ }
210
+ // Extract class declarations
211
+ const classRegex = /export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{/g;
212
+ for (const match of content.matchAll(classRegex)) {
213
+ const className = match[1];
214
+ const extendsClass = match[2];
215
+ const classInfo = {
216
+ name: className,
217
+ methods: [],
218
+ properties: [],
219
+ extends: extendsClass,
220
+ lineNumber: this.findLineNumber(content, match.index)
221
+ };
222
+ // Extract class methods
223
+ const classContent = this.extractClassContent(content, match.index);
224
+ const methodRegex = /(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\w+))?/g;
225
+ for (const methodMatch of classContent.matchAll(methodRegex)) {
226
+ if (methodMatch[1] === 'constructor')
227
+ continue;
228
+ classInfo.methods.push({
229
+ name: methodMatch[1],
230
+ params: this.parseParams(methodMatch[2]),
231
+ returnType: methodMatch[3],
232
+ isAsync: classContent.includes('async') && classContent.indexOf('async') < classContent.indexOf(methodMatch[1]),
233
+ isPublic: !classContent.slice(0, methodMatch.index).includes('private'),
234
+ lineNumber: analysis.linesOfCode,
235
+ complexity: this.estimateFunctionComplexity(classContent)
236
+ });
237
+ }
238
+ analysis.classes.push(classInfo);
239
+ }
240
+ // Extract function declarations
241
+ const fnRegex = /((?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)(?:\s*:\s*\w+)?)/g;
242
+ for (const match of content.matchAll(fnRegex)) {
243
+ const fullMatch = match[1];
244
+ const fnNameMatch = /function\s+(\w+)/.exec(fullMatch);
245
+ const paramsMatch = /\(([^)]*)\)/.exec(fullMatch);
246
+ const returnMatch = /:\s*(\w+)/.exec(fullMatch);
247
+ if (fnNameMatch) {
248
+ // Get more context for complexity estimation (function body)
249
+ const fnStart = match.index;
250
+ const fnEnd = Math.min(fnStart + 1000, content.length); // Look ahead up to 1000 chars
251
+ const fnContext = content.slice(fnStart, fnEnd);
252
+ analysis.functions.push({
253
+ name: fnNameMatch[1],
254
+ params: paramsMatch ? this.parseParams(paramsMatch[1]) : [],
255
+ returnType: returnMatch ? returnMatch[1] : undefined,
256
+ isAsync: fullMatch.includes('async'),
257
+ isPublic: this.options.includePrivate || !fullMatch.includes('private'),
258
+ lineNumber: this.findLineNumber(content, match.index),
259
+ complexity: this.estimateFunctionComplexity(fnContext)
260
+ });
261
+ }
262
+ }
263
+ // Extract imports
264
+ const importRegex = /import\s+(?:(\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
265
+ for (const match of content.matchAll(importRegex)) {
266
+ const module = match[2];
267
+ const imports = match[1] ? match[1].replace(/[{}*\s]/g, '').split(',').filter(Boolean) : [module.split('/').pop() || module];
268
+ analysis.imports.push({
269
+ module,
270
+ imports,
271
+ isExternal: this.isExternalModule(module)
272
+ });
273
+ }
274
+ // Check for require statements
275
+ const requireRegex = /(?:const|let|var)\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g;
276
+ for (const match of content.matchAll(requireRegex)) {
277
+ analysis.imports.push({
278
+ module: match[2],
279
+ imports: [match[1]],
280
+ isExternal: this.isExternalModule(match[2])
281
+ });
282
+ }
283
+ }
284
+ /**
285
+ * Analyze Python code
286
+ */
287
+ analyzePython(content, analysis) {
288
+ // Extract class definitions
289
+ const classRegex = /^class\s+(\w+)(?:\(([^)]+)\))?\s*:/gm;
290
+ for (const match of content.matchAll(classRegex)) {
291
+ const className = match[1];
292
+ const baseClass = match[2];
293
+ analysis.classes.push({
294
+ name: className,
295
+ methods: [],
296
+ properties: [],
297
+ extends: baseClass,
298
+ lineNumber: this.findLineNumber(content, match.index)
299
+ });
300
+ }
301
+ // Extract function definitions
302
+ const fnRegex = /^def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?:/gm;
303
+ for (const match of content.matchAll(fnRegex)) {
304
+ const isPrivate = match[1].startsWith('_');
305
+ analysis.functions.push({
306
+ name: match[1],
307
+ params: this.parseParams(match[2]),
308
+ returnType: match[3],
309
+ isAsync: content.slice(0, match.index).trimEnd().endsWith('async'),
310
+ isPublic: this.options.includePrivate || !isPrivate,
311
+ lineNumber: this.findLineNumber(content, match.index),
312
+ complexity: this.estimateFunctionComplexity(content.slice(match.index, Math.min(match.index + 500, content.length)))
313
+ });
314
+ // Top-level functions are exports
315
+ if (!isPrivate && !content.slice(0, match.index).includes('class ')) {
316
+ analysis.exports.push(match[1]);
317
+ }
318
+ }
319
+ // Extract imports
320
+ const importRegex = /^(?:from\s+(\S+)\s+)?import\s+(.+)/gm;
321
+ for (const match of content.matchAll(importRegex)) {
322
+ const module = match[1] || match[2].split('.')[0];
323
+ const imports = match[1] ? match[2].split(',').map(s => s.trim()) : [module];
324
+ analysis.imports.push({
325
+ module,
326
+ imports,
327
+ isExternal: !module.startsWith('.')
328
+ });
329
+ }
330
+ }
331
+ /**
332
+ * Analyze Go code
333
+ */
334
+ analyzeGo(content, analysis) {
335
+ // Extract function definitions
336
+ const fnRegex = /func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?/g;
337
+ for (const match of content.matchAll(fnRegex)) {
338
+ const isPrivate = match[1] && match[1][0] === match[1][0].toLowerCase();
339
+ analysis.exports.push(match[1]);
340
+ analysis.functions.push({
341
+ name: match[1],
342
+ params: this.parseParams(match[2]),
343
+ returnType: match[3],
344
+ isAsync: false,
345
+ isPublic: this.options.includePrivate || !isPrivate,
346
+ lineNumber: this.findLineNumber(content, match.index),
347
+ complexity: this.estimateFunctionComplexity(content.slice(match.index, match.index + 500))
348
+ });
349
+ }
350
+ // Extract type definitions (similar to classes)
351
+ const typeRegex = /type\s+(\w+)\s+struct\s*\{([^}]*)\}/g;
352
+ for (const match of content.matchAll(typeRegex)) {
353
+ analysis.classes.push({
354
+ name: match[1],
355
+ methods: [],
356
+ properties: match[2].split('\n').map(l => l.trim()).filter(Boolean),
357
+ lineNumber: this.findLineNumber(content, match.index)
358
+ });
359
+ }
360
+ // Extract imports
361
+ const importRegex = /import\s+(?:\(\s*)?["']([^"']+)["']/g;
362
+ for (const match of content.matchAll(importRegex)) {
363
+ analysis.imports.push({
364
+ module: match[1],
365
+ imports: [],
366
+ isExternal: !match[1].startsWith('.')
367
+ });
368
+ }
369
+ }
370
+ /**
371
+ * Get language from file extension
372
+ */
373
+ getLanguage(ext) {
374
+ const langMap = {
375
+ '.ts': 'typescript',
376
+ '.tsx': 'typescript',
377
+ '.js': 'javascript',
378
+ '.jsx': 'javascript',
379
+ '.py': 'python',
380
+ '.go': 'go'
381
+ };
382
+ return langMap[ext] || 'typescript';
383
+ }
384
+ /**
385
+ * Check if file is a source file
386
+ */
387
+ isSourceFile(ext) {
388
+ return ['.ts', '.tsx', '.js', '.jsx', '.py', '.go'].includes(ext);
389
+ }
390
+ /**
391
+ * Traverse directory recursively
392
+ */
393
+ traverseDirectory(dir, callback) {
394
+ const entries = readdirSync(dir, { withFileTypes: true });
395
+ for (const entry of entries) {
396
+ const fullPath = join(dir, entry.name);
397
+ // Skip excluded directories
398
+ if (this.options.exclude.some(pattern => fullPath.includes(pattern))) {
399
+ continue;
400
+ }
401
+ if (entry.isDirectory()) {
402
+ this.traverseDirectory(fullPath, callback);
403
+ }
404
+ else if (entry.isFile()) {
405
+ callback(fullPath);
406
+ }
407
+ }
408
+ }
409
+ /**
410
+ * Find test file for a source file
411
+ */
412
+ findTestFile(filePath) {
413
+ const dir = join(filePath, '..');
414
+ const base = basename(filePath, extname(filePath));
415
+ const ext = extname(filePath);
416
+ const testPatterns = [
417
+ join(dir, `${base}.test.ts`),
418
+ join(dir, `${base}.spec.ts`),
419
+ join(dir, `${base}.test.js`),
420
+ join(dir, `${base}.spec.js`),
421
+ join(dir, '__tests__', `${base}.test.ts`),
422
+ join(dir, '__tests__', `${base}.spec.ts`),
423
+ join(dir, 'tests', `${base}.test.ts`)
424
+ ];
425
+ for (const pattern of testPatterns) {
426
+ if (existsSync(pattern)) {
427
+ return pattern;
428
+ }
429
+ }
430
+ return undefined;
431
+ }
432
+ /**
433
+ * Calculate overall complexity score
434
+ */
435
+ calculateComplexity(analysis) {
436
+ let complexity = 0;
437
+ for (const fn of analysis.functions) {
438
+ complexity += fn.complexity;
439
+ }
440
+ for (const cls of analysis.classes) {
441
+ for (const method of cls.methods) {
442
+ complexity += method.complexity;
443
+ }
444
+ }
445
+ return complexity;
446
+ }
447
+ /**
448
+ * Estimate function complexity
449
+ */
450
+ estimateFunctionComplexity(code) {
451
+ let complexity = 1; // Base complexity
452
+ // Count decision points
453
+ const decisionPatterns = [
454
+ /\bif\b/g,
455
+ /\belse\b/g,
456
+ /\bfor\b/g,
457
+ /\bwhile\b/g,
458
+ /\bswitch\b/g,
459
+ /\bcase\b/g,
460
+ /\bcatch\b/g,
461
+ /\?\s*:/g, // ternary
462
+ /\|\|/g, // logical OR
463
+ /&&/g // logical AND
464
+ ];
465
+ for (const pattern of decisionPatterns) {
466
+ const matches = code.match(pattern);
467
+ if (matches) {
468
+ complexity += matches.length;
469
+ }
470
+ }
471
+ return complexity;
472
+ }
473
+ /**
474
+ * Parse function parameters
475
+ */
476
+ parseParams(paramsStr) {
477
+ if (!paramsStr.trim())
478
+ return [];
479
+ return paramsStr.split(',').map(p => p.trim()).filter(Boolean);
480
+ }
481
+ /**
482
+ * Extract class content
483
+ */
484
+ extractClassContent(content, startIndex) {
485
+ let depth = 0;
486
+ let inClass = false;
487
+ let result = '';
488
+ for (let i = startIndex; i < content.length; i++) {
489
+ const char = content[i];
490
+ if (char === '{') {
491
+ depth++;
492
+ inClass = true;
493
+ }
494
+ else if (char === '}') {
495
+ depth--;
496
+ if (inClass && depth === 0) {
497
+ break;
498
+ }
499
+ }
500
+ if (inClass) {
501
+ result += char;
502
+ }
503
+ }
504
+ return result;
505
+ }
506
+ /**
507
+ * Find line number for a position in content
508
+ */
509
+ findLineNumber(content, index) {
510
+ return content.slice(0, index).split('\n').length;
511
+ }
512
+ /**
513
+ * Generate test name from file path
514
+ */
515
+ generateTestName(filePath) {
516
+ const base = basename(filePath, extname(filePath));
517
+ return base.replace(/[^a-zA-Z0-9]/g, ' ') + ' Tests';
518
+ }
519
+ /**
520
+ * Recommend testing framework based on language
521
+ */
522
+ recommendFramework(language) {
523
+ const frameworkMap = {
524
+ typescript: 'vitest',
525
+ javascript: 'vitest',
526
+ python: 'pytest',
527
+ go: 'go test'
528
+ };
529
+ return frameworkMap[language] || 'vitest';
530
+ }
531
+ /**
532
+ * Generate assertions for a function
533
+ */
534
+ generateAssertions(fn) {
535
+ const assertions = [];
536
+ if (fn.isAsync) {
537
+ assertions.push('function resolves', 'function completes without timeout');
538
+ }
539
+ if (fn.returnType?.toLowerCase().includes('void') || fn.params.length === 0) {
540
+ assertions.push('function executes without error');
541
+ }
542
+ else {
543
+ assertions.push('function returns expected value');
544
+ }
545
+ if (fn.params.length > 0) {
546
+ assertions.push('function handles invalid input');
547
+ }
548
+ return assertions;
549
+ }
550
+ /**
551
+ * Generate mocks for a function
552
+ */
553
+ generateMocks(fn, imports) {
554
+ const mocks = [];
555
+ for (const imp of imports) {
556
+ if (imp.isExternal && this.isHttpModule(imp.module)) {
557
+ mocks.push(imp.module);
558
+ }
559
+ }
560
+ return mocks;
561
+ }
562
+ /**
563
+ * Generate mocks for a class
564
+ */
565
+ generateMocksForClass(cls, imports) {
566
+ const mocks = [];
567
+ for (const imp of imports) {
568
+ if (imp.isExternal) {
569
+ mocks.push(imp.module);
570
+ }
571
+ }
572
+ return mocks;
573
+ }
574
+ /**
575
+ * Check if module is an HTTP module
576
+ */
577
+ isHttpModule(module) {
578
+ const httpModules = ['http', 'https', 'axios', 'fetch', 'node-fetch', 'request', 'supertest'];
579
+ return httpModules.some(m => module.toLowerCase().includes(m));
580
+ }
581
+ /**
582
+ * Check if module is external
583
+ */
584
+ isExternalModule(module) {
585
+ return !module.startsWith('.') && !module.startsWith('/');
586
+ }
587
+ /**
588
+ * Extract base URL from imports
589
+ */
590
+ extractBaseUrl(analysis) {
591
+ // Try to find a base URL in constants or config
592
+ const urlConstants = analysis.imports.filter(i => i.module.toLowerCase().includes('config') ||
593
+ i.module.toLowerCase().includes('constants'));
594
+ if (urlConstants.length > 0) {
595
+ return 'https://api.example.com'; // Placeholder
596
+ }
597
+ return 'https://localhost:3000';
598
+ }
599
+ /**
600
+ * Extract API endpoint from function
601
+ */
602
+ extractEndpoint(fn, analysis) {
603
+ const name = fn.name.toLowerCase();
604
+ // Check for HTTP method prefixes
605
+ const httpMethods = ['get', 'post', 'put', 'patch', 'delete'];
606
+ let method;
607
+ let path = `/${fn.name}`;
608
+ for (const httpMethod of httpMethods) {
609
+ if (name.startsWith(httpMethod)) {
610
+ method = httpMethod.toUpperCase();
611
+ path = `/${name.slice(httpMethod.length)}`;
612
+ break;
613
+ }
614
+ }
615
+ // Check for Express/Fastify patterns
616
+ if (fn.params.some(p => p.includes('req') || p.includes('request') || p.includes('res') || p.includes('response'))) {
617
+ return {
618
+ method: method || 'GET',
619
+ path,
620
+ description: fn.name,
621
+ expectations: [{ status: 200 }]
622
+ };
623
+ }
624
+ return null;
625
+ }
626
+ /**
627
+ * Get analysis as GenerationSource
628
+ */
629
+ toGenerationSource(analysis) {
630
+ return {
631
+ type: 'code',
632
+ code: readFileSync(analysis.filePath, 'utf-8'),
633
+ language: analysis.language
634
+ };
635
+ }
636
+ }
637
+ /**
638
+ * Convenience function to analyze a source file
639
+ */
640
+ export function analyzeSourceFile(filePath, options) {
641
+ const analyzer = new SourceAnalyzer(options);
642
+ return analyzer.analyzeFile(filePath);
643
+ }
644
+ /**
645
+ * Convenience function to analyze a directory
646
+ */
647
+ export function analyzeSourceDirectory(dirPath, options) {
648
+ const analyzer = new SourceAnalyzer(options);
649
+ return analyzer.analyzeDirectory(dirPath);
650
+ }
651
+ /**
652
+ * Convenience function to generate test spec from analysis
653
+ */
654
+ export function generateTestSpec(analysis) {
655
+ const analyzer = new SourceAnalyzer();
656
+ return analyzer.generateSpec(analysis);
657
+ }