jinzd-ai-cli 0.1.66 → 0.1.68
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/dist/chunk-K4Q235ZG.js +473 -0
- package/dist/index.js +579 -577
- package/dist/{run-tests-2A4S5S3S.js → run-tests-W7MNGH4I.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-MHD5T6LN.js +0 -1318
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tools/builtin/run-tests.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { platform } from "os";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/core/constants.ts
|
|
11
|
+
var VERSION = "0.1.68";
|
|
12
|
+
var APP_NAME = "ai-cli";
|
|
13
|
+
var CONFIG_DIR_NAME = ".aicli";
|
|
14
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
15
|
+
var HISTORY_DIR_NAME = "history";
|
|
16
|
+
var PLUGINS_DIR_NAME = "plugins";
|
|
17
|
+
var SKILLS_DIR_NAME = "skills";
|
|
18
|
+
var CUSTOM_COMMANDS_DIR_NAME = "commands";
|
|
19
|
+
var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
|
|
20
|
+
var MEMORY_FILE_NAME = "memory.md";
|
|
21
|
+
var MEMORY_MAX_CHARS = 1e4;
|
|
22
|
+
var DEV_STATE_FILE_NAME = "dev-state.md";
|
|
23
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
24
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
25
|
+
var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
|
|
26
|
+
var MCP_CONNECT_TIMEOUT = 3e4;
|
|
27
|
+
var MCP_CALL_TIMEOUT = 6e4;
|
|
28
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
29
|
+
var PLAN_MODE_READONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
30
|
+
"read_file",
|
|
31
|
+
"list_dir",
|
|
32
|
+
"grep_files",
|
|
33
|
+
"glob_files",
|
|
34
|
+
"web_fetch",
|
|
35
|
+
"google_search",
|
|
36
|
+
"ask_user",
|
|
37
|
+
// 允许:可向用户澄清需求
|
|
38
|
+
"write_todos"
|
|
39
|
+
// 允许:可输出任务列表作为实施计划
|
|
40
|
+
]);
|
|
41
|
+
var PLAN_MODE_SYSTEM_ADDON = `# \u{1F50D} Plan Mode \u2014 Read-Only Planning Mode
|
|
42
|
+
|
|
43
|
+
You are currently in read-only planning (Plan) mode.
|
|
44
|
+
|
|
45
|
+
**Allowed tools**: read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files \xB7 web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos
|
|
46
|
+
**Disabled tools**: bash \xB7 write_file \xB7 edit_file \xB7 run_interactive \xB7 save_last_response \xB7 save_memory and all MCP tools
|
|
47
|
+
|
|
48
|
+
**Your task**:
|
|
49
|
+
1. Use read-only tools to thoroughly analyze the codebase, file structure, and existing implementation
|
|
50
|
+
2. Use ask_user to clarify any ambiguous requirements with the user
|
|
51
|
+
3. Develop a detailed implementation plan (you may use write_todos to present the task list), including:
|
|
52
|
+
- List of files to be modified or created
|
|
53
|
+
- Specific changes for each file
|
|
54
|
+
- Execution order and dependencies
|
|
55
|
+
- Potential risks and considerations
|
|
56
|
+
|
|
57
|
+
Once planning is complete, clearly inform the user: type \`/plan execute\` to begin executing the plan, or \`/plan exit\` to discard it.`;
|
|
58
|
+
var SUBAGENT_DEFAULT_MAX_ROUNDS = 10;
|
|
59
|
+
var SUBAGENT_MAX_ROUNDS_LIMIT = 15;
|
|
60
|
+
var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
|
|
61
|
+
"bash",
|
|
62
|
+
"read_file",
|
|
63
|
+
"write_file",
|
|
64
|
+
"edit_file",
|
|
65
|
+
"list_dir",
|
|
66
|
+
"grep_files",
|
|
67
|
+
"glob_files",
|
|
68
|
+
"run_interactive",
|
|
69
|
+
"web_fetch",
|
|
70
|
+
"google_search",
|
|
71
|
+
"write_todos",
|
|
72
|
+
"run_tests"
|
|
73
|
+
]);
|
|
74
|
+
var CONTEXT_PRESSURE_THRESHOLD = 0.8;
|
|
75
|
+
var TEST_TIMEOUT = 3e5;
|
|
76
|
+
var AGENTIC_BEHAVIOR_GUIDELINE = `# Important Behavioral Guidelines
|
|
77
|
+
|
|
78
|
+
**Distinguish between "understanding" and "executing"**:
|
|
79
|
+
- When the user asks you to "read", "understand", "review", "analyze", "examine", or "look at" files or a project, your task is only to **read and summarize**, then wait for the user's next instruction. Do not automatically start executing tasks described in the project.
|
|
80
|
+
- Only begin using write/execute tools when the user **explicitly requests** an action (e.g., "generate", "create", "modify", "run", "start", etc.).
|
|
81
|
+
- If you are unsure about the user's intent, use the ask_user tool to confirm with the user, rather than assuming and executing on your own.`;
|
|
82
|
+
var AUTHOR = "Jin Zhengdong";
|
|
83
|
+
var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
|
|
84
|
+
var DESCRIPTION = "Cross-platform REPL-style AI conversation tool with multi-provider and agentic tool calling support";
|
|
85
|
+
var REPO_URL = "https://github.com/jinzhengdong/ai-cli";
|
|
86
|
+
|
|
87
|
+
// src/tools/builtin/run-tests.ts
|
|
88
|
+
var IS_WINDOWS = platform() === "win32";
|
|
89
|
+
function detectNodeTestFramework(cwd, pkg) {
|
|
90
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
91
|
+
const deps = pkg.dependencies ?? {};
|
|
92
|
+
const allDeps = { ...deps, ...devDeps };
|
|
93
|
+
if ("vitest" in allDeps || existsSync(join(cwd, "vitest.config.ts")) || existsSync(join(cwd, "vitest.config.js")) || existsSync(join(cwd, "vitest.config.mts"))) {
|
|
94
|
+
return { type: "node", framework: "vitest", command: "npx vitest run" };
|
|
95
|
+
}
|
|
96
|
+
if ("jest" in allDeps || existsSync(join(cwd, "jest.config.js")) || existsSync(join(cwd, "jest.config.ts")) || existsSync(join(cwd, "jest.config.mjs"))) {
|
|
97
|
+
return { type: "node", framework: "jest", command: "npx jest" };
|
|
98
|
+
}
|
|
99
|
+
if ("mocha" in allDeps || existsSync(join(cwd, ".mocharc.yml")) || existsSync(join(cwd, ".mocharc.yaml")) || existsSync(join(cwd, ".mocharc.json")) || existsSync(join(cwd, ".mocharc.js"))) {
|
|
100
|
+
return { type: "node", framework: "mocha", command: "npx mocha" };
|
|
101
|
+
}
|
|
102
|
+
if ("ava" in allDeps) {
|
|
103
|
+
return { type: "node", framework: "ava", command: "npx ava" };
|
|
104
|
+
}
|
|
105
|
+
if ("@playwright/test" in allDeps) {
|
|
106
|
+
return { type: "node", framework: "playwright", command: "npx playwright test" };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function safeReadPackageJson(cwd) {
|
|
111
|
+
const filePath = join(cwd, "package.json");
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = readFileSync(filePath, "utf-8");
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const code = err.code;
|
|
117
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
118
|
+
process.stderr.write(`[Warning] Permission denied reading package.json (${code})
|
|
119
|
+
`);
|
|
120
|
+
} else {
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`[Warning] Cannot read package.json: ${err instanceof Error ? err.message : String(err)}
|
|
123
|
+
`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (raw.charCodeAt(0) === 65279) {
|
|
129
|
+
raw = raw.slice(1);
|
|
130
|
+
}
|
|
131
|
+
if (!raw.trim()) {
|
|
132
|
+
process.stderr.write("[Warning] package.json is empty\n");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
138
|
+
process.stderr.write("[Warning] package.json root is not a JSON object\n");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
144
|
+
if (msg.includes("Unexpected token")) {
|
|
145
|
+
process.stderr.write(`[Warning] package.json has syntax error: ${msg}
|
|
146
|
+
`);
|
|
147
|
+
} else if (msg.includes("Unexpected end")) {
|
|
148
|
+
process.stderr.write("[Warning] package.json is truncated or incomplete\n");
|
|
149
|
+
} else {
|
|
150
|
+
process.stderr.write(`[Warning] package.json parse failed: ${msg}
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function detectProject(cwd) {
|
|
157
|
+
if (existsSync(join(cwd, "pom.xml"))) {
|
|
158
|
+
return { type: "java", framework: "Maven (JUnit)", command: IS_WINDOWS ? "mvn.cmd test" : "mvn test" };
|
|
159
|
+
}
|
|
160
|
+
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
161
|
+
const wrapper = IS_WINDOWS ? "gradlew.bat" : "./gradlew";
|
|
162
|
+
const cmd = existsSync(join(cwd, IS_WINDOWS ? "gradlew.bat" : "gradlew")) ? `${wrapper} test` : "gradle test";
|
|
163
|
+
return { type: "java", framework: "Gradle (JUnit)", command: cmd };
|
|
164
|
+
}
|
|
165
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
166
|
+
const pkg = safeReadPackageJson(cwd);
|
|
167
|
+
if (pkg) {
|
|
168
|
+
const testScript = pkg.scripts?.test;
|
|
169
|
+
if (testScript && testScript !== 'echo "Error: no test specified" && exit 1') {
|
|
170
|
+
return { type: "node", framework: "npm", command: "npm test" };
|
|
171
|
+
}
|
|
172
|
+
const frameworkDetected = detectNodeTestFramework(cwd, pkg);
|
|
173
|
+
if (frameworkDetected) return frameworkDetected;
|
|
174
|
+
} else {
|
|
175
|
+
return { type: "node", framework: "npm (package.json error)", command: "npm test" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, "pytest.ini"))) {
|
|
179
|
+
return { type: "python", framework: "pytest", command: "pytest -v" };
|
|
180
|
+
}
|
|
181
|
+
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
182
|
+
return { type: "rust", framework: "cargo", command: "cargo test" };
|
|
183
|
+
}
|
|
184
|
+
if (existsSync(join(cwd, "go.mod"))) {
|
|
185
|
+
return { type: "go", framework: "go test", command: "go test ./..." };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function parseJUnitXml(xmlContent) {
|
|
190
|
+
const summary = { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, duration: 0, failedTests: [] };
|
|
191
|
+
const suiteMatch = xmlContent.match(/<testsuite[^>]*\btests="(\d+)"[^>]*/);
|
|
192
|
+
if (suiteMatch) {
|
|
193
|
+
summary.tests = parseInt(suiteMatch[1], 10);
|
|
194
|
+
const failMatch = xmlContent.match(/<testsuite[^>]*\bfailures="(\d+)"/);
|
|
195
|
+
const errMatch = xmlContent.match(/<testsuite[^>]*\berrors="(\d+)"/);
|
|
196
|
+
const skipMatch = xmlContent.match(/<testsuite[^>]*\bskipped="(\d+)"/);
|
|
197
|
+
const timeMatch = xmlContent.match(/<testsuite[^>]*\btime="([^"]*)"/);
|
|
198
|
+
summary.failures = failMatch ? parseInt(failMatch[1], 10) : 0;
|
|
199
|
+
summary.errors = errMatch ? parseInt(errMatch[1], 10) : 0;
|
|
200
|
+
summary.skipped = skipMatch ? parseInt(skipMatch[1], 10) : 0;
|
|
201
|
+
summary.duration = timeMatch ? parseFloat(timeMatch[1]) : 0;
|
|
202
|
+
summary.passed = summary.tests - summary.failures - summary.errors - summary.skipped;
|
|
203
|
+
}
|
|
204
|
+
const tcPattern = /<testcase[^>]*\bname="([^"]*)"[^>]*classname="([^"]*)"[^>]*>[\s\S]*?<(failure|error)[^>]*>([^<]*)/g;
|
|
205
|
+
let m;
|
|
206
|
+
while ((m = tcPattern.exec(xmlContent)) !== null) {
|
|
207
|
+
summary.failedTests.push({
|
|
208
|
+
name: `${m[2]}#${m[1]}`,
|
|
209
|
+
message: m[4].trim().slice(0, 200)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return summary;
|
|
213
|
+
}
|
|
214
|
+
function findJUnitReports(cwd) {
|
|
215
|
+
const dirs = [
|
|
216
|
+
join(cwd, "target", "surefire-reports"),
|
|
217
|
+
// Maven
|
|
218
|
+
join(cwd, "build", "test-results", "test"),
|
|
219
|
+
// Gradle
|
|
220
|
+
join(cwd, "build", "test-results")
|
|
221
|
+
// Gradle (older)
|
|
222
|
+
];
|
|
223
|
+
const xmlFiles = [];
|
|
224
|
+
for (const dir of dirs) {
|
|
225
|
+
if (!existsSync(dir)) continue;
|
|
226
|
+
try {
|
|
227
|
+
const files = readdirSync(dir);
|
|
228
|
+
for (const f of files) {
|
|
229
|
+
if (f.endsWith(".xml")) xmlFiles.push(join(dir, f));
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return xmlFiles;
|
|
235
|
+
}
|
|
236
|
+
function parseGenericOutput(output) {
|
|
237
|
+
const mvn = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
|
|
238
|
+
if (mvn) {
|
|
239
|
+
const [, tests, failures, errors, skipped] = mvn.map(Number);
|
|
240
|
+
return { tests, failures, errors, skipped, passed: tests - failures - errors - skipped };
|
|
241
|
+
}
|
|
242
|
+
const jest = output.match(/Tests:\s+(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(\d+)\s+total/);
|
|
243
|
+
if (jest) {
|
|
244
|
+
const passed = parseInt(jest[1] ?? "0", 10);
|
|
245
|
+
const failures = parseInt(jest[2] ?? "0", 10);
|
|
246
|
+
const skipped = parseInt(jest[3] ?? "0", 10);
|
|
247
|
+
const tests = parseInt(jest[4], 10);
|
|
248
|
+
return { tests, passed, failures, skipped, errors: 0 };
|
|
249
|
+
}
|
|
250
|
+
const pytest = output.match(/(\d+)\s+passed(?:.*?(\d+)\s+failed)?(?:.*?(\d+)\s+error)?/);
|
|
251
|
+
if (pytest) {
|
|
252
|
+
const passed = parseInt(pytest[1], 10);
|
|
253
|
+
const failures = parseInt(pytest[2] ?? "0", 10);
|
|
254
|
+
const errors = parseInt(pytest[3] ?? "0", 10);
|
|
255
|
+
return { tests: passed + failures + errors, passed, failures, errors, skipped: 0 };
|
|
256
|
+
}
|
|
257
|
+
const cargo = output.match(/test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
|
|
258
|
+
if (cargo) {
|
|
259
|
+
const passed = parseInt(cargo[1], 10);
|
|
260
|
+
const failures = parseInt(cargo[2], 10);
|
|
261
|
+
const skipped = parseInt(cargo[3], 10);
|
|
262
|
+
return { tests: passed + failures + skipped, passed, failures, skipped, errors: 0 };
|
|
263
|
+
}
|
|
264
|
+
const goPass = (output.match(/^ok\s/gm) ?? []).length;
|
|
265
|
+
const goFail = (output.match(/^FAIL\s/gm) ?? []).length;
|
|
266
|
+
if (goPass + goFail > 0) {
|
|
267
|
+
return { tests: goPass + goFail, passed: goPass, failures: goFail, errors: 0, skipped: 0 };
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function formatReport(summary, framework, output, exitCode) {
|
|
272
|
+
const status = summary.failures + summary.errors > 0 ? "FAILED \u2717" : "PASSED \u2713";
|
|
273
|
+
const lines = [];
|
|
274
|
+
lines.push(`## Test Results \u2014 ${status}`);
|
|
275
|
+
lines.push(`**Total: ${summary.tests} | Passed: ${summary.passed} | Failed: ${summary.failures}${summary.errors > 0 ? ` | Errors: ${summary.errors}` : ""} | Skipped: ${summary.skipped}**`);
|
|
276
|
+
if (summary.duration > 0) {
|
|
277
|
+
lines.push(`Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(`Framework: ${framework}`);
|
|
280
|
+
}
|
|
281
|
+
if (summary.failedTests.length > 0) {
|
|
282
|
+
lines.push("");
|
|
283
|
+
lines.push("### Failed Tests");
|
|
284
|
+
for (const ft of summary.failedTests) {
|
|
285
|
+
lines.push(`- ${ft.name} \u2014 ${ft.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const outputLines = output.split("\n");
|
|
289
|
+
const tail = outputLines.length > 150 ? outputLines.slice(-150) : outputLines;
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push(`### Output (last ${tail.length} lines)`);
|
|
292
|
+
lines.push("```");
|
|
293
|
+
lines.push(tail.join("\n"));
|
|
294
|
+
lines.push("```");
|
|
295
|
+
return lines.join("\n");
|
|
296
|
+
}
|
|
297
|
+
function renderColorReport(summary, framework) {
|
|
298
|
+
const isPass = summary.failures + summary.errors === 0;
|
|
299
|
+
const status = isPass ? chalk.green.bold("PASSED \u2713") : chalk.red.bold("FAILED \u2717");
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(` ${chalk.bold("Test Results")} \u2014 ${status}`);
|
|
302
|
+
console.log(
|
|
303
|
+
` Total: ${chalk.bold(String(summary.tests))} | ${chalk.green(`Passed: ${summary.passed}`)} | ${chalk.red(`Failed: ${summary.failures}`)}` + (summary.errors > 0 ? ` | ${chalk.red(`Errors: ${summary.errors}`)}` : "") + ` | ${chalk.yellow(`Skipped: ${summary.skipped}`)}`
|
|
304
|
+
);
|
|
305
|
+
if (summary.duration > 0) {
|
|
306
|
+
console.log(` Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
307
|
+
}
|
|
308
|
+
if (summary.failedTests.length > 0) {
|
|
309
|
+
console.log(chalk.red("\n Failed Tests:"));
|
|
310
|
+
for (const ft of summary.failedTests) {
|
|
311
|
+
console.log(chalk.red(` - ${ft.name}`));
|
|
312
|
+
if (ft.message) console.log(chalk.dim(` ${ft.message}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
console.log();
|
|
316
|
+
}
|
|
317
|
+
async function executeTests(args) {
|
|
318
|
+
const cwd = process.cwd();
|
|
319
|
+
const customCmd = args["command"] ? String(args["command"]).trim() : "";
|
|
320
|
+
const filter = args["filter"] ? String(args["filter"]).trim() : "";
|
|
321
|
+
let command;
|
|
322
|
+
let framework;
|
|
323
|
+
if (customCmd) {
|
|
324
|
+
command = customCmd;
|
|
325
|
+
framework = "custom";
|
|
326
|
+
} else {
|
|
327
|
+
const detected = detectProject(cwd);
|
|
328
|
+
if (!detected) {
|
|
329
|
+
return "Error: Could not detect project type. No pom.xml, build.gradle, package.json, pyproject.toml, Cargo.toml, or go.mod found.\nPlease specify a test command using the `command` parameter.";
|
|
330
|
+
}
|
|
331
|
+
command = detected.command;
|
|
332
|
+
framework = detected.framework;
|
|
333
|
+
if (filter) {
|
|
334
|
+
if (detected.type === "java" && command.includes("mvn")) {
|
|
335
|
+
command += ` -Dtest="${filter}"`;
|
|
336
|
+
} else if (detected.type === "python") {
|
|
337
|
+
command += ` -k "${filter}"`;
|
|
338
|
+
} else if (detected.type === "rust") {
|
|
339
|
+
command += ` ${filter}`;
|
|
340
|
+
} else if (detected.type === "go") {
|
|
341
|
+
command = `go test ./... -run "${filter}"`;
|
|
342
|
+
} else if (detected.type === "node") {
|
|
343
|
+
if (detected.framework === "vitest") {
|
|
344
|
+
command += ` -t "${filter}"`;
|
|
345
|
+
} else if (detected.framework === "jest") {
|
|
346
|
+
command += ` -t "${filter}"`;
|
|
347
|
+
} else if (detected.framework === "mocha") {
|
|
348
|
+
command += ` --grep "${filter}"`;
|
|
349
|
+
} else if (detected.framework === "playwright") {
|
|
350
|
+
command += ` --grep "${filter}"`;
|
|
351
|
+
} else {
|
|
352
|
+
command += ` -- --grep "${filter}"`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
let output;
|
|
358
|
+
let exitCode = 0;
|
|
359
|
+
try {
|
|
360
|
+
const buf = execSync(command, {
|
|
361
|
+
cwd,
|
|
362
|
+
timeout: TEST_TIMEOUT,
|
|
363
|
+
encoding: "buffer",
|
|
364
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
365
|
+
env: {
|
|
366
|
+
...process.env,
|
|
367
|
+
...IS_WINDOWS ? {} : { FORCE_COLOR: "0" }
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
output = buf.toString("utf-8");
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const e = err;
|
|
373
|
+
exitCode = e.status ?? 1;
|
|
374
|
+
const stdout = e.stdout?.toString("utf-8") ?? "";
|
|
375
|
+
const stderr = e.stderr?.toString("utf-8") ?? "";
|
|
376
|
+
output = stdout + (stderr ? "\n" + stderr : "");
|
|
377
|
+
}
|
|
378
|
+
let summary = {
|
|
379
|
+
tests: 0,
|
|
380
|
+
passed: 0,
|
|
381
|
+
failures: 0,
|
|
382
|
+
errors: 0,
|
|
383
|
+
skipped: 0,
|
|
384
|
+
duration: 0,
|
|
385
|
+
failedTests: []
|
|
386
|
+
};
|
|
387
|
+
const xmlFiles = findJUnitReports(cwd);
|
|
388
|
+
if (xmlFiles.length > 0) {
|
|
389
|
+
for (const xmlFile of xmlFiles) {
|
|
390
|
+
try {
|
|
391
|
+
const xml = readFileSync(xmlFile, "utf-8");
|
|
392
|
+
const parsed = parseJUnitXml(xml);
|
|
393
|
+
summary.tests += parsed.tests;
|
|
394
|
+
summary.passed += parsed.passed;
|
|
395
|
+
summary.failures += parsed.failures;
|
|
396
|
+
summary.errors += parsed.errors;
|
|
397
|
+
summary.skipped += parsed.skipped;
|
|
398
|
+
summary.duration += parsed.duration;
|
|
399
|
+
summary.failedTests.push(...parsed.failedTests);
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
const parsed = parseGenericOutput(output);
|
|
405
|
+
if (parsed) {
|
|
406
|
+
summary = { ...summary, ...parsed };
|
|
407
|
+
} else {
|
|
408
|
+
summary.tests = 1;
|
|
409
|
+
if (exitCode === 0) {
|
|
410
|
+
summary.passed = 1;
|
|
411
|
+
} else {
|
|
412
|
+
summary.failures = 1;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
renderColorReport(summary, framework);
|
|
417
|
+
return formatReport(summary, framework, output, exitCode);
|
|
418
|
+
}
|
|
419
|
+
var runTestsTool = {
|
|
420
|
+
definition: {
|
|
421
|
+
name: "run_tests",
|
|
422
|
+
description: "Run project tests and return a structured report. Auto-detects project type (Maven/Gradle/npm/pytest/cargo/go). Returns test counts (passed/failed/skipped) and failed test details.",
|
|
423
|
+
parameters: {
|
|
424
|
+
command: {
|
|
425
|
+
type: "string",
|
|
426
|
+
description: "Optional: custom test command to run (overrides auto-detection)",
|
|
427
|
+
required: false
|
|
428
|
+
},
|
|
429
|
+
filter: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: 'Optional: test name filter/pattern (e.g., "ExamService" to run only matching tests)',
|
|
432
|
+
required: false
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
async execute(args) {
|
|
437
|
+
return executeTests(args);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export {
|
|
442
|
+
VERSION,
|
|
443
|
+
APP_NAME,
|
|
444
|
+
CONFIG_DIR_NAME,
|
|
445
|
+
CONFIG_FILE_NAME,
|
|
446
|
+
HISTORY_DIR_NAME,
|
|
447
|
+
PLUGINS_DIR_NAME,
|
|
448
|
+
SKILLS_DIR_NAME,
|
|
449
|
+
CUSTOM_COMMANDS_DIR_NAME,
|
|
450
|
+
CONTEXT_FILE_CANDIDATES,
|
|
451
|
+
MEMORY_FILE_NAME,
|
|
452
|
+
MEMORY_MAX_CHARS,
|
|
453
|
+
DEV_STATE_FILE_NAME,
|
|
454
|
+
DEFAULT_MAX_TOKENS,
|
|
455
|
+
MCP_TOOL_PREFIX,
|
|
456
|
+
MCP_PROJECT_CONFIG_NAME,
|
|
457
|
+
MCP_CONNECT_TIMEOUT,
|
|
458
|
+
MCP_CALL_TIMEOUT,
|
|
459
|
+
MCP_PROTOCOL_VERSION,
|
|
460
|
+
PLAN_MODE_READONLY_TOOLS,
|
|
461
|
+
PLAN_MODE_SYSTEM_ADDON,
|
|
462
|
+
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
463
|
+
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
464
|
+
SUBAGENT_ALLOWED_TOOLS,
|
|
465
|
+
CONTEXT_PRESSURE_THRESHOLD,
|
|
466
|
+
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
467
|
+
AUTHOR,
|
|
468
|
+
AUTHOR_EMAIL,
|
|
469
|
+
DESCRIPTION,
|
|
470
|
+
REPO_URL,
|
|
471
|
+
executeTests,
|
|
472
|
+
runTestsTool
|
|
473
|
+
};
|