pi-lens 3.6.2 → 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.
- package/CHANGELOG.md +10 -2
- package/package.json +4 -4
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/file-time.test.ts +0 -276
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/format-service.test.ts +0 -339
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/__tests__/formatters.test.ts +0 -401
- package/clients/agent-behavior-client.js +0 -110
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/agent-behavior-client.test.ts +0 -116
- package/clients/amain-types.js +0 -164
- package/clients/architect-client.js +0 -291
- package/clients/ast-grep-client.js +0 -253
- package/clients/ast-grep-parser.js +0 -84
- package/clients/ast-grep-rule-manager.js +0 -89
- package/clients/ast-grep-types.js +0 -9
- package/clients/auto-loop.js +0 -131
- package/clients/biome-client.js +0 -420
- package/clients/biome-client.test.js +0 -144
- package/clients/biome-client.test.ts +0 -163
- package/clients/cache/rule-cache.js +0 -72
- package/clients/cache-manager.js +0 -245
- package/clients/cache-manager.test.js +0 -197
- package/clients/cache-manager.test.ts +0 -299
- package/clients/complexity-client.js +0 -675
- package/clients/complexity-client.test.js +0 -234
- package/clients/complexity-client.test.ts +0 -255
- package/clients/config-validator.js +0 -465
- package/clients/dependency-checker.js +0 -325
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dependency-checker.test.ts +0 -71
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
- package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
- package/clients/dispatch/debug.log +0 -1
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.edge.test.ts +0 -100
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.format.test.ts +0 -58
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.inline.test.ts +0 -93
- package/clients/dispatch/dispatcher.js +0 -381
- package/clients/dispatch/dispatcher.test.js +0 -116
- package/clients/dispatch/dispatcher.test.ts +0 -149
- package/clients/dispatch/integration.js +0 -108
- package/clients/dispatch/plan.js +0 -183
- package/clients/dispatch/runners/architect.js +0 -83
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/architect.test.ts +0 -162
- package/clients/dispatch/runners/ast-grep-napi.js +0 -405
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
- package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
- package/clients/dispatch/runners/ast-grep.js +0 -157
- package/clients/dispatch/runners/biome.js +0 -55
- package/clients/dispatch/runners/config-validation.js +0 -67
- package/clients/dispatch/runners/go-vet.js +0 -48
- package/clients/dispatch/runners/index.js +0 -47
- package/clients/dispatch/runners/lsp.js +0 -102
- package/clients/dispatch/runners/oxlint.js +0 -67
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/oxlint.test.ts +0 -303
- package/clients/dispatch/runners/pyright.js +0 -100
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/pyright.test.ts +0 -121
- package/clients/dispatch/runners/python-slop.js +0 -97
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/python-slop.test.ts +0 -298
- package/clients/dispatch/runners/ruff.js +0 -48
- package/clients/dispatch/runners/rust-clippy.js +0 -102
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
- package/clients/dispatch/runners/shellcheck.js +0 -147
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/shellcheck.test.ts +0 -129
- package/clients/dispatch/runners/similarity.js +0 -230
- package/clients/dispatch/runners/spellcheck.js +0 -106
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/spellcheck.test.ts +0 -214
- package/clients/dispatch/runners/tree-sitter.js +0 -246
- package/clients/dispatch/runners/ts-lsp.js +0 -125
- package/clients/dispatch/runners/ts-slop.js +0 -113
- package/clients/dispatch/runners/type-safety.js +0 -142
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
- package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
- package/clients/dispatch/runners/utils.js +0 -51
- package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
- package/clients/dispatch/types.js +0 -16
- package/clients/dispatch/utils/format-utils.js +0 -44
- package/clients/dogfood.test.js +0 -201
- package/clients/dogfood.test.ts +0 -269
- package/clients/file-kinds.js +0 -177
- package/clients/file-kinds.test.js +0 -169
- package/clients/file-kinds.test.ts +0 -210
- package/clients/file-time.js +0 -152
- package/clients/file-utils.js +0 -40
- package/clients/fix-scanners.js +0 -204
- package/clients/format-service.js +0 -184
- package/clients/formatters.js +0 -488
- package/clients/go-client.js +0 -203
- package/clients/go-client.test.js +0 -127
- package/clients/go-client.test.ts +0 -143
- package/clients/installer/index.js +0 -403
- package/clients/interviewer-templates.js +0 -75
- package/clients/interviewer.js +0 -173
- package/clients/jscpd-client.js +0 -196
- package/clients/jscpd-client.test.js +0 -127
- package/clients/jscpd-client.test.ts +0 -145
- package/clients/knip-client.js +0 -239
- package/clients/knip-client.test.js +0 -112
- package/clients/knip-client.test.ts +0 -128
- package/clients/latency-logger.js +0 -40
- package/clients/lsp/__tests__/client.test.js +0 -310
- package/clients/lsp/__tests__/client.test.ts +0 -412
- package/clients/lsp/__tests__/config.test.js +0 -167
- package/clients/lsp/__tests__/config.test.ts +0 -217
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/integration.test.ts +0 -160
- package/clients/lsp/__tests__/launch.test.js +0 -313
- package/clients/lsp/__tests__/launch.test.ts +0 -394
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/server.test.ts +0 -332
- package/clients/lsp/__tests__/service.test.js +0 -438
- package/clients/lsp/__tests__/service.test.ts +0 -530
- package/clients/lsp/client.js +0 -350
- package/clients/lsp/config.js +0 -112
- package/clients/lsp/index.js +0 -318
- package/clients/lsp/installer/index.js +0 -391
- package/clients/lsp/interactive-install.js +0 -221
- package/clients/lsp/language.js +0 -170
- package/clients/lsp/launch.js +0 -329
- package/clients/lsp/lsp/launch.js +0 -116
- package/clients/lsp/lsp/server.js +0 -532
- package/clients/lsp/lsp-index.js +0 -10
- package/clients/lsp/path-utils.js +0 -5
- package/clients/lsp/server.js +0 -725
- package/clients/lsp/test-py-spawn/requirements.txt +0 -1
- package/clients/lsp/test-py-spawn/test.py +0 -3
- package/clients/lsp/test-py-svc/requirements.txt +0 -1
- package/clients/lsp/test-py-svc/test.py +0 -3
- package/clients/lsp/test-python-project/requirements.txt +0 -1
- package/clients/lsp/test-python-project/test.py +0 -5
- package/clients/metrics-client.js +0 -107
- package/clients/metrics-client.test.js +0 -128
- package/clients/metrics-client.test.ts +0 -163
- package/clients/metrics-history.js +0 -367
- package/clients/path-utils.js +0 -142
- package/clients/pipeline.js +0 -272
- package/clients/production-readiness.js +0 -522
- package/clients/project-index.js +0 -255
- package/clients/project-metadata.js +0 -531
- package/clients/ruff-client.js +0 -325
- package/clients/ruff-client.test.js +0 -132
- package/clients/ruff-client.test.ts +0 -153
- package/clients/rules-scanner.js +0 -97
- package/clients/runner-tracker.js +0 -152
- package/clients/rust-client.js +0 -205
- package/clients/rust-client.test.js +0 -108
- package/clients/rust-client.test.ts +0 -130
- package/clients/safe-spawn-async.js +0 -163
- package/clients/safe-spawn.js +0 -241
- package/clients/sanitize.js +0 -291
- package/clients/sanitize.test.js +0 -177
- package/clients/sanitize.test.ts +0 -223
- package/clients/scan-architectural-debt.js +0 -167
- package/clients/scan-utils.js +0 -83
- package/clients/secrets-scanner.js +0 -119
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/secrets-scanner.test.ts +0 -113
- package/clients/sg-runner.js +0 -292
- package/clients/state-matrix.js +0 -160
- package/clients/subprocess-client.js +0 -65
- package/clients/symbol-types.js +0 -5
- package/clients/test-runner-client.js +0 -523
- package/clients/test-runner-client.test.js +0 -192
- package/clients/test-runner-client.test.ts +0 -253
- package/clients/test-utils.js +0 -27
- package/clients/test-utils.ts +0 -36
- package/clients/todo-scanner.js +0 -200
- package/clients/todo-scanner.test.js +0 -301
- package/clients/todo-scanner.test.ts +0 -352
- package/clients/tool-availability.js +0 -207
- package/clients/tree-sitter-client.js +0 -601
- package/clients/tree-sitter-query-loader.js +0 -355
- package/clients/tree-sitter-symbol-extractor.js +0 -289
- package/clients/ts-service.js +0 -129
- package/clients/type-coverage-client.js +0 -127
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/type-coverage-client.test.ts +0 -125
- package/clients/type-safety-client.js +0 -138
- package/clients/types.js +0 -11
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.codefix.test.ts +0 -186
- package/clients/typescript-client.js +0 -509
- package/clients/typescript-client.test.js +0 -105
- package/clients/typescript-client.test.ts +0 -126
- package/commands/booboo.js +0 -1007
- package/commands/fix-from-booboo.js +0 -398
- package/commands/fix-simplified.js +0 -618
- package/commands/rate.js +0 -281
- package/commands/rate.test.js +0 -119
- package/commands/rate.test.ts +0 -131
- 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
|
-
}
|