@yuaone/core 0.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 (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,1165 @@
1
+ /**
2
+ * @module test-intelligence
3
+ * @description Test Intelligence System — discovers test files, maps source-to-test coverage,
4
+ * detects affected tests for changed files, identifies coverage gaps, and generates test suggestions.
5
+ *
6
+ * Uses regex-based analysis (no external test runner dependency). Designed for the YUAN coding agent
7
+ * to intelligently run only relevant tests and suggest missing test coverage.
8
+ */
9
+ import { readdir, readFile } from "node:fs/promises";
10
+ import { join, resolve, dirname, extname, basename, relative } from "node:path";
11
+ // ─── Constants ───
12
+ const DEFAULT_TEST_PATTERNS = [
13
+ "**/*.test.ts",
14
+ "**/*.spec.ts",
15
+ "**/*.test.js",
16
+ "**/*.spec.js",
17
+ "**/__tests__/**/*.ts",
18
+ "**/__tests__/**/*.js",
19
+ "**/test/**/*.ts",
20
+ "**/test/**/*.js",
21
+ ];
22
+ const DEFAULT_IGNORE_PATTERNS = [
23
+ "node_modules",
24
+ "dist",
25
+ "build",
26
+ ".git",
27
+ "coverage",
28
+ ".next",
29
+ ".turbo",
30
+ "__pycache__",
31
+ ];
32
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
33
+ /** Regex for test file naming conventions */
34
+ const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
35
+ const TEST_DIR_RE = /(?:^|[/\\])(?:__tests__|test)[/\\]/;
36
+ const INTEGRATION_TEST_RE = /\.(?:integration|e2e)\.(test|spec)\.(ts|tsx|js|jsx)$/;
37
+ // ─── Import parsing regex (mirrors dependency-analyzer.ts) ───
38
+ const IMPORT_RE = /import\s+(?:type\s+)?(?:\{([^}]*)\}|(\w+)|\*\s+as\s+(\w+))\s+from\s+["']([^"']+)["']/g;
39
+ const RE_EXPORT_RE = /export\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
40
+ const REQUIRE_RE = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
41
+ // ─── Test case extraction regex ───
42
+ const IT_RE = /it\(\s*['"`](.+?)['"`]\s*,/g;
43
+ const TEST_RE = /test\(\s*['"`](.+?)['"`]\s*,/g;
44
+ const DESCRIBE_RE = /describe\(\s*['"`](.+?)['"`]\s*,/g;
45
+ // ─── Framework detection regex ───
46
+ const VITEST_IMPORT_RE = /import\s+\{[^}]*\}\s+from\s+['"]vitest['"]/;
47
+ const VITEST_IMPORT_TEST_RE = /import\s+\{?\s*test\s*\}?\s+from\s+['"]vitest['"]/;
48
+ const NODE_TEST_RE = /import\s+(?:\{[^}]*\}|test)\s+from\s+['"]node:test['"]/;
49
+ const CHAI_RE = /require\s*\(\s*['"]chai['"]\s*\)/;
50
+ // ─── Symbol extraction regex (simplified from codebase-context.ts) ───
51
+ const EXPORT_SYMBOL_RE = /export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|const|let|var|interface|type|enum)\s+(\w+)/g;
52
+ /**
53
+ * Test Intelligence System — discovers, maps, and analyzes test coverage.
54
+ *
55
+ * Provides affected test detection, coverage gap analysis, test suggestions,
56
+ * and test command building for the YUAN coding agent.
57
+ */
58
+ export class TestIntelligence {
59
+ config;
60
+ testFiles;
61
+ sourceToTests;
62
+ testHistory;
63
+ constructor(config) {
64
+ this.config = {
65
+ projectPath: resolve(config.projectPath),
66
+ testPatterns: config.testPatterns ?? DEFAULT_TEST_PATTERNS,
67
+ ignorePatterns: config.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS,
68
+ maxTransitiveDepth: config.maxTransitiveDepth ?? 3,
69
+ includeIntegrationTests: config.includeIntegrationTests ?? true,
70
+ };
71
+ this.testFiles = new Map();
72
+ this.sourceToTests = new Map();
73
+ this.testHistory = [];
74
+ }
75
+ // ─── Discovery ───
76
+ /**
77
+ * Scan the project for all test files, parse their content,
78
+ * and build the internal test file index.
79
+ *
80
+ * @returns Array of discovered test files
81
+ */
82
+ async discoverTests() {
83
+ const testPaths = await this.findTestFiles();
84
+ this.testFiles.clear();
85
+ for (const filePath of testPaths) {
86
+ const content = await this.readFile(filePath);
87
+ if (!content)
88
+ continue;
89
+ const framework = this.detectFramework(content);
90
+ const testCases = this.parseTestCases(content, framework);
91
+ const imports = this.parseTestImports(content, filePath);
92
+ const testFile = {
93
+ path: filePath,
94
+ framework,
95
+ testCases,
96
+ imports,
97
+ };
98
+ this.testFiles.set(filePath, testFile);
99
+ }
100
+ return [...this.testFiles.values()];
101
+ }
102
+ /**
103
+ * Detect the test framework used in a file based on its content.
104
+ *
105
+ * Detection priority:
106
+ * 1. Vitest (explicit import from 'vitest')
107
+ * 2. Node.js test runner (import from 'node:test')
108
+ * 3. Mocha (chai require)
109
+ * 4. Jest (describe/it/test without vitest import)
110
+ * 5. Unknown (fallback)
111
+ *
112
+ * @param content - File content to analyze
113
+ * @returns Detected framework identifier
114
+ */
115
+ detectFramework(content) {
116
+ if (VITEST_IMPORT_RE.test(content) || VITEST_IMPORT_TEST_RE.test(content)) {
117
+ return "vitest";
118
+ }
119
+ if (NODE_TEST_RE.test(content)) {
120
+ return "node_test";
121
+ }
122
+ if (CHAI_RE.test(content)) {
123
+ return "mocha";
124
+ }
125
+ // Jest uses describe/it/test as globals (no explicit import needed)
126
+ const hasDescribe = /\bdescribe\s*\(/.test(content);
127
+ const hasItOrTest = /\b(?:it|test)\s*\(/.test(content);
128
+ if (hasDescribe && hasItOrTest) {
129
+ return "jest";
130
+ }
131
+ if (hasItOrTest) {
132
+ return "jest";
133
+ }
134
+ return "unknown";
135
+ }
136
+ /**
137
+ * Extract test cases from file content.
138
+ *
139
+ * Parses `it()`, `test()`, and `describe()` blocks, infers test type
140
+ * from naming patterns, and extracts tags from the description.
141
+ *
142
+ * @param content - File content to parse
143
+ * @param _framework - Framework identifier (reserved for future framework-specific parsing)
144
+ * @returns Array of extracted test cases
145
+ */
146
+ parseTestCases(content, _framework) {
147
+ const cases = [];
148
+ const lines = content.split("\n");
149
+ // Build a line lookup for fast line-number resolution
150
+ const lineOffsets = [];
151
+ let offset = 0;
152
+ for (const line of lines) {
153
+ lineOffsets.push(offset);
154
+ offset += line.length + 1; // +1 for newline
155
+ }
156
+ const getLineNumber = (charIndex) => {
157
+ let lo = 0;
158
+ let hi = lineOffsets.length - 1;
159
+ while (lo <= hi) {
160
+ const mid = (lo + hi) >>> 1;
161
+ if (lineOffsets[mid] <= charIndex) {
162
+ lo = mid + 1;
163
+ }
164
+ else {
165
+ hi = mid - 1;
166
+ }
167
+ }
168
+ return lo; // 1-based
169
+ };
170
+ // Extract describe blocks for context
171
+ const describeNames = [];
172
+ const describeRe = new RegExp(DESCRIBE_RE.source, "g");
173
+ let match;
174
+ while ((match = describeRe.exec(content)) !== null) {
175
+ describeNames.push(match[1]);
176
+ }
177
+ // Extract it() cases
178
+ const itRe = new RegExp(IT_RE.source, "g");
179
+ while ((match = itRe.exec(content)) !== null) {
180
+ const name = match[1];
181
+ const line = getLineNumber(match.index);
182
+ cases.push({
183
+ name,
184
+ line,
185
+ type: this.inferTestType(name, describeNames),
186
+ tags: this.extractTags(name),
187
+ });
188
+ }
189
+ // Extract test() cases
190
+ const testRe = new RegExp(TEST_RE.source, "g");
191
+ while ((match = testRe.exec(content)) !== null) {
192
+ const name = match[1];
193
+ const line = getLineNumber(match.index);
194
+ // Avoid duplicates (test() and it() may overlap in pattern matching)
195
+ if (!cases.some((c) => c.line === line)) {
196
+ cases.push({
197
+ name,
198
+ line,
199
+ type: this.inferTestType(name, describeNames),
200
+ tags: this.extractTags(name),
201
+ });
202
+ }
203
+ }
204
+ return cases;
205
+ }
206
+ /**
207
+ * Build a mapping from source files to their corresponding test files.
208
+ *
209
+ * Uses two strategies:
210
+ * 1. Import-based: checks what each test file imports
211
+ * 2. Convention-based: maps test file names to source files
212
+ *
213
+ * @returns Map from source file path to array of test file paths
214
+ */
215
+ async buildTestMap() {
216
+ if (this.testFiles.size === 0) {
217
+ await this.discoverTests();
218
+ }
219
+ this.sourceToTests.clear();
220
+ for (const [testPath, testFile] of this.testFiles) {
221
+ // Strategy 1: import-based mapping
222
+ for (const importedFile of testFile.imports) {
223
+ if (!this.isTestFile(importedFile)) {
224
+ const existing = this.sourceToTests.get(importedFile) ?? [];
225
+ if (!existing.includes(testPath)) {
226
+ existing.push(testPath);
227
+ }
228
+ this.sourceToTests.set(importedFile, existing);
229
+ }
230
+ }
231
+ // Strategy 2: convention-based mapping
232
+ const sourceFile = this.findSourceForTest(testPath);
233
+ if (sourceFile) {
234
+ const existing = this.sourceToTests.get(sourceFile) ?? [];
235
+ if (!existing.includes(testPath)) {
236
+ existing.push(testPath);
237
+ }
238
+ this.sourceToTests.set(sourceFile, existing);
239
+ }
240
+ }
241
+ return new Map(this.sourceToTests);
242
+ }
243
+ // ─── Affected Tests ───
244
+ /**
245
+ * Find all tests affected by a set of changed files.
246
+ *
247
+ * Algorithm:
248
+ * 1. Direct: test files that directly import any changed file
249
+ * 2. Transitive: test files that depend on changed files through intermediaries
250
+ * 3. Integration: integration/e2e tests in the same module directory
251
+ *
252
+ * Confidence scoring:
253
+ * - 1.0 if only direct tests found
254
+ * - 0.8 if transitive depth <= 2
255
+ * - 0.6 if transitive depth > 2
256
+ * - -0.1 for each changed file without any test coverage
257
+ *
258
+ * @param changedFiles - Array of absolute file paths that changed
259
+ * @returns Affected test result with confidence and reasoning
260
+ */
261
+ async findAffectedTests(changedFiles) {
262
+ if (this.sourceToTests.size === 0) {
263
+ await this.buildTestMap();
264
+ }
265
+ const directTests = new Set();
266
+ const transitiveTests = new Set();
267
+ const integrationTests = new Set();
268
+ const reasoning = [];
269
+ let maxDepthUsed = 0;
270
+ let filesWithoutTests = 0;
271
+ const resolvedChanged = changedFiles.map((f) => resolve(f));
272
+ for (const changedFile of resolvedChanged) {
273
+ // 1. Direct tests
274
+ const directForFile = this.sourceToTests.get(changedFile) ?? [];
275
+ if (directForFile.length === 0 && !this.isTestFile(changedFile)) {
276
+ filesWithoutTests++;
277
+ }
278
+ for (const testPath of directForFile) {
279
+ directTests.add(testPath);
280
+ reasoning.push(`Direct: ${relative(this.config.projectPath, testPath)} imports ${relative(this.config.projectPath, changedFile)}`);
281
+ }
282
+ // If the changed file is itself a test, include it
283
+ if (this.isTestFile(changedFile) && this.testFiles.has(changedFile)) {
284
+ directTests.add(changedFile);
285
+ reasoning.push(`Direct: ${relative(this.config.projectPath, changedFile)} is a test file that was modified`);
286
+ }
287
+ // 2. Transitive tests
288
+ const visited = new Set();
289
+ const transitive = this.findTransitiveTestDeps(changedFile, this.config.maxTransitiveDepth, visited);
290
+ for (const testPath of transitive) {
291
+ if (!directTests.has(testPath)) {
292
+ transitiveTests.add(testPath);
293
+ const depth = visited.size;
294
+ if (depth > maxDepthUsed)
295
+ maxDepthUsed = depth;
296
+ reasoning.push(`Transitive: ${relative(this.config.projectPath, testPath)} depends on ${relative(this.config.projectPath, changedFile)} (via reverse deps)`);
297
+ }
298
+ }
299
+ // 3. Integration tests
300
+ if (this.config.includeIntegrationTests) {
301
+ const integrationForFile = this.findModuleIntegrationTests(changedFile);
302
+ for (const testPath of integrationForFile) {
303
+ if (!directTests.has(testPath) && !transitiveTests.has(testPath)) {
304
+ integrationTests.add(testPath);
305
+ reasoning.push(`Integration: ${relative(this.config.projectPath, testPath)} is an integration test in the same module as ${relative(this.config.projectPath, changedFile)}`);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ // Confidence scoring
311
+ let confidence = 1.0;
312
+ if (transitiveTests.size > 0) {
313
+ confidence = maxDepthUsed > 2 ? 0.6 : 0.8;
314
+ }
315
+ confidence = Math.max(0, confidence - filesWithoutTests * 0.1);
316
+ const allTests = new Set([...directTests, ...transitiveTests, ...integrationTests]);
317
+ return {
318
+ changedFiles: resolvedChanged,
319
+ directTests: [...directTests],
320
+ transitiveTests: [...transitiveTests],
321
+ integrationTests: [...integrationTests],
322
+ totalTests: allTests.size,
323
+ confidence: Math.round(confidence * 100) / 100,
324
+ reasoning,
325
+ };
326
+ }
327
+ /**
328
+ * Quick check: does a source file have any tests?
329
+ *
330
+ * @param sourceFile - Absolute path to the source file
331
+ * @returns True if at least one test file covers this source
332
+ */
333
+ hasTests(sourceFile) {
334
+ const resolved = resolve(sourceFile);
335
+ const tests = this.sourceToTests.get(resolved);
336
+ return tests !== undefined && tests.length > 0;
337
+ }
338
+ /**
339
+ * Get test file paths for a source file.
340
+ *
341
+ * @param sourceFile - Absolute path to the source file
342
+ * @returns Array of absolute test file paths
343
+ */
344
+ getTestsFor(sourceFile) {
345
+ const resolved = resolve(sourceFile);
346
+ return this.sourceToTests.get(resolved) ?? [];
347
+ }
348
+ // ─── Coverage Analysis ───
349
+ /**
350
+ * Find untested code (coverage gaps).
351
+ *
352
+ * For each source file, checks:
353
+ * 1. Whether a corresponding test file exists
354
+ * 2. Whether exported symbols are referenced in any test
355
+ * 3. Whether error cases are tested
356
+ *
357
+ * @param files - Optional list of files to check (defaults to all source files)
358
+ * @returns Array of coverage gaps
359
+ */
360
+ async findCoverageGaps(files) {
361
+ if (this.sourceToTests.size === 0) {
362
+ await this.buildTestMap();
363
+ }
364
+ const sourceFiles = files
365
+ ? files.map((f) => resolve(f))
366
+ : await this.collectSourceFiles();
367
+ const gaps = [];
368
+ for (const filePath of sourceFiles) {
369
+ if (this.isTestFile(filePath))
370
+ continue;
371
+ const content = await this.readFile(filePath);
372
+ if (!content)
373
+ continue;
374
+ const tests = this.sourceToTests.get(filePath) ?? [];
375
+ const relPath = relative(this.config.projectPath, filePath);
376
+ // No test file at all
377
+ if (tests.length === 0) {
378
+ // Extract exported symbols to report gaps
379
+ const symbols = this.extractExportedSymbols(content);
380
+ if (symbols.length > 0) {
381
+ for (const sym of symbols) {
382
+ gaps.push({
383
+ file: filePath,
384
+ symbol: sym.name,
385
+ symbolType: sym.kind,
386
+ line: sym.line,
387
+ reason: "no test file",
388
+ severity: "high",
389
+ suggestion: `Add test for ${sym.kind} '${sym.name}' from ${relPath}`,
390
+ });
391
+ }
392
+ }
393
+ else {
394
+ gaps.push({
395
+ file: filePath,
396
+ symbol: basename(filePath, extname(filePath)),
397
+ symbolType: "module",
398
+ line: 1,
399
+ reason: "no test file",
400
+ severity: "medium",
401
+ suggestion: `Add test file for ${relPath}`,
402
+ });
403
+ }
404
+ continue;
405
+ }
406
+ // Has test files — check if specific exported symbols are tested
407
+ const symbols = this.extractExportedSymbols(content);
408
+ let testContents = "";
409
+ for (const testPath of tests) {
410
+ const tc = await this.readFile(testPath);
411
+ if (tc)
412
+ testContents += "\n" + tc;
413
+ }
414
+ for (const sym of symbols) {
415
+ const symbolRe = new RegExp(`\\b${this.escapeRegex(sym.name)}\\b`);
416
+ if (!symbolRe.test(testContents)) {
417
+ gaps.push({
418
+ file: filePath,
419
+ symbol: sym.name,
420
+ symbolType: sym.kind,
421
+ line: sym.line,
422
+ reason: "untested function",
423
+ severity: sym.exported ? "high" : "low",
424
+ suggestion: `Add test for ${sym.kind} '${sym.name}' in ${relPath}`,
425
+ });
426
+ }
427
+ }
428
+ // Check for error case testing
429
+ const hasThrows = /\bthrow\s+new\b/.test(content);
430
+ const hasErrorTests = /\b(?:toThrow|rejects|throws|expect.*error|expect.*Error)\b/i.test(testContents);
431
+ if (hasThrows && !hasErrorTests) {
432
+ gaps.push({
433
+ file: filePath,
434
+ symbol: basename(filePath, extname(filePath)),
435
+ symbolType: "module",
436
+ line: 1,
437
+ reason: "no error case",
438
+ severity: "medium",
439
+ suggestion: `Add error case tests for ${relPath} (has throw statements but no error assertions in tests)`,
440
+ });
441
+ }
442
+ }
443
+ return gaps;
444
+ }
445
+ /**
446
+ * Suggest tests for a specific source file.
447
+ *
448
+ * Generates test case suggestions based on exported symbols,
449
+ * their types, parameters, and common testing patterns.
450
+ *
451
+ * @param sourceFile - Absolute path to the source file
452
+ * @param symbols - Optional list of specific symbol names to suggest tests for
453
+ * @returns Array of test suggestions
454
+ */
455
+ suggestTests(sourceFile, symbols) {
456
+ const resolved = resolve(sourceFile);
457
+ const suggestions = [];
458
+ // Read from cache if we have the content (best-effort, may need async)
459
+ const testFile = this.inferTestFilePath(resolved);
460
+ const existingTests = this.testFiles.get(testFile);
461
+ const framework = existingTests?.framework ?? "vitest";
462
+ // We need synchronous access, so we work with what we have in cache
463
+ // For full analysis, callers should use findCoverageGaps() first
464
+ const existingTestNames = new Set(existingTests?.testCases.map((tc) => tc.name) ?? []);
465
+ // Generate suggestions based on file naming patterns
466
+ const fileName = basename(resolved, extname(resolved));
467
+ const isClass = /[A-Z]/.test(fileName[0] ?? "");
468
+ const targetSymbol = symbols?.[0] ?? fileName;
469
+ const testCases = [];
470
+ // Happy path
471
+ testCases.push({
472
+ name: `should ${isClass ? "create instance" : "return expected result"}`,
473
+ type: "happy_path",
474
+ description: `Verify ${targetSymbol} works with valid input`,
475
+ inputHint: "valid input matching expected type",
476
+ expectedHint: "expected return value or side effect",
477
+ });
478
+ // Edge case
479
+ testCases.push({
480
+ name: `should handle empty input`,
481
+ type: "edge_case",
482
+ description: `Verify ${targetSymbol} handles edge cases gracefully`,
483
+ inputHint: "empty string, empty array, or zero",
484
+ expectedHint: "graceful handling (default value or empty result)",
485
+ });
486
+ // Error case
487
+ testCases.push({
488
+ name: `should throw on invalid input`,
489
+ type: "error_case",
490
+ description: `Verify ${targetSymbol} rejects invalid input`,
491
+ inputHint: "null, undefined, or malformed data",
492
+ expectedHint: "throws appropriate error",
493
+ });
494
+ // Null check
495
+ testCases.push({
496
+ name: `should handle null/undefined`,
497
+ type: "null_check",
498
+ description: `Verify ${targetSymbol} handles nullish values`,
499
+ inputHint: "null or undefined",
500
+ expectedHint: "does not crash, returns default or throws",
501
+ });
502
+ // Boundary case
503
+ testCases.push({
504
+ name: `should handle boundary values`,
505
+ type: "boundary",
506
+ description: `Verify ${targetSymbol} at boundary conditions`,
507
+ inputHint: "max int, empty collection, single element",
508
+ expectedHint: "correct behavior at boundaries",
509
+ });
510
+ // Filter out already-existing tests
511
+ const filteredCases = testCases.filter((tc) => !existingTestNames.has(tc.name));
512
+ if (filteredCases.length > 0) {
513
+ suggestions.push({
514
+ targetFile: resolved,
515
+ targetSymbol,
516
+ testFile,
517
+ framework,
518
+ testCases: filteredCases,
519
+ priority: this.hasTests(resolved) ? "medium" : "high",
520
+ });
521
+ }
522
+ return suggestions;
523
+ }
524
+ // ─── Test Execution ───
525
+ /**
526
+ * Build the shell command to run specific test files with a given framework.
527
+ *
528
+ * @param testFiles - Array of test file paths to run
529
+ * @param framework - Test framework identifier
530
+ * @returns Shell command string
531
+ */
532
+ buildTestCommand(testFiles, framework) {
533
+ const files = testFiles
534
+ .map((f) => relative(this.config.projectPath, resolve(f)))
535
+ .join(" ");
536
+ switch (framework) {
537
+ case "vitest":
538
+ return `npx vitest run ${files}`;
539
+ case "jest":
540
+ return `npx jest ${files}`;
541
+ case "node_test":
542
+ return `node --test ${files}`;
543
+ case "mocha":
544
+ return `npx mocha ${files}`;
545
+ default:
546
+ return `npx vitest run ${files}`;
547
+ }
548
+ }
549
+ /**
550
+ * Parse test runner output into a structured TestRunResult.
551
+ *
552
+ * Supports vitest, jest, mocha, and node:test output formats.
553
+ *
554
+ * @param output - Raw test runner stdout/stderr
555
+ * @param framework - Framework that produced the output
556
+ * @returns Parsed test run result
557
+ */
558
+ parseTestOutput(output, framework) {
559
+ const result = {
560
+ file: "",
561
+ passed: 0,
562
+ failed: 0,
563
+ skipped: 0,
564
+ duration: 0,
565
+ errors: [],
566
+ timestamp: Date.now(),
567
+ };
568
+ switch (framework) {
569
+ case "vitest":
570
+ this.parseVitestOutput(output, result);
571
+ break;
572
+ case "jest":
573
+ this.parseJestOutput(output, result);
574
+ break;
575
+ case "node_test":
576
+ this.parseNodeTestOutput(output, result);
577
+ break;
578
+ case "mocha":
579
+ this.parseMochaOutput(output, result);
580
+ break;
581
+ default:
582
+ this.parseGenericOutput(output, result);
583
+ break;
584
+ }
585
+ return result;
586
+ }
587
+ /**
588
+ * Record a test run result for history tracking.
589
+ *
590
+ * @param result - Test run result to record
591
+ */
592
+ recordResult(result) {
593
+ this.testHistory.push(result);
594
+ // Update lastRun on the test file if we have it
595
+ const testFile = this.testFiles.get(resolve(result.file));
596
+ if (testFile) {
597
+ testFile.lastRun = result;
598
+ }
599
+ }
600
+ /**
601
+ * Get test run history for a specific test file.
602
+ *
603
+ * @param testFile - Absolute path to the test file
604
+ * @returns Array of test run results, most recent first
605
+ */
606
+ getHistory(testFile) {
607
+ const resolved = resolve(testFile);
608
+ return this.testHistory
609
+ .filter((r) => resolve(r.file) === resolved)
610
+ .sort((a, b) => b.timestamp - a.timestamp);
611
+ }
612
+ // ─── Stats ───
613
+ /**
614
+ * Get test coverage statistics for the project.
615
+ *
616
+ * @returns Test coverage summary
617
+ */
618
+ getStats() {
619
+ const totalTestFiles = this.testFiles.size;
620
+ let totalTestCases = 0;
621
+ const frameworkBreakdown = {};
622
+ for (const testFile of this.testFiles.values()) {
623
+ totalTestCases += testFile.testCases.length;
624
+ frameworkBreakdown[testFile.framework] =
625
+ (frameworkBreakdown[testFile.framework] ?? 0) + 1;
626
+ }
627
+ const sourceFilesWithTests = this.sourceToTests.size;
628
+ // Count unique source files we know about (from test imports)
629
+ const allSourceFiles = new Set();
630
+ for (const testFile of this.testFiles.values()) {
631
+ for (const imp of testFile.imports) {
632
+ if (!this.isTestFile(imp)) {
633
+ allSourceFiles.add(imp);
634
+ }
635
+ }
636
+ }
637
+ // Also add source files from sourceToTests that have no tests
638
+ for (const src of this.sourceToTests.keys()) {
639
+ allSourceFiles.add(src);
640
+ }
641
+ const totalSourceFiles = Math.max(allSourceFiles.size, sourceFilesWithTests);
642
+ const sourceFilesWithoutTests = totalSourceFiles - sourceFilesWithTests;
643
+ const coveragePercent = totalSourceFiles > 0
644
+ ? Math.round((sourceFilesWithTests / totalSourceFiles) * 100)
645
+ : 0;
646
+ return {
647
+ totalTestFiles,
648
+ totalTestCases,
649
+ sourceFilesWithTests,
650
+ sourceFilesWithoutTests,
651
+ coveragePercent,
652
+ frameworkBreakdown,
653
+ };
654
+ }
655
+ // ─── Private Methods ───
656
+ /**
657
+ * Find test files matching configured patterns.
658
+ * Walks the project directory and filters by test file conventions.
659
+ */
660
+ async findTestFiles() {
661
+ const allFiles = await this.walkDirectory(this.config.projectPath);
662
+ return allFiles.filter((f) => this.isTestFile(f));
663
+ }
664
+ /**
665
+ * Check if a file path matches test file conventions.
666
+ *
667
+ * @param filePath - File path to check
668
+ * @returns True if the file is a test file
669
+ */
670
+ isTestFile(filePath) {
671
+ const rel = relative(this.config.projectPath, filePath);
672
+ return TEST_FILE_RE.test(rel) || TEST_DIR_RE.test(rel);
673
+ }
674
+ /**
675
+ * Find the source file for a test file using naming conventions.
676
+ *
677
+ * Conventions:
678
+ * - `foo.test.ts` → `foo.ts`
679
+ * - `foo.spec.ts` → `foo.ts`
680
+ * - `__tests__/foo.ts` → `../foo.ts`
681
+ *
682
+ * @param testFile - Absolute test file path
683
+ * @returns Absolute source file path, or null if not found
684
+ */
685
+ findSourceForTest(testFile) {
686
+ const dir = dirname(testFile);
687
+ const base = basename(testFile);
688
+ // foo.test.ts → foo.ts
689
+ const sourceMatch = base.match(/^(.+)\.(test|spec)\.(ts|tsx|js|jsx)$/);
690
+ if (sourceMatch) {
691
+ const sourceName = sourceMatch[1];
692
+ const ext = sourceMatch[3];
693
+ // Check same directory
694
+ const sameDirPath = join(dir, `${sourceName}.${ext}`);
695
+ // Check parent directory (for __tests__/ convention)
696
+ const parentDirPath = join(dirname(dir), `${sourceName}.${ext}`);
697
+ // Check src/ sibling (for test/ convention)
698
+ const srcDirPath = join(dirname(dir), "src", `${sourceName}.${ext}`);
699
+ // Return the first plausible path (we can't verify existence synchronously)
700
+ if (dirname(dir).endsWith("__tests__") || basename(dir) === "__tests__") {
701
+ return parentDirPath;
702
+ }
703
+ if (basename(dir) === "test") {
704
+ return srcDirPath;
705
+ }
706
+ return sameDirPath;
707
+ }
708
+ // __tests__/foo.ts → ../foo.ts
709
+ if (basename(dir) === "__tests__") {
710
+ return join(dirname(dir), base);
711
+ }
712
+ return null;
713
+ }
714
+ /**
715
+ * Traverse reverse dependency graph to find test files that
716
+ * transitively depend on a given source file.
717
+ *
718
+ * @param file - Starting file path
719
+ * @param depth - Maximum traversal depth
720
+ * @param visited - Set of already-visited paths (cycle prevention)
721
+ * @returns Array of test file paths found via transitive dependencies
722
+ */
723
+ findTransitiveTestDeps(file, depth, visited) {
724
+ if (depth <= 0)
725
+ return [];
726
+ visited.add(file);
727
+ const results = [];
728
+ // Find all files that import this file (reverse lookup from sourceToTests)
729
+ // We need to check all test files' imports
730
+ for (const [testPath, testFile] of this.testFiles) {
731
+ if (visited.has(testPath))
732
+ continue;
733
+ // Check if any of the test's imports transitively reach our file
734
+ for (const importedFile of testFile.imports) {
735
+ if (importedFile === file && !visited.has(testPath)) {
736
+ results.push(testPath);
737
+ visited.add(testPath);
738
+ }
739
+ }
740
+ }
741
+ // Also check non-test files that import this file, then find their tests
742
+ for (const [sourcePath, testPaths] of this.sourceToTests) {
743
+ if (visited.has(sourcePath))
744
+ continue;
745
+ // Check if any test of this source file imports our target file
746
+ // This is a simplified transitive check — for deeper traversal,
747
+ // we'd need the full dependency graph from DependencyAnalyzer
748
+ for (const testPath of testPaths) {
749
+ if (visited.has(testPath))
750
+ continue;
751
+ const testFile = this.testFiles.get(testPath);
752
+ if (!testFile)
753
+ continue;
754
+ for (const imp of testFile.imports) {
755
+ if (imp === file) {
756
+ results.push(testPath);
757
+ visited.add(testPath);
758
+ }
759
+ }
760
+ }
761
+ }
762
+ // Recurse into dependents at reduced depth
763
+ // Simplified: look for source files that import the current file
764
+ for (const [testPath, testFile] of this.testFiles) {
765
+ if (visited.has(testPath))
766
+ continue;
767
+ for (const imp of testFile.imports) {
768
+ if (!visited.has(imp) && !this.isTestFile(imp)) {
769
+ const deeper = this.findTransitiveTestDeps(imp, depth - 1, visited);
770
+ results.push(...deeper);
771
+ }
772
+ }
773
+ }
774
+ return [...new Set(results)];
775
+ }
776
+ /**
777
+ * Find integration/e2e test files in the same directory or module.
778
+ *
779
+ * @param sourceFile - Source file to find integration tests for
780
+ * @returns Array of integration test file paths
781
+ */
782
+ findModuleIntegrationTests(sourceFile) {
783
+ const dir = dirname(sourceFile);
784
+ const results = [];
785
+ for (const [testPath] of this.testFiles) {
786
+ if (INTEGRATION_TEST_RE.test(testPath) && dirname(testPath) === dir) {
787
+ results.push(testPath);
788
+ }
789
+ // Also check parent directory for integration tests
790
+ if (INTEGRATION_TEST_RE.test(testPath) && dirname(testPath) === dirname(dir)) {
791
+ results.push(testPath);
792
+ }
793
+ }
794
+ return results;
795
+ }
796
+ /**
797
+ * Collect all source (non-test) files in the project.
798
+ */
799
+ async collectSourceFiles() {
800
+ const allFiles = await this.walkDirectory(this.config.projectPath);
801
+ return allFiles.filter((f) => !this.isTestFile(f));
802
+ }
803
+ /**
804
+ * Read a file's content, returning empty string on failure.
805
+ *
806
+ * @param path - Absolute file path
807
+ * @returns File content or empty string
808
+ */
809
+ async readFile(path) {
810
+ try {
811
+ return await readFile(path, "utf-8");
812
+ }
813
+ catch {
814
+ return "";
815
+ }
816
+ }
817
+ /**
818
+ * Recursively walk a directory, collecting source files.
819
+ * Skips ignored directories (node_modules, dist, etc.).
820
+ */
821
+ async walkDirectory(dir) {
822
+ const results = [];
823
+ let entries;
824
+ try {
825
+ entries = await readdir(dir, { withFileTypes: true });
826
+ }
827
+ catch {
828
+ return results;
829
+ }
830
+ const ignoreSet = new Set(this.config.ignorePatterns);
831
+ for (const entry of entries) {
832
+ const fullPath = join(dir, entry.name);
833
+ if (entry.isDirectory()) {
834
+ if (!ignoreSet.has(entry.name)) {
835
+ const sub = await this.walkDirectory(fullPath);
836
+ results.push(...sub);
837
+ }
838
+ }
839
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
840
+ results.push(fullPath);
841
+ }
842
+ }
843
+ return results;
844
+ }
845
+ /**
846
+ * Parse import paths from test file content and resolve them to absolute paths.
847
+ */
848
+ parseTestImports(content, fromFile) {
849
+ const imports = [];
850
+ const dir = dirname(fromFile);
851
+ let match;
852
+ // Standard imports
853
+ const importRe = new RegExp(IMPORT_RE.source, "g");
854
+ while ((match = importRe.exec(content)) !== null) {
855
+ const source = match[4];
856
+ if (source.startsWith(".")) {
857
+ const resolved = this.resolveImportPath(dir, source);
858
+ if (resolved)
859
+ imports.push(resolved);
860
+ }
861
+ }
862
+ // Re-exports
863
+ const reExportRe = new RegExp(RE_EXPORT_RE.source, "g");
864
+ while ((match = reExportRe.exec(content)) !== null) {
865
+ const source = match[2];
866
+ if (source.startsWith(".")) {
867
+ const resolved = this.resolveImportPath(dir, source);
868
+ if (resolved)
869
+ imports.push(resolved);
870
+ }
871
+ }
872
+ // CJS requires
873
+ const requireRe = new RegExp(REQUIRE_RE.source, "g");
874
+ while ((match = requireRe.exec(content)) !== null) {
875
+ const source = match[1];
876
+ if (source.startsWith(".")) {
877
+ const resolved = this.resolveImportPath(dir, source);
878
+ if (resolved)
879
+ imports.push(resolved);
880
+ }
881
+ }
882
+ return [...new Set(imports)];
883
+ }
884
+ /**
885
+ * Resolve a relative import specifier to an absolute path.
886
+ * Handles .js → .ts resolution for ESM TypeScript projects.
887
+ */
888
+ resolveImportPath(fromDir, importPath) {
889
+ let resolved = resolve(fromDir, importPath);
890
+ // Strip .js extension and try .ts (ESM TS convention)
891
+ if (resolved.endsWith(".js")) {
892
+ const tsPath = resolved.slice(0, -3) + ".ts";
893
+ return tsPath;
894
+ }
895
+ // If it already has a known extension, return as-is
896
+ if (SOURCE_EXTENSIONS.has(extname(resolved))) {
897
+ return resolved;
898
+ }
899
+ // Try adding .ts extension (most common in this codebase)
900
+ return resolved + ".ts";
901
+ }
902
+ /**
903
+ * Infer the test type from test name and describe block context.
904
+ */
905
+ inferTestType(testName, describeNames) {
906
+ const combined = `${testName} ${describeNames.join(" ")}`.toLowerCase();
907
+ if (/\b(?:e2e|end.to.end|playwright|cypress|browser)\b/.test(combined)) {
908
+ return "e2e";
909
+ }
910
+ if (/\b(?:integration|api|database|db|http|server|route)\b/.test(combined)) {
911
+ return "integration";
912
+ }
913
+ return "unit";
914
+ }
915
+ /**
916
+ * Extract tags from a test name.
917
+ * Tags are inferred from keywords in the test description.
918
+ */
919
+ extractTags(testName) {
920
+ const tags = [];
921
+ const lower = testName.toLowerCase();
922
+ if (/\b(?:error|fail|throw|reject)\b/.test(lower))
923
+ tags.push("error");
924
+ if (/\b(?:edge|boundary|limit|max|min)\b/.test(lower))
925
+ tags.push("edge-case");
926
+ if (/\b(?:async|await|promise)\b/.test(lower))
927
+ tags.push("async");
928
+ if (/\b(?:mock|stub|spy)\b/.test(lower))
929
+ tags.push("mock");
930
+ if (/\b(?:snapshot)\b/.test(lower))
931
+ tags.push("snapshot");
932
+ if (/\b(?:performance|perf|bench)\b/.test(lower))
933
+ tags.push("performance");
934
+ if (/\b(?:security|auth|permission)\b/.test(lower))
935
+ tags.push("security");
936
+ if (/\b(?:regression)\b/.test(lower))
937
+ tags.push("regression");
938
+ return tags;
939
+ }
940
+ /**
941
+ * Extract exported symbols from file content.
942
+ */
943
+ extractExportedSymbols(content) {
944
+ const symbols = [];
945
+ const lines = content.split("\n");
946
+ // Build line offset map
947
+ const lineOffsets = [];
948
+ let offset = 0;
949
+ for (const line of lines) {
950
+ lineOffsets.push(offset);
951
+ offset += line.length + 1;
952
+ }
953
+ const getLineNumber = (charIndex) => {
954
+ let lo = 0;
955
+ let hi = lineOffsets.length - 1;
956
+ while (lo <= hi) {
957
+ const mid = (lo + hi) >>> 1;
958
+ if (lineOffsets[mid] <= charIndex) {
959
+ lo = mid + 1;
960
+ }
961
+ else {
962
+ hi = mid - 1;
963
+ }
964
+ }
965
+ return lo;
966
+ };
967
+ const exportRe = new RegExp(EXPORT_SYMBOL_RE.source, "g");
968
+ let match;
969
+ while ((match = exportRe.exec(content)) !== null) {
970
+ const name = match[1];
971
+ const line = getLineNumber(match.index);
972
+ // Determine kind from the match
973
+ const fullMatch = match[0];
974
+ let kind = "variable";
975
+ if (/\bfunction\b/.test(fullMatch))
976
+ kind = "function";
977
+ else if (/\bclass\b/.test(fullMatch))
978
+ kind = "class";
979
+ else if (/\binterface\b/.test(fullMatch))
980
+ kind = "interface";
981
+ else if (/\btype\b/.test(fullMatch))
982
+ kind = "type";
983
+ else if (/\benum\b/.test(fullMatch))
984
+ kind = "enum";
985
+ symbols.push({ name, kind, line, exported: true });
986
+ }
987
+ return symbols;
988
+ }
989
+ /**
990
+ * Infer the expected test file path for a source file.
991
+ */
992
+ inferTestFilePath(sourceFile) {
993
+ const dir = dirname(sourceFile);
994
+ const ext = extname(sourceFile);
995
+ const base = basename(sourceFile, ext);
996
+ return join(dir, `${base}.test${ext}`);
997
+ }
998
+ /**
999
+ * Escape special regex characters in a string.
1000
+ */
1001
+ escapeRegex(str) {
1002
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1003
+ }
1004
+ // ─── Output Parsers (Private) ───
1005
+ /**
1006
+ * Parse vitest output format.
1007
+ *
1008
+ * Vitest output example:
1009
+ * ```
1010
+ * ✓ src/foo.test.ts (3 tests) 45ms
1011
+ * ✓ should do X
1012
+ * × should do Y
1013
+ * Tests 2 passed | 1 failed
1014
+ * ```
1015
+ */
1016
+ parseVitestOutput(output, result) {
1017
+ // Match summary line: "Tests N passed | N failed | N skipped"
1018
+ const summaryMatch = output.match(/Tests\s+(?:(\d+)\s+passed)?(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|\s*(\d+)\s+skipped)?/);
1019
+ if (summaryMatch) {
1020
+ result.passed = parseInt(summaryMatch[1] ?? "0", 10);
1021
+ result.failed = parseInt(summaryMatch[2] ?? "0", 10);
1022
+ result.skipped = parseInt(summaryMatch[3] ?? "0", 10);
1023
+ }
1024
+ // Match duration: "Duration 1.23s" or "Time 45ms"
1025
+ const durationMatch = output.match(/(?:Duration|Time)\s+([\d.]+)\s*(s|ms)/);
1026
+ if (durationMatch) {
1027
+ const value = parseFloat(durationMatch[1]);
1028
+ result.duration = durationMatch[2] === "s" ? value * 1000 : value;
1029
+ }
1030
+ // Extract failures
1031
+ this.extractFailureBlocks(output, result);
1032
+ }
1033
+ /**
1034
+ * Parse jest output format.
1035
+ *
1036
+ * Jest output example:
1037
+ * ```
1038
+ * Tests: 1 failed, 2 passed, 3 total
1039
+ * Time: 1.234 s
1040
+ * ```
1041
+ */
1042
+ parseJestOutput(output, result) {
1043
+ const summaryMatch = output.match(/Tests:\s+(?:(\d+)\s+failed,\s+)?(?:(\d+)\s+passed,\s+)?(\d+)\s+total/);
1044
+ if (summaryMatch) {
1045
+ result.failed = parseInt(summaryMatch[1] ?? "0", 10);
1046
+ result.passed = parseInt(summaryMatch[2] ?? "0", 10);
1047
+ const total = parseInt(summaryMatch[3] ?? "0", 10);
1048
+ result.skipped = total - result.passed - result.failed;
1049
+ }
1050
+ const durationMatch = output.match(/Time:\s+([\d.]+)\s*(s|ms)/);
1051
+ if (durationMatch) {
1052
+ const value = parseFloat(durationMatch[1]);
1053
+ result.duration = durationMatch[2] === "s" ? value * 1000 : value;
1054
+ }
1055
+ this.extractFailureBlocks(output, result);
1056
+ }
1057
+ /**
1058
+ * Parse node:test output format (TAP-like).
1059
+ *
1060
+ * Node test output example:
1061
+ * ```
1062
+ * TAP version 13
1063
+ * ok 1 - should work
1064
+ * not ok 2 - should fail
1065
+ * 1..2
1066
+ * # tests 2
1067
+ * # pass 1
1068
+ * # fail 1
1069
+ * ```
1070
+ */
1071
+ parseNodeTestOutput(output, result) {
1072
+ const passMatch = output.match(/#\s*pass\s+(\d+)/);
1073
+ const failMatch = output.match(/#\s*fail\s+(\d+)/);
1074
+ const skipMatch = output.match(/#\s*skip\s+(\d+)/);
1075
+ result.passed = parseInt(passMatch?.[1] ?? "0", 10);
1076
+ result.failed = parseInt(failMatch?.[1] ?? "0", 10);
1077
+ result.skipped = parseInt(skipMatch?.[1] ?? "0", 10);
1078
+ const durationMatch = output.match(/#\s*duration_ms\s+([\d.]+)/);
1079
+ if (durationMatch) {
1080
+ result.duration = parseFloat(durationMatch[1]);
1081
+ }
1082
+ // Extract "not ok" lines as failures
1083
+ const failLineRe = /not ok \d+ - (.+)/g;
1084
+ let match;
1085
+ while ((match = failLineRe.exec(output)) !== null) {
1086
+ result.errors.push({
1087
+ testName: match[1],
1088
+ message: match[1],
1089
+ file: result.file,
1090
+ });
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Parse mocha output format.
1095
+ *
1096
+ * Mocha output example:
1097
+ * ```
1098
+ * 2 passing (45ms)
1099
+ * 1 failing
1100
+ * ```
1101
+ */
1102
+ parseMochaOutput(output, result) {
1103
+ const passMatch = output.match(/(\d+)\s+passing/);
1104
+ const failMatch = output.match(/(\d+)\s+failing/);
1105
+ const pendingMatch = output.match(/(\d+)\s+pending/);
1106
+ result.passed = parseInt(passMatch?.[1] ?? "0", 10);
1107
+ result.failed = parseInt(failMatch?.[1] ?? "0", 10);
1108
+ result.skipped = parseInt(pendingMatch?.[1] ?? "0", 10);
1109
+ const durationMatch = output.match(/passing\s+\((\d+)(ms|s)\)/);
1110
+ if (durationMatch) {
1111
+ const value = parseInt(durationMatch[1], 10);
1112
+ result.duration = durationMatch[2] === "s" ? value * 1000 : value;
1113
+ }
1114
+ this.extractFailureBlocks(output, result);
1115
+ }
1116
+ /**
1117
+ * Generic output parser — tries common patterns.
1118
+ */
1119
+ parseGenericOutput(output, result) {
1120
+ // Try to find pass/fail counts from any format
1121
+ const passMatch = output.match(/(\d+)\s+(?:pass(?:ed|ing)?)/i);
1122
+ const failMatch = output.match(/(\d+)\s+(?:fail(?:ed|ing|ure)?)/i);
1123
+ result.passed = parseInt(passMatch?.[1] ?? "0", 10);
1124
+ result.failed = parseInt(failMatch?.[1] ?? "0", 10);
1125
+ this.extractFailureBlocks(output, result);
1126
+ }
1127
+ /**
1128
+ * Extract failure blocks from test output.
1129
+ * Looks for common failure patterns across frameworks.
1130
+ */
1131
+ extractFailureBlocks(output, result) {
1132
+ // Common pattern: "FAIL" or "✕" or "×" followed by test name
1133
+ const failRe = /(?:FAIL|✕|×|✗)\s+(.+?)(?:\n|$)/g;
1134
+ let match;
1135
+ while ((match = failRe.exec(output)) !== null) {
1136
+ const testName = match[1].trim();
1137
+ if (testName && !result.errors.some((e) => e.testName === testName)) {
1138
+ result.errors.push({
1139
+ testName,
1140
+ message: testName,
1141
+ file: result.file,
1142
+ });
1143
+ }
1144
+ }
1145
+ // Extract assertion errors: "Expected: X" / "Received: Y"
1146
+ const assertRe = /Expected:\s*(.+?)\n\s*Received:\s*(.+?)(?:\n|$)/g;
1147
+ while ((match = assertRe.exec(output)) !== null) {
1148
+ const lastError = result.errors[result.errors.length - 1];
1149
+ if (lastError) {
1150
+ lastError.expected = match[1].trim();
1151
+ lastError.actual = match[2].trim();
1152
+ }
1153
+ }
1154
+ // Extract file:line references
1155
+ const fileLineRe = /at\s+(?:\S+\s+)?\(?(.+?):(\d+):\d+\)?/g;
1156
+ while ((match = fileLineRe.exec(output)) !== null) {
1157
+ const lastError = result.errors[result.errors.length - 1];
1158
+ if (lastError && !lastError.line) {
1159
+ lastError.file = match[1];
1160
+ lastError.line = parseInt(match[2], 10);
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+ //# sourceMappingURL=test-intelligence.js.map