pi-lens 3.6.3 → 3.6.4

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 (206) hide show
  1. package/package.json +3 -3
  2. package/tsconfig.json +1 -1
  3. package/clients/__tests__/file-time.test.js +0 -216
  4. package/clients/__tests__/file-time.test.ts +0 -276
  5. package/clients/__tests__/format-service.test.js +0 -245
  6. package/clients/__tests__/format-service.test.ts +0 -339
  7. package/clients/__tests__/formatters.test.js +0 -271
  8. package/clients/__tests__/formatters.test.ts +0 -401
  9. package/clients/agent-behavior-client.js +0 -110
  10. package/clients/agent-behavior-client.test.js +0 -94
  11. package/clients/agent-behavior-client.test.ts +0 -116
  12. package/clients/amain-types.js +0 -164
  13. package/clients/architect-client.js +0 -291
  14. package/clients/ast-grep-client.js +0 -253
  15. package/clients/ast-grep-parser.js +0 -84
  16. package/clients/ast-grep-rule-manager.js +0 -89
  17. package/clients/ast-grep-types.js +0 -9
  18. package/clients/auto-loop.js +0 -131
  19. package/clients/biome-client.js +0 -420
  20. package/clients/biome-client.test.js +0 -144
  21. package/clients/biome-client.test.ts +0 -163
  22. package/clients/cache/rule-cache.js +0 -72
  23. package/clients/cache-manager.js +0 -245
  24. package/clients/cache-manager.test.js +0 -197
  25. package/clients/cache-manager.test.ts +0 -299
  26. package/clients/complexity-client.js +0 -675
  27. package/clients/complexity-client.test.js +0 -234
  28. package/clients/complexity-client.test.ts +0 -255
  29. package/clients/config-validator.js +0 -465
  30. package/clients/dependency-checker.js +0 -325
  31. package/clients/dependency-checker.test.js +0 -60
  32. package/clients/dependency-checker.test.ts +0 -71
  33. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  34. package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
  35. package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
  36. package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
  37. package/clients/dispatch/debug.log +0 -1
  38. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  39. package/clients/dispatch/dispatcher.edge.test.ts +0 -100
  40. package/clients/dispatch/dispatcher.format.test.js +0 -46
  41. package/clients/dispatch/dispatcher.format.test.ts +0 -58
  42. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  43. package/clients/dispatch/dispatcher.inline.test.ts +0 -93
  44. package/clients/dispatch/dispatcher.js +0 -381
  45. package/clients/dispatch/dispatcher.test.js +0 -116
  46. package/clients/dispatch/dispatcher.test.ts +0 -149
  47. package/clients/dispatch/integration.js +0 -108
  48. package/clients/dispatch/plan.js +0 -183
  49. package/clients/dispatch/runners/architect.js +0 -83
  50. package/clients/dispatch/runners/architect.test.js +0 -138
  51. package/clients/dispatch/runners/architect.test.ts +0 -162
  52. package/clients/dispatch/runners/ast-grep-napi.js +0 -405
  53. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
  54. package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
  55. package/clients/dispatch/runners/ast-grep.js +0 -157
  56. package/clients/dispatch/runners/biome.js +0 -55
  57. package/clients/dispatch/runners/config-validation.js +0 -67
  58. package/clients/dispatch/runners/go-vet.js +0 -48
  59. package/clients/dispatch/runners/index.js +0 -47
  60. package/clients/dispatch/runners/lsp.js +0 -102
  61. package/clients/dispatch/runners/oxlint.js +0 -67
  62. package/clients/dispatch/runners/oxlint.test.js +0 -230
  63. package/clients/dispatch/runners/oxlint.test.ts +0 -303
  64. package/clients/dispatch/runners/pyright.js +0 -100
  65. package/clients/dispatch/runners/pyright.test.js +0 -98
  66. package/clients/dispatch/runners/pyright.test.ts +0 -121
  67. package/clients/dispatch/runners/python-slop.js +0 -97
  68. package/clients/dispatch/runners/python-slop.test.js +0 -203
  69. package/clients/dispatch/runners/python-slop.test.ts +0 -298
  70. package/clients/dispatch/runners/ruff.js +0 -48
  71. package/clients/dispatch/runners/rust-clippy.js +0 -102
  72. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  73. package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
  74. package/clients/dispatch/runners/shellcheck.js +0 -147
  75. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  76. package/clients/dispatch/runners/shellcheck.test.ts +0 -129
  77. package/clients/dispatch/runners/similarity.js +0 -230
  78. package/clients/dispatch/runners/spellcheck.js +0 -106
  79. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  80. package/clients/dispatch/runners/spellcheck.test.ts +0 -214
  81. package/clients/dispatch/runners/tree-sitter.js +0 -246
  82. package/clients/dispatch/runners/ts-lsp.js +0 -125
  83. package/clients/dispatch/runners/ts-slop.js +0 -113
  84. package/clients/dispatch/runners/type-safety.js +0 -142
  85. package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
  86. package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
  87. package/clients/dispatch/runners/utils.js +0 -51
  88. package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
  89. package/clients/dispatch/types.js +0 -16
  90. package/clients/dispatch/utils/format-utils.js +0 -44
  91. package/clients/dogfood.test.js +0 -201
  92. package/clients/dogfood.test.ts +0 -269
  93. package/clients/file-kinds.js +0 -177
  94. package/clients/file-kinds.test.js +0 -169
  95. package/clients/file-kinds.test.ts +0 -210
  96. package/clients/file-time.js +0 -152
  97. package/clients/file-utils.js +0 -40
  98. package/clients/fix-scanners.js +0 -204
  99. package/clients/format-service.js +0 -184
  100. package/clients/formatters.js +0 -488
  101. package/clients/go-client.js +0 -203
  102. package/clients/go-client.test.js +0 -127
  103. package/clients/go-client.test.ts +0 -143
  104. package/clients/installer/index.js +0 -403
  105. package/clients/interviewer-templates.js +0 -75
  106. package/clients/interviewer.js +0 -173
  107. package/clients/jscpd-client.js +0 -196
  108. package/clients/jscpd-client.test.js +0 -127
  109. package/clients/jscpd-client.test.ts +0 -145
  110. package/clients/knip-client.js +0 -239
  111. package/clients/knip-client.test.js +0 -112
  112. package/clients/knip-client.test.ts +0 -128
  113. package/clients/latency-logger.js +0 -40
  114. package/clients/lsp/__tests__/client.test.js +0 -310
  115. package/clients/lsp/__tests__/client.test.ts +0 -412
  116. package/clients/lsp/__tests__/config.test.js +0 -167
  117. package/clients/lsp/__tests__/config.test.ts +0 -217
  118. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  119. package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/integration.test.ts +0 -160
  122. package/clients/lsp/__tests__/launch.test.js +0 -313
  123. package/clients/lsp/__tests__/launch.test.ts +0 -394
  124. package/clients/lsp/__tests__/server.test.js +0 -259
  125. package/clients/lsp/__tests__/server.test.ts +0 -332
  126. package/clients/lsp/__tests__/service.test.js +0 -438
  127. package/clients/lsp/__tests__/service.test.ts +0 -530
  128. package/clients/lsp/client.js +0 -350
  129. package/clients/lsp/config.js +0 -112
  130. package/clients/lsp/index.js +0 -318
  131. package/clients/lsp/installer/index.js +0 -391
  132. package/clients/lsp/interactive-install.js +0 -221
  133. package/clients/lsp/language.js +0 -170
  134. package/clients/lsp/launch.js +0 -329
  135. package/clients/lsp/lsp/launch.js +0 -116
  136. package/clients/lsp/lsp/server.js +0 -532
  137. package/clients/lsp/lsp-index.js +0 -10
  138. package/clients/lsp/path-utils.js +0 -5
  139. package/clients/lsp/server.js +0 -725
  140. package/clients/lsp/test-py-spawn/requirements.txt +0 -1
  141. package/clients/lsp/test-py-spawn/test.py +0 -3
  142. package/clients/lsp/test-py-svc/requirements.txt +0 -1
  143. package/clients/lsp/test-py-svc/test.py +0 -3
  144. package/clients/lsp/test-python-project/requirements.txt +0 -1
  145. package/clients/lsp/test-python-project/test.py +0 -5
  146. package/clients/metrics-client.js +0 -107
  147. package/clients/metrics-client.test.js +0 -128
  148. package/clients/metrics-client.test.ts +0 -163
  149. package/clients/metrics-history.js +0 -367
  150. package/clients/path-utils.js +0 -142
  151. package/clients/pipeline.js +0 -272
  152. package/clients/production-readiness.js +0 -522
  153. package/clients/project-index.js +0 -255
  154. package/clients/project-metadata.js +0 -531
  155. package/clients/ruff-client.js +0 -325
  156. package/clients/ruff-client.test.js +0 -132
  157. package/clients/ruff-client.test.ts +0 -153
  158. package/clients/rules-scanner.js +0 -97
  159. package/clients/runner-tracker.js +0 -152
  160. package/clients/rust-client.js +0 -205
  161. package/clients/rust-client.test.js +0 -108
  162. package/clients/rust-client.test.ts +0 -130
  163. package/clients/safe-spawn-async.js +0 -163
  164. package/clients/safe-spawn.js +0 -241
  165. package/clients/sanitize.js +0 -291
  166. package/clients/sanitize.test.js +0 -177
  167. package/clients/sanitize.test.ts +0 -223
  168. package/clients/scan-architectural-debt.js +0 -167
  169. package/clients/scan-utils.js +0 -83
  170. package/clients/secrets-scanner.js +0 -119
  171. package/clients/secrets-scanner.test.js +0 -100
  172. package/clients/secrets-scanner.test.ts +0 -113
  173. package/clients/sg-runner.js +0 -292
  174. package/clients/state-matrix.js +0 -160
  175. package/clients/subprocess-client.js +0 -65
  176. package/clients/symbol-types.js +0 -5
  177. package/clients/test-runner-client.js +0 -523
  178. package/clients/test-runner-client.test.js +0 -192
  179. package/clients/test-runner-client.test.ts +0 -253
  180. package/clients/test-utils.js +0 -27
  181. package/clients/test-utils.ts +0 -36
  182. package/clients/todo-scanner.js +0 -200
  183. package/clients/todo-scanner.test.js +0 -301
  184. package/clients/todo-scanner.test.ts +0 -352
  185. package/clients/tool-availability.js +0 -207
  186. package/clients/tree-sitter-client.js +0 -601
  187. package/clients/tree-sitter-query-loader.js +0 -355
  188. package/clients/tree-sitter-symbol-extractor.js +0 -289
  189. package/clients/ts-service.js +0 -129
  190. package/clients/type-coverage-client.js +0 -127
  191. package/clients/type-coverage-client.test.js +0 -105
  192. package/clients/type-coverage-client.test.ts +0 -125
  193. package/clients/type-safety-client.js +0 -138
  194. package/clients/types.js +0 -11
  195. package/clients/typescript-client.codefix.test.js +0 -157
  196. package/clients/typescript-client.codefix.test.ts +0 -186
  197. package/clients/typescript-client.js +0 -509
  198. package/clients/typescript-client.test.js +0 -105
  199. package/clients/typescript-client.test.ts +0 -126
  200. package/commands/booboo.js +0 -1007
  201. package/commands/fix-from-booboo.js +0 -398
  202. package/commands/fix-simplified.js +0 -618
  203. package/commands/rate.js +0 -281
  204. package/commands/rate.test.js +0 -119
  205. package/commands/rate.test.ts +0 -131
  206. package/commands/refactor.js +0 -130
@@ -1,523 +0,0 @@
1
- /**
2
- * Test Runner Client for pi-lens
3
- *
4
- * Detects test files and runs them on write/edit to provide
5
- * immediate test feedback to the AI agent.
6
- *
7
- * Supports: vitest, jest, pytest (extensible to more)
8
- *
9
- * Design: File-level targeted testing — only runs tests for the
10
- * specific file being edited, not the entire suite.
11
- */
12
- import * as fs from "node:fs";
13
- import * as path from "node:path";
14
- import { safeSpawn } from "./safe-spawn.js";
15
- // --- Test File Patterns ---
16
- const _TEST_FILE_PATTERNS = [
17
- {
18
- lang: "typescript",
19
- patterns: [
20
- /^(.+)\.test\.tsx?$/,
21
- /^(.+)\.spec\.tsx?$/,
22
- /^(.+?)__tests__\/(.+)\.tsx?$/,
23
- ],
24
- },
25
- {
26
- lang: "javascript",
27
- patterns: [
28
- /^(.+)\.test\.jsx?$/,
29
- /^(.+)\.spec\.jsx?$/,
30
- /^(.+?)__tests__\/(.+)\.jsx?$/,
31
- ],
32
- },
33
- {
34
- lang: "python",
35
- patterns: [/^(.+)\.py$/, /^(.+?)test_(.+)\.py$/],
36
- },
37
- ];
38
- // Source file → test file patterns (reverse lookup)
39
- const SOURCE_TO_TEST_PATTERNS = [
40
- {
41
- ext: ".ts",
42
- testExts: [".test.ts", ".spec.ts"],
43
- dirs: ["__tests__", "tests", ".", "__tests__"],
44
- },
45
- {
46
- ext: ".tsx",
47
- testExts: [".test.tsx", ".spec.tsx"],
48
- dirs: ["__tests__", "tests", ".", "__tests__"],
49
- },
50
- {
51
- ext: ".js",
52
- testExts: [".test.js", ".spec.js"],
53
- dirs: ["__tests__", "tests", ".", "__tests__"],
54
- },
55
- {
56
- ext: ".jsx",
57
- testExts: [".test.jsx", ".spec.jsx"],
58
- dirs: ["__tests__", "tests", ".", "__tests__"],
59
- },
60
- {
61
- ext: ".py",
62
- testExts: ["test_*.py", "*_test.py"],
63
- dirs: ["tests", "test", ".", "."],
64
- },
65
- { ext: ".go", testExts: ["_test.go"], dirs: [".", ".", ".", "."] }, // Go tests are co-located
66
- { ext: ".rs", testExts: [".rs"], dirs: ["tests", "tests", "src", "."] }, // Rust: tests/ or #[test] in src
67
- ];
68
- // --- Runner Detection ---
69
- const RUNNERS = {
70
- vitest: {
71
- configFiles: [
72
- "vitest.config.ts",
73
- "vitest.config.js",
74
- "vitest.config.mjs",
75
- "vite.config.ts",
76
- ],
77
- command: "npx",
78
- args: (testFile, _cwd) => [
79
- "vitest",
80
- "run",
81
- testFile,
82
- "--reporter=json",
83
- "--passWithNoTests",
84
- ],
85
- parseJson: true,
86
- },
87
- jest: {
88
- configFiles: [
89
- "jest.config.ts",
90
- "jest.config.js",
91
- "jest.config.json",
92
- ".jestrc.js",
93
- ],
94
- command: "npx",
95
- args: (testFile, _cwd) => [
96
- "jest",
97
- testFile,
98
- "--json",
99
- "--passWithNoTests",
100
- "--forceExit",
101
- ],
102
- parseJson: true,
103
- },
104
- pytest: {
105
- configFiles: ["pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini"],
106
- command: "python",
107
- args: (testFile, _cwd) => ["-m", "pytest", testFile, "--tb=short", "-q"],
108
- parseJson: false, // pytest JSON requires plugin, use text parsing
109
- },
110
- go: {
111
- configFiles: ["go.mod"],
112
- command: "go",
113
- args: (testFile, cwd) => {
114
- // Convert file path to package path
115
- const relPath = path.relative(cwd, testFile);
116
- const pkgDir = path.dirname(relPath);
117
- return ["test", `-run`, ".", `./${pkgDir === "." ? "." : pkgDir}`];
118
- },
119
- parseJson: false, // Go test output is text-based
120
- },
121
- cargo: {
122
- configFiles: ["Cargo.toml"],
123
- command: "cargo",
124
- args: (_testFile, _cwd) => ["test", "--no-fail-fast"],
125
- parseJson: false, // cargo test output is text-based
126
- },
127
- dotnet: {
128
- configFiles: ["*.csproj", "*.sln"],
129
- command: "dotnet",
130
- args: (_testFile, _cwd) => ["test", "--no-build"],
131
- parseJson: false,
132
- },
133
- gradle: {
134
- configFiles: ["build.gradle", "build.gradle.kts", "settings.gradle"],
135
- command: "./gradlew",
136
- args: (_testFile, _cwd) => ["test", "--no-daemon"],
137
- parseJson: false,
138
- },
139
- maven: {
140
- configFiles: ["pom.xml"],
141
- command: "mvn",
142
- args: (_testFile, _cwd) => ["test", "-q"],
143
- parseJson: false,
144
- },
145
- rspec: {
146
- configFiles: [".rspec", "spec/spec_helper.rb"],
147
- command: "bundle",
148
- args: (testFile, _cwd) => ["exec", "rspec", testFile],
149
- parseJson: false,
150
- },
151
- minitest: {
152
- configFiles: ["Gemfile"],
153
- command: "ruby",
154
- args: (testFile, _cwd) => ["-Itest", testFile],
155
- parseJson: false,
156
- },
157
- };
158
- // --- Client ---
159
- export class TestRunnerClient {
160
- constructor(verbose = false) {
161
- this.availableRunners = new Map();
162
- this.log = verbose
163
- ? (msg) => console.error(`[test-runner] ${msg}`)
164
- : () => { };
165
- }
166
- /**
167
- * Check if a test runner is available in the project
168
- * Detection order:
169
- * 1. Config files (vitest.config.ts, jest.config.js, etc.)
170
- * 2. package.json dependencies
171
- * 3. node_modules presence
172
- */
173
- detectRunner(cwd) {
174
- // Priority 1: Config files
175
- for (const [name, config] of Object.entries(RUNNERS)) {
176
- const cacheKey = `${cwd}:${name}:config`;
177
- if (this.availableRunners.has(cacheKey)) {
178
- if (this.availableRunners.get(cacheKey)) {
179
- return { runner: name, config };
180
- }
181
- continue;
182
- }
183
- const found = config.configFiles.some((cf) => fs.existsSync(path.join(cwd, cf)));
184
- this.availableRunners.set(cacheKey, found);
185
- if (found) {
186
- this.log(`Detected runner via config: ${name}`);
187
- return { runner: name, config };
188
- }
189
- }
190
- const packageJsonPath = path.join(cwd, "package.json");
191
- try {
192
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
193
- const allDeps = {
194
- ...pkg.dependencies,
195
- ...pkg.devDependencies,
196
- };
197
- // Check for vitest first (more specific than jest)
198
- if (allDeps.vitest) {
199
- this.log("Detected vitest in package.json");
200
- this.availableRunners.set(`${cwd}:vitest:config`, true);
201
- return { runner: "vitest", config: RUNNERS.vitest };
202
- }
203
- if (allDeps.jest) {
204
- this.log("Detected jest in package.json");
205
- this.availableRunners.set(`${cwd}:jest:config`, true);
206
- return { runner: "jest", config: RUNNERS.jest };
207
- }
208
- if (allDeps.pytest || allDeps["pytest-cov"]) {
209
- this.log("Detected pytest in package.json (unusual)");
210
- this.availableRunners.set(`${cwd}:pytest:config`, true);
211
- return { runner: "pytest", config: RUNNERS.pytest };
212
- }
213
- }
214
- catch (err) {
215
- void err;
216
- // package.json parse error or file not found
217
- }
218
- // Priority 3: Check node_modules for installed packages
219
- const nodeModulesPath = path.join(cwd, "node_modules");
220
- if (fs.existsSync(nodeModulesPath)) {
221
- if (fs.existsSync(path.join(nodeModulesPath, "vitest"))) {
222
- this.log("Detected vitest in node_modules");
223
- return { runner: "vitest", config: RUNNERS.vitest };
224
- }
225
- if (fs.existsSync(path.join(nodeModulesPath, "jest"))) {
226
- this.log("Detected jest in node_modules");
227
- return { runner: "jest", config: RUNNERS.jest };
228
- }
229
- }
230
- for (const name of ["go", "cargo", "dotnet", "gradle", "maven"]) {
231
- const config = RUNNERS[name];
232
- const found = config.configFiles.some((cf) => {
233
- // Handle glob patterns like *.csproj
234
- if (cf.includes("*")) {
235
- try {
236
- const files = fs.readdirSync(cwd);
237
- return files.some((f) => new RegExp(cf.replace(/\*/g, ".*")).test(f));
238
- }
239
- catch {
240
- return false;
241
- }
242
- }
243
- return fs.existsSync(path.join(cwd, cf));
244
- });
245
- if (found) {
246
- this.log(`Detected ${name} from config file`);
247
- return { runner: name, config };
248
- }
249
- }
250
- // Priority 5: Check if pytest is available globally (for Python)
251
- try {
252
- const whichCmd = process.platform === "win32" ? "where" : "which";
253
- const result = safeSpawn(whichCmd, ["pytest"], {
254
- timeout: 2000,
255
- });
256
- if (result.status === 0) {
257
- this.log("Detected pytest globally");
258
- return { runner: "pytest", config: RUNNERS.pytest };
259
- }
260
- }
261
- catch (err) {
262
- void err;
263
- }
264
- return null;
265
- }
266
- /**
267
- * Find test file for a given source file
268
- * Returns the test file path if it exists, null otherwise
269
- */
270
- findTestFile(sourceFilePath, cwd) {
271
- const ext = path.extname(sourceFilePath);
272
- const basename = path.basename(sourceFilePath, ext);
273
- const dir = path.dirname(sourceFilePath);
274
- const _relativeDir = path.relative(cwd, dir);
275
- const patterns = SOURCE_TO_TEST_PATTERNS.find((p) => p.ext === ext);
276
- if (!patterns)
277
- return null;
278
- const detected = this.detectRunner(cwd);
279
- if (!detected)
280
- return null;
281
- // Check each potential test file location
282
- for (let i = 0; i < patterns.testExts.length; i++) {
283
- const testExt = patterns.testExts[i];
284
- const testDir = patterns.dirs[i];
285
- // Handle glob patterns (pytest style: test_*.py)
286
- if (testExt.includes("*")) {
287
- const pattern = testExt.replace("*", basename);
288
- const searchDir = testDir === "." ? dir : path.join(cwd, testDir);
289
- let files;
290
- try {
291
- files = fs.readdirSync(searchDir);
292
- }
293
- catch (err) {
294
- void err;
295
- continue;
296
- }
297
- const match = files.find((f) => f === pattern ||
298
- (f.startsWith("test_") &&
299
- f.endsWith(".py") &&
300
- f.includes(basename)));
301
- if (match) {
302
- const testPath = path.join(searchDir, match);
303
- this.log(`Found test file: ${testPath}`);
304
- return { testFile: testPath, runner: detected.runner };
305
- }
306
- }
307
- else {
308
- // Exact pattern match (jest/vitest style)
309
- const testFilename = basename + testExt;
310
- const searchPaths = [
311
- path.join(dir, testFilename), // same directory
312
- path.join(dir, "__tests__", testFilename), // __tests__ subdirectory
313
- path.join(cwd, "tests", testFilename), // top-level tests/
314
- path.join(cwd, "__tests__", testFilename), // top-level __tests__/
315
- ];
316
- for (const testPath of searchPaths) {
317
- if (fs.existsSync(testPath)) {
318
- this.log(`Found test file: ${testPath}`);
319
- return { testFile: testPath, runner: detected.runner };
320
- }
321
- }
322
- }
323
- }
324
- return null;
325
- }
326
- /**
327
- * Run tests for a specific file
328
- */
329
- runTestFile(testFile, cwd, runner, config) {
330
- const absoluteTestFile = path.resolve(testFile);
331
- if (!fs.existsSync(absoluteTestFile)) {
332
- return this.emptyResult(absoluteTestFile, "", runner, "Test file not found");
333
- }
334
- try {
335
- const args = config.args(absoluteTestFile, cwd);
336
- this.log(`Running: ${config.command} ${args.join(" ")}`);
337
- const result = safeSpawn(config.command, args, {
338
- cwd,
339
- timeout: 60000, // 60s timeout
340
- });
341
- const stdout = result.stdout || "";
342
- const stderr = result.stderr || "";
343
- // Check for runner errors (not test failures)
344
- if (result.error) {
345
- this.log(`Runner error: ${result.error.message}`);
346
- return this.emptyResult(absoluteTestFile, "", runner, `Runner error: ${result.error.message}`);
347
- }
348
- // Parse output based on runner
349
- switch (runner) {
350
- case "vitest":
351
- return this.parseVitestOutput(stdout, stderr, absoluteTestFile, cwd, runner);
352
- case "jest":
353
- return this.parseJestOutput(stdout, stderr, absoluteTestFile, cwd, runner);
354
- case "pytest":
355
- return this.parsePytestOutput(stdout, stderr, result.status ?? 0, absoluteTestFile, cwd, runner);
356
- default:
357
- return this.emptyResult(absoluteTestFile, "", runner, "Unknown runner");
358
- }
359
- }
360
- catch (err) {
361
- this.log(`Run error: ${err.message}`);
362
- return this.emptyResult(absoluteTestFile, "", runner, err.message);
363
- }
364
- }
365
- /**
366
- * Check if a source file has corresponding tests (without running them)
367
- */
368
- hasTestFile(sourceFilePath, cwd) {
369
- return this.findTestFile(sourceFilePath, cwd) !== null;
370
- }
371
- // --- Shared JSON test output parser (Vitest + Jest share the same structure) ---
372
- parseJsonTestOutput(stdout, stderr, testFile, cwd, runner) {
373
- try {
374
- const json = JSON.parse(stdout);
375
- const failures = [];
376
- for (const suite of json.testResults || []) {
377
- if (suite.status === "failed" && suite.assertionResults) {
378
- for (const test of suite.assertionResults) {
379
- if (test.status === "failed") {
380
- failures.push({
381
- name: test.title,
382
- message: test.failureMessages?.[0] || suite.message || "Test failed",
383
- location: test.location
384
- ? `${path.relative(cwd, testFile)}:${test.location.line}`
385
- : undefined,
386
- stack: this.truncateStack(test.failureMessages?.join("\n")),
387
- });
388
- }
389
- }
390
- }
391
- }
392
- return {
393
- file: testFile,
394
- sourceFile: "",
395
- runner,
396
- passed: json.numPassedTests || 0,
397
- failed: json.numFailedTests || 0,
398
- skipped: json.numSkippedTests || 0,
399
- failures,
400
- duration: 0,
401
- };
402
- }
403
- catch (err) {
404
- void err;
405
- const failed = stdout.includes("FAIL") || stderr.includes("FAIL");
406
- return this.emptyResult(testFile, "", runner, failed ? "Tests failed (could not parse output)" : undefined);
407
- }
408
- }
409
- // --- Vitest Parser ---
410
- parseVitestOutput(stdout, stderr, testFile, cwd, runner) {
411
- return this.parseJsonTestOutput(stdout, stderr, testFile, cwd, runner);
412
- }
413
- // --- Jest Parser ---
414
- parseJestOutput(stdout, stderr, testFile, cwd, runner) {
415
- return this.parseJsonTestOutput(stdout, stderr, testFile, cwd, runner);
416
- }
417
- // --- Pytest Parser (text-based, no JSON dependency) ---
418
- parsePytestOutput(stdout, stderr, exitCode, testFile, _cwd, runner) {
419
- const failures = [];
420
- const output = `${stdout}\n${stderr}`;
421
- // Parse summary line: "5 passed, 2 failed, 1 skipped in 0.23s"
422
- const summaryMatch = output.match(/(\d+)\s+passed?.*?(\d+)\s+failed.*?in\s+([\d.]+)s/i) ||
423
- output.match(/(\d+)\s+passed.*?in\s+([\d.]+)s/i);
424
- let passed = 0;
425
- let failed = 0;
426
- let skipped = 0;
427
- let duration = 0;
428
- if (summaryMatch) {
429
- // Extract numbers from various patterns
430
- const passedMatch = output.match(/(\d+)\s+passed/);
431
- const failedMatch = output.match(/(\d+)\s+failed/);
432
- const skippedMatch = output.match(/(\d+)\s+skipped/);
433
- const durationMatch = output.match(/in\s+([\d.]+)s/);
434
- passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
435
- failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
436
- skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
437
- duration = durationMatch ? parseFloat(durationMatch[1]) * 1000 : 0;
438
- }
439
- // Parse individual failures: "FAILED tests/test_foo.py::test_something - AssertionError: ..."
440
- const failureRegex = /FAILED\s+(\S+::\S+)\s*-\s*(.+?)(?:\n|$)/g;
441
- let match;
442
- while ((match = failureRegex.exec(output)) !== null) {
443
- failures.push({
444
- name: match[1],
445
- message: match[2].trim().slice(0, 500),
446
- location: match[1].replace("::", ":"),
447
- });
448
- }
449
- // Also look for assertion errors with traceback
450
- const tracebackRegex = /_{10,}\s*\n\s*(\w+Error:\s*.+?)(?:\n|$)/gs;
451
- while ((match = tracebackRegex.exec(output)) !== null) {
452
- // Add to last failure if exists, or create generic
453
- if (failures.length > 0 && !failures[failures.length - 1].stack) {
454
- failures[failures.length - 1].stack = match[1].trim().slice(0, 1000);
455
- }
456
- }
457
- return {
458
- file: testFile,
459
- sourceFile: "",
460
- runner,
461
- passed,
462
- failed,
463
- skipped,
464
- failures,
465
- duration,
466
- error: exitCode === 2 ? "Pytest configuration error" : undefined,
467
- };
468
- }
469
- // --- Formatting ---
470
- /**
471
- * Format test result for LLM consumption
472
- */
473
- formatResult(result) {
474
- if (result.error && result.passed === 0 && result.failed === 0) {
475
- // Runner error, not test failure
476
- return `[Tests] ⚠ Could not run tests: ${result.error}`;
477
- }
478
- const total = result.passed + result.failed + result.skipped;
479
- if (total === 0) {
480
- return ""; // No tests to report
481
- }
482
- const durationStr = result.duration > 0 ? ` (${(result.duration / 1000).toFixed(2)}s)` : "";
483
- if (result.failed === 0) {
484
- return `[Tests] ✓ ${result.passed}/${total} passed${durationStr} — ${result.runner}`;
485
- }
486
- // Has failures
487
- let output = `[Tests] ✗ ${result.failed}/${total} failed, ${result.passed} passed${durationStr} — ${result.runner}\n`;
488
- for (const failure of result.failures.slice(0, 5)) {
489
- output += ` ✗ ${failure.name}\n`;
490
- const msg = failure.message.split("\n")[0].slice(0, 200); // First line, truncated
491
- output += ` ${msg}\n`;
492
- if (failure.location) {
493
- output += ` at ${failure.location}\n`;
494
- }
495
- }
496
- if (result.failures.length > 5) {
497
- output += ` ... and ${result.failures.length - 5} more failure(s)\n`;
498
- }
499
- output += ` → Fix failing tests before proceeding\n`;
500
- return output.trimEnd();
501
- }
502
- // --- Helpers ---
503
- emptyResult(testFile, sourceFile, runner, error) {
504
- return {
505
- file: testFile,
506
- sourceFile,
507
- runner,
508
- passed: 0,
509
- failed: 0,
510
- skipped: 0,
511
- failures: [],
512
- duration: 0,
513
- error,
514
- };
515
- }
516
- truncateStack(stack) {
517
- if (!stack)
518
- return undefined;
519
- // Keep first 3 lines of stack trace
520
- const lines = stack.split("\n").slice(0, 3);
521
- return lines.join("\n").slice(0, 500);
522
- }
523
- }