@testkase/test-management-mcp 2.4.0 → 2.5.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.
- package/README.md +48 -136
- package/build/compact.d.ts +5 -0
- package/build/compact.d.ts.map +1 -0
- package/build/compact.js +106 -0
- package/build/compact.js.map +1 -0
- package/build/index.js +324 -432
- package/build/index.js.map +1 -1
- package/nul +2 -0
- package/package.json +12 -4
package/build/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
|
+
import { compact, flattenFolders, pick, shapeList } from "./compact.js";
|
|
6
7
|
const config = {
|
|
7
8
|
apiBaseUrl: (process.env.TESTKASE_API_BASE_URL || "https://api.testkase.com").replace(/\/+$/, ""),
|
|
8
9
|
patToken: process.env.TESTKASE_PAT_TOKEN,
|
|
@@ -12,8 +13,8 @@ const config = {
|
|
|
12
13
|
: undefined,
|
|
13
14
|
appUrl: process.env.TESTKASE_APP_URL?.replace(/\/+$/, ""),
|
|
14
15
|
};
|
|
15
|
-
const VERSION = "2.
|
|
16
|
-
const REQUEST_TIMEOUT_MS = 60_000;
|
|
16
|
+
const VERSION = "2.5.0";
|
|
17
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.TESTKASE_TIMEOUT_MS) || 60_000;
|
|
17
18
|
// ─── Frontend URL helper ─────────────────────────────────────────────────────
|
|
18
19
|
// Test management lives on its own subdomain
|
|
19
20
|
const KNOWN_APP_HOSTS = {
|
|
@@ -88,13 +89,29 @@ async function callApi(method, path, options = {}) {
|
|
|
88
89
|
}
|
|
89
90
|
const qs = queryParams.toString();
|
|
90
91
|
const url = `${config.apiBaseUrl}/api/v1/${path}${qs ? `?${qs}` : ""}`;
|
|
91
|
-
const
|
|
92
|
+
const send = () => axios({
|
|
92
93
|
method,
|
|
93
94
|
url,
|
|
94
95
|
headers: getApiHeaders(),
|
|
95
96
|
timeout: REQUEST_TIMEOUT_MS,
|
|
96
97
|
...(options.body !== undefined && { data: options.body }),
|
|
97
98
|
});
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await send();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Reads are safe to repeat: retry once on a dropped connection or 502/503/504. Writes and timeouts never retry.
|
|
105
|
+
const status = axios.isAxiosError(error) ? error.response?.status : undefined;
|
|
106
|
+
const transient = axios.isAxiosError(error) &&
|
|
107
|
+
error.code !== "ECONNABORTED" &&
|
|
108
|
+
error.code !== "ETIMEDOUT" &&
|
|
109
|
+
(!error.response || [502, 503, 504].includes(status));
|
|
110
|
+
if (method !== "get" || !transient)
|
|
111
|
+
throw error;
|
|
112
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
113
|
+
response = await send();
|
|
114
|
+
}
|
|
98
115
|
// Some endpoints report "not found" as HTTP 200 with status "error"
|
|
99
116
|
const data = response.data;
|
|
100
117
|
if (data && typeof data === "object" && data.status === "error") {
|
|
@@ -107,6 +124,12 @@ async function callApi(method, path, options = {}) {
|
|
|
107
124
|
function handleApiError(error) {
|
|
108
125
|
if (axios.isAxiosError(error)) {
|
|
109
126
|
const e = error;
|
|
127
|
+
if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
|
|
128
|
+
const isRead = (e.config?.method ?? "get").toLowerCase() === "get";
|
|
129
|
+
return new Error(isRead
|
|
130
|
+
? `Request timed out after ${REQUEST_TIMEOUT_MS} ms. Try a smaller limit or set TESTKASE_TIMEOUT_MS.`
|
|
131
|
+
: `Request timed out after ${REQUEST_TIMEOUT_MS} ms. The change may have been applied: check (search/get) before retrying.`);
|
|
132
|
+
}
|
|
110
133
|
let msg = `API request failed: ${e.message}`;
|
|
111
134
|
if (e.response) {
|
|
112
135
|
msg += `\nStatus: ${e.response.status}`;
|
|
@@ -125,11 +148,35 @@ function handleApiError(error) {
|
|
|
125
148
|
return error;
|
|
126
149
|
}
|
|
127
150
|
// ─── JSON response helper ────────────────────────────────────────────────────
|
|
128
|
-
|
|
151
|
+
// Replies are compact JSON (no indentation, no empty values); verbose returns the raw backend data
|
|
152
|
+
function jsonResponse(data, verbose = false) {
|
|
129
153
|
return {
|
|
130
|
-
content: [{ type: "text", text: JSON.stringify(data
|
|
154
|
+
content: [{ type: "text", text: JSON.stringify(verbose ? data : compact(data)) }],
|
|
131
155
|
};
|
|
132
156
|
}
|
|
157
|
+
const LIST_KEYS = {
|
|
158
|
+
testcases: ["id", "title", "priority", "status", "automated"],
|
|
159
|
+
cycles: ["test_cycle_id", "title", "status", "progress", "execution_summary", "folder_path"],
|
|
160
|
+
plans: ["test_plan_id", "title", "status", "progress", "test_cycle_count", "testcase_count", "execution_summary", "folder_path"],
|
|
161
|
+
linkedCases: ["test_case_id", "title", "priority", "execution_status", "assignee=assignee.assignee_name"],
|
|
162
|
+
projects: ["project_id", "name", "role"],
|
|
163
|
+
};
|
|
164
|
+
// Lists: chosen fields + paging, unless the caller asks for the raw reply
|
|
165
|
+
function listResponse(args, data, keys, urlIdKey) {
|
|
166
|
+
const verbose = !!p(args, "verbose");
|
|
167
|
+
return jsonResponse(verbose ? data : shapeList(data, keys, urlIdKey), verbose);
|
|
168
|
+
}
|
|
169
|
+
function memberRow(m) {
|
|
170
|
+
const name = String(m.name ?? [m.first_name ?? m.firstName, m.last_name ?? m.lastName].filter(Boolean).join(" ")).trim();
|
|
171
|
+
return { ...pick(m, ["user_id", "id"]), ...(name ? { name } : {}), ...pick(m, ["email", "role"]) };
|
|
172
|
+
}
|
|
173
|
+
const JSON_EXAMPLES = {
|
|
174
|
+
test_steps: '[{"description":"Open the login page","expected_result":"Form is shown"}]',
|
|
175
|
+
testcases: '[{"title":"Login works","test_steps":[{"description":"Open the login page"}]}]',
|
|
176
|
+
results: '[{"testcase_id":"TEST-1","execution_status":"pass"}]',
|
|
177
|
+
filters: '{"status":["Active"]}',
|
|
178
|
+
custom_fields: '{"Browser":"Chrome"}',
|
|
179
|
+
};
|
|
133
180
|
function safeJsonParse(raw, fieldName) {
|
|
134
181
|
if (typeof raw !== "string")
|
|
135
182
|
return raw;
|
|
@@ -137,9 +184,37 @@ function safeJsonParse(raw, fieldName) {
|
|
|
137
184
|
return JSON.parse(raw);
|
|
138
185
|
}
|
|
139
186
|
catch {
|
|
140
|
-
|
|
187
|
+
const example = JSON_EXAMPLES[fieldName];
|
|
188
|
+
if (!example)
|
|
189
|
+
throw new Error(`Invalid JSON in ${fieldName}. Ensure the value is valid JSON.`);
|
|
190
|
+
const kind = example.startsWith("[") ? "JSON array" : "JSON object";
|
|
191
|
+
throw new Error(`Invalid JSON in ${fieldName}. Send it as a ${kind} (not inside a string), e.g. ${fieldName}: ${example}`);
|
|
141
192
|
}
|
|
142
193
|
}
|
|
194
|
+
// Small models write "Passed", "FAILED", "not run": map them to the API's values
|
|
195
|
+
const EXECUTION_STATUS = {
|
|
196
|
+
pass: "pass", passed: "pass", ok: "pass", success: "pass",
|
|
197
|
+
fail: "fail", failed: "fail", failure: "fail",
|
|
198
|
+
blocked: "blocked", block: "blocked",
|
|
199
|
+
not_executed: "not_executed", not_run: "not_executed", notrun: "not_executed", untested: "not_executed",
|
|
200
|
+
not_tested: "not_executed", unexecuted: "not_executed", pending: "not_executed",
|
|
201
|
+
};
|
|
202
|
+
const PLAN_STATUS = {
|
|
203
|
+
open: "open", new: "open", not_started: "open", todo: "open",
|
|
204
|
+
in_progress: "in_progress", inprogress: "in_progress", started: "in_progress", active: "in_progress",
|
|
205
|
+
completed: "completed", complete: "completed", done: "completed", closed: "completed", finished: "completed",
|
|
206
|
+
};
|
|
207
|
+
const statusKey = (v) => String(v).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
208
|
+
function normalizeExecutionStatus(v, where) {
|
|
209
|
+
const s = EXECUTION_STATUS[statusKey(v)];
|
|
210
|
+
if (!s)
|
|
211
|
+
throw new Error(`${where}: execution_status "${v}" is not valid. Use one of: pass, fail, blocked, not_executed`);
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
// Unknown values pass through so the API can explain them
|
|
215
|
+
function normalizePlanStatus(v) {
|
|
216
|
+
return v === undefined || v === null || v === "" ? v : PLAN_STATUS[statusKey(v)] ?? v;
|
|
217
|
+
}
|
|
143
218
|
function validateId(id, fieldName) {
|
|
144
219
|
const str = String(id);
|
|
145
220
|
if (!/^[a-zA-Z0-9_\-]+$/.test(str)) {
|
|
@@ -177,6 +252,11 @@ function parseSteps(raw) {
|
|
|
177
252
|
throw new Error("test_steps must be a JSON array");
|
|
178
253
|
return parsed.map(normalizeStep);
|
|
179
254
|
}
|
|
255
|
+
// Step ids + text in their current order
|
|
256
|
+
async function currentSteps(projectId, testcaseId) {
|
|
257
|
+
const detail = await callApi("get", `projects/testcases/get-testcase-detail/${projectId}/${testcaseId}`);
|
|
258
|
+
return (detail?.data?.steps || []).map((st) => pick(st, ["id", "description"]));
|
|
259
|
+
}
|
|
180
260
|
// Steps are added one call each; collect failures instead of aborting
|
|
181
261
|
async function addSteps(projectId, testcaseId, steps) {
|
|
182
262
|
let created = 0;
|
|
@@ -212,6 +292,11 @@ function buildFilters(raw, extra = {}) {
|
|
|
212
292
|
}
|
|
213
293
|
return Object.keys(merged).length ? JSON.stringify(merged) : undefined;
|
|
214
294
|
}
|
|
295
|
+
// create_bulk: these top-level args are defaults for every item
|
|
296
|
+
const BULK_SHARED_FIELDS = [
|
|
297
|
+
"folder_id", "folderId", "priority", "status", "automated", "platform", "labels", "features", "environment",
|
|
298
|
+
"owner_id", "ownerId", "precondition", "custom_fields", "customFields",
|
|
299
|
+
];
|
|
215
300
|
// Builds the create/bulk-create payload from tool args (shared by both actions)
|
|
216
301
|
async function buildTestcasePayload(projectId, tc) {
|
|
217
302
|
const data = { title: tc.title ?? tc.name };
|
|
@@ -256,12 +341,9 @@ async function buildTestcasePayload(projectId, tc) {
|
|
|
256
341
|
return data;
|
|
257
342
|
}
|
|
258
343
|
function jsonResponseWithHints(data, hints) {
|
|
259
|
-
const
|
|
260
|
-
const hintsBlock = hints.length > 0
|
|
261
|
-
? `\n\n---\nHINTS:\n${hints.map((h) => `- ${h}`).join("\n")}`
|
|
262
|
-
: "";
|
|
344
|
+
const hintsBlock = hints.length > 0 ? `\nHINTS: ${hints.join(" | ")}` : "";
|
|
263
345
|
return {
|
|
264
|
-
content: [{ type: "text", text:
|
|
346
|
+
content: [{ type: "text", text: JSON.stringify(compact(data)) + hintsBlock }],
|
|
265
347
|
};
|
|
266
348
|
}
|
|
267
349
|
// ─── Error enrichment ───────────────────────────────────────────────────────
|
|
@@ -339,331 +421,235 @@ function requireArg(args, name, aliases = []) {
|
|
|
339
421
|
return value;
|
|
340
422
|
}
|
|
341
423
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
424
|
+
// REPORT TYPE → ENDPOINT MAPPING
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
426
|
+
const REPORT_TYPE_MAP = {
|
|
427
|
+
// Execution
|
|
428
|
+
execution_summary: "execution/summary",
|
|
429
|
+
execution_by_cycle: "execution/by-cycle",
|
|
430
|
+
execution_by_tester: "execution/by-tester",
|
|
431
|
+
execution_by_priority: "execution/by-priority",
|
|
432
|
+
execution_by_environment: "execution/by-environment",
|
|
433
|
+
execution_by_folder: "execution/by-folder",
|
|
434
|
+
// Coverage
|
|
435
|
+
requirement_coverage: "coverage/requirement-coverage",
|
|
436
|
+
traceability_matrix: "coverage/traceability-matrix",
|
|
437
|
+
failed_requirements: "coverage/failed-requirements",
|
|
438
|
+
uncovered_requirements: "coverage/uncovered-requirements",
|
|
439
|
+
testcase_coverage: "coverage/testcase-coverage",
|
|
440
|
+
unlinked_testcases: "coverage/unlinked-testcases",
|
|
441
|
+
// Defects
|
|
442
|
+
defects_by_folder: "defects/by-folder",
|
|
443
|
+
// Team
|
|
444
|
+
tester_workload: "team/tester-workload",
|
|
445
|
+
testcase_distribution: "team/testcase-distribution",
|
|
446
|
+
// Trends
|
|
447
|
+
execution_trend: "trends/execution-trend",
|
|
448
|
+
execution_burnup: "trends/execution-burnup",
|
|
449
|
+
execution_burndown: "trends/execution-burndown",
|
|
450
|
+
test_creation: "trends/test-creation",
|
|
451
|
+
cycle_comparison: "trends/cycle-comparison",
|
|
452
|
+
scorecard_by_folder: "trends/scorecard-by-folder",
|
|
453
|
+
scorecard_by_tester: "trends/scorecard-by-tester",
|
|
454
|
+
created_vs_executed: "trends/created-vs-executed",
|
|
455
|
+
execution_by_automation: "trends/execution-by-automation",
|
|
456
|
+
defects_by_cycle: "trends/defects-by-cycle",
|
|
457
|
+
defects_by_tester: "trends/defects-by-tester",
|
|
458
|
+
requirement_coverage_trend: "trends/requirement-coverage-trend",
|
|
459
|
+
// Insights (computed from project data, no AI credits)
|
|
460
|
+
flaky_tests: "ai/flaky-tests",
|
|
461
|
+
release_readiness: "ai/release-readiness",
|
|
462
|
+
risk_heatmap_folder: "ai/risk-heatmap-folder",
|
|
463
|
+
risk_heatmap_feature: "ai/risk-heatmap-feature",
|
|
464
|
+
testcase_quality: "ai/testcase-quality",
|
|
465
|
+
predictive_failure: "ai/predictive-failure",
|
|
466
|
+
smart_prioritization: "ai/smart-prioritization",
|
|
467
|
+
tester_effectiveness: "ai/tester-effectiveness",
|
|
468
|
+
stale_tests: "ai/stale-tests",
|
|
469
|
+
defect_hotspots: "ai/defect-hotspots",
|
|
470
|
+
cycle_health: "ai/cycle-health",
|
|
471
|
+
suite_optimization: "ai/suite-optimization",
|
|
472
|
+
requirement_risk_matrix: "ai/requirement-risk-matrix",
|
|
473
|
+
execution_velocity: "ai/execution-velocity",
|
|
474
|
+
project_health: "ai/project-health",
|
|
475
|
+
};
|
|
476
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
342
477
|
// TOOL DEFINITIONS (11 tools)
|
|
478
|
+
// Kept short on purpose: every client sends these on every turn. * = required.
|
|
343
479
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
480
|
+
const S = { type: "string" };
|
|
481
|
+
const N = { type: "number" };
|
|
482
|
+
const VERBOSE = { type: "boolean" };
|
|
483
|
+
const STATUS = { type: "string", enum: ["open", "in_progress", "completed"] };
|
|
484
|
+
const SECTION = { type: "string", enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"] };
|
|
485
|
+
const CSV = S;
|
|
486
|
+
const PROJECT_VALUE = S;
|
|
344
487
|
const LIST_PROJECTS_TOOL = {
|
|
345
488
|
name: "list_projects",
|
|
346
|
-
description: "
|
|
347
|
-
"Use this first to find a projectId, then pass it to other tools.",
|
|
489
|
+
description: "Projects you can access. Start here to get a projectId.",
|
|
348
490
|
annotations: { readOnlyHint: true },
|
|
349
|
-
inputSchema: {
|
|
350
|
-
type: "object",
|
|
351
|
-
properties: {
|
|
352
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
353
|
-
limit: { type: "number", description: "Items per page (default: 12)" },
|
|
354
|
-
},
|
|
355
|
-
required: [],
|
|
356
|
-
},
|
|
491
|
+
inputSchema: { type: "object", properties: { page: N, limit: N, verbose: VERBOSE }, required: [] },
|
|
357
492
|
};
|
|
358
493
|
const GET_PROJECT_STRUCTURE_TOOL = {
|
|
359
494
|
name: "get_project_structure",
|
|
360
|
-
description: "
|
|
361
|
-
"IMPORTANT: Call this before create/update to discover allowed field values, which are project-specific.",
|
|
495
|
+
description: "Folders, labels, members, field_options. Call before create/update: priority, status etc. accept only these values (any case).",
|
|
362
496
|
annotations: { readOnlyHint: true },
|
|
363
497
|
inputSchema: {
|
|
364
498
|
type: "object",
|
|
365
499
|
properties: {
|
|
366
|
-
projectId:
|
|
367
|
-
section:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
description: "Folder section (default: TESTCASE)",
|
|
371
|
-
},
|
|
372
|
-
include: {
|
|
373
|
-
type: "string",
|
|
374
|
-
description: "Comma-separated list of what to include: folders,labels,members,field_options (default: all)",
|
|
375
|
-
},
|
|
500
|
+
projectId: S,
|
|
501
|
+
section: SECTION,
|
|
502
|
+
include: { type: "string", description: "CSV of folders,labels,members,field_options (default all)" },
|
|
503
|
+
verbose: VERBOSE,
|
|
376
504
|
},
|
|
377
505
|
required: ["projectId"],
|
|
378
506
|
},
|
|
379
507
|
};
|
|
380
508
|
const SEARCH_TESTCASES_TOOL = {
|
|
381
509
|
name: "search_testcases",
|
|
382
|
-
description: "Search
|
|
510
|
+
description: "Search/list test cases.",
|
|
383
511
|
annotations: { readOnlyHint: true },
|
|
384
512
|
inputSchema: {
|
|
385
513
|
type: "object",
|
|
386
514
|
properties: {
|
|
387
|
-
projectId:
|
|
388
|
-
search:
|
|
389
|
-
folder_id: {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
'Example: {"status":["Active"],"priority":["High","Critical"]}. Also supports "exclude_status".',
|
|
397
|
-
},
|
|
398
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
399
|
-
limit: { type: "number", description: "Items per page (default: 20, max: 100)" },
|
|
400
|
-
sortBy: { type: "string", description: "Field to sort by (e.g. created_at, title)" },
|
|
401
|
-
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
515
|
+
projectId: S,
|
|
516
|
+
search: S,
|
|
517
|
+
folder_id: { type: "number", description: "includes sub-folders; 0 = no folder" },
|
|
518
|
+
filters: { type: "object", description: 'values are lists: {"status":["Active"]}; also exclude_status' },
|
|
519
|
+
page: N,
|
|
520
|
+
limit: { type: "number", description: "max 100" },
|
|
521
|
+
sortBy: S,
|
|
522
|
+
sortOrder: { type: "string", enum: ["asc", "desc"] },
|
|
523
|
+
verbose: VERBOSE,
|
|
402
524
|
},
|
|
403
525
|
required: ["projectId"],
|
|
404
526
|
},
|
|
405
527
|
};
|
|
406
528
|
const GET_TESTCASE_TOOL = {
|
|
407
529
|
name: "get_testcase",
|
|
408
|
-
description: "
|
|
530
|
+
description: "One test case with steps (step ids) and attachments.",
|
|
409
531
|
annotations: { readOnlyHint: true },
|
|
410
|
-
inputSchema: {
|
|
411
|
-
type: "object",
|
|
412
|
-
properties: {
|
|
413
|
-
projectId: { type: "string", description: "Project ID" },
|
|
414
|
-
testcaseId: { type: "string", description: "Test case ID (e.g. TEST-123)" },
|
|
415
|
-
},
|
|
416
|
-
required: ["projectId", "testcaseId"],
|
|
417
|
-
},
|
|
532
|
+
inputSchema: { type: "object", properties: { projectId: S, testcaseId: S, verbose: VERBOSE }, required: ["projectId", "testcaseId"] },
|
|
418
533
|
};
|
|
419
534
|
const TESTCASE_FIELDS = "title, summary, precondition, priority, status, automated, test_data, expected_result, labels, features, platform, environment, custom_fields, owner_id, folder_id";
|
|
420
535
|
const MANAGE_TESTCASE_TOOL = {
|
|
421
536
|
name: "manage_testcase",
|
|
422
|
-
description: "
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
" create - Create a single test case. Params: title (required), test_steps, and any of: " + TESTCASE_FIELDS + "\n" +
|
|
426
|
-
" create_bulk - Create up to 100 test cases in one go. Params: testcases (JSON array; each item takes the same fields as create, including test_steps)\n" +
|
|
427
|
-
" update - Update one field for test case(s). Params: ids, field, value (all required)\n" +
|
|
428
|
-
" delete - PERMANENTLY delete test case(s) with their steps, history and links. Params: ids (required)\n" +
|
|
429
|
-
" add_steps - Append steps to a test case. Params: testcaseId, test_steps\n" +
|
|
430
|
-
" update_step - Change one step. Params: testcaseId, step_id, and description/test_data/expected_result\n" +
|
|
431
|
-
" delete_steps - Delete steps. Params: testcaseId, step_ids (comma-separated)\n" +
|
|
432
|
-
" reorder_steps - Set the step order. Params: testcaseId, step_ids (all step ids, in the new order)\n" +
|
|
433
|
-
"Step ids come from get_testcase.\n\n" +
|
|
434
|
-
"VALID UPDATE FIELDS: " + TESTCASE_FIELDS + "\n\n" +
|
|
435
|
-
"EXAMPLES:\n" +
|
|
436
|
-
" Create: { action: 'create', title: 'Verify login', test_steps: '[{\"description\":\"Open login page\",\"expected_result\":\"Form shown\"}]' }\n" +
|
|
437
|
-
" Update: { action: 'update', ids: 'TEST-1', field: 'priority', value: '<value from get_project_structure>' }\n" +
|
|
438
|
-
" Delete: { action: 'delete', ids: 'TEST-1,TEST-2' }",
|
|
537
|
+
description: "* = required, ids are CSV. create(title*) | create_bulk(testcases*, max 100) | update(ids*, field*, value*): one field | " +
|
|
538
|
+
"delete(ids*): permanent | add_steps(testcaseId*, test_steps*) | update_step(testcaseId*, step_id*) | delete_steps(testcaseId*, step_ids*) | " +
|
|
539
|
+
"reorder_steps(testcaseId*, step_ids*: all, new order)",
|
|
439
540
|
annotations: { destructiveHint: true },
|
|
440
541
|
inputSchema: {
|
|
441
542
|
type: "object",
|
|
442
543
|
properties: {
|
|
443
|
-
projectId:
|
|
444
|
-
action: {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
},
|
|
470
|
-
automated: {
|
|
471
|
-
type: "string",
|
|
472
|
-
description: "[create] Automation status. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
473
|
-
},
|
|
474
|
-
test_data: { type: "string", description: "[create/update_step] Test data" },
|
|
475
|
-
expected_result: { type: "string", description: "[create/update_step] Expected result" },
|
|
476
|
-
platform: {
|
|
477
|
-
type: "string",
|
|
478
|
-
description: "[create] Platform. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
479
|
-
},
|
|
480
|
-
environment: { type: "string", description: "[create] Comma-separated environment values" },
|
|
481
|
-
features: { type: "string", description: "[create] Comma-separated feature/component names" },
|
|
482
|
-
owner_id: { type: "string", description: "[create] Owner user ID (a project member)" },
|
|
483
|
-
custom_fields: {
|
|
484
|
-
type: "string",
|
|
485
|
-
description: '[create] JSON object of custom field values (e.g. {"custom1": "value", "custom2": ["val1","val2"]})',
|
|
486
|
-
},
|
|
487
|
-
// create_bulk params
|
|
488
|
-
testcases: {
|
|
489
|
-
type: "string",
|
|
490
|
-
description: "[create_bulk] JSON array of test cases. Each item takes the create fields, e.g. " +
|
|
491
|
-
'[{"title":"...","folder_id":12,"priority":"High","test_steps":[{"description":"...","expected_result":"..."}]}]',
|
|
492
|
-
},
|
|
493
|
-
// update/delete params
|
|
494
|
-
ids: {
|
|
495
|
-
type: "string",
|
|
496
|
-
description: "[update/delete] Test case IDs, comma-separated (e.g. 'TEST-1' or 'TEST-1,TEST-2')",
|
|
497
|
-
},
|
|
498
|
-
field: {
|
|
499
|
-
type: "string",
|
|
500
|
-
enum: TESTCASE_FIELDS.split(", "),
|
|
501
|
-
description: "[update] Field to update. For priority/status/automated/platform/environment, call get_project_structure(include='field_options') to get allowed values.",
|
|
502
|
-
},
|
|
503
|
-
value: { type: "string", description: "[update] New value. String for most fields. For labels/features/environment: comma-separated. For custom_fields: JSON object (e.g. {\"fieldName\": \"value\"})" },
|
|
504
|
-
// step params
|
|
505
|
-
testcaseId: { type: "string", description: "[add_steps/update_step/delete_steps/reorder_steps] Test case ID (e.g. TEST-123)" },
|
|
506
|
-
step_id: { type: "number", description: "[update_step] Step ID to change" },
|
|
507
|
-
description: { type: "string", description: "[update_step] Step description" },
|
|
508
|
-
step_ids: { type: "string", description: "[delete_steps/reorder_steps] Comma-separated step IDs" },
|
|
544
|
+
projectId: S,
|
|
545
|
+
action: { type: "string", enum: ["create", "create_bulk", "update", "delete", "add_steps", "update_step", "delete_steps", "reorder_steps"] },
|
|
546
|
+
title: S,
|
|
547
|
+
summary: S,
|
|
548
|
+
precondition: S,
|
|
549
|
+
priority: PROJECT_VALUE,
|
|
550
|
+
status: PROJECT_VALUE,
|
|
551
|
+
automated: PROJECT_VALUE,
|
|
552
|
+
platform: PROJECT_VALUE,
|
|
553
|
+
folder_id: N,
|
|
554
|
+
labels: CSV,
|
|
555
|
+
features: CSV,
|
|
556
|
+
environment: CSV,
|
|
557
|
+
test_data: S,
|
|
558
|
+
expected_result: S,
|
|
559
|
+
owner_id: S,
|
|
560
|
+
custom_fields: { type: "object" },
|
|
561
|
+
test_steps: { type: "array", items: { type: "object", properties: { description: S, test_data: S, expected_result: S } } },
|
|
562
|
+
testcases: { type: "array", items: { type: "object" }, description: "top-level fields apply to all" },
|
|
563
|
+
ids: CSV,
|
|
564
|
+
field: { type: "string", enum: TESTCASE_FIELDS.split(", ") },
|
|
565
|
+
value: { type: "string", description: "CSV for list fields, JSON for custom_fields" },
|
|
566
|
+
testcaseId: S,
|
|
567
|
+
step_id: N,
|
|
568
|
+
description: S,
|
|
569
|
+
step_ids: CSV,
|
|
509
570
|
},
|
|
510
571
|
required: ["projectId", "action"],
|
|
511
572
|
},
|
|
512
573
|
};
|
|
513
574
|
const MANAGE_FOLDER_TOOL = {
|
|
514
575
|
name: "manage_folder",
|
|
515
|
-
description: "
|
|
516
|
-
"
|
|
517
|
-
" create - Create a folder. Params: name (required), section, parentId\n" +
|
|
518
|
-
" rename - Rename a folder. Params: folderId (required), name (required)\n" +
|
|
519
|
-
" move - Move a folder. Params: folderId (required), parentId (omit or null for root)\n" +
|
|
520
|
-
" delete - PERMANENTLY delete a folder, all its sub-folders AND every test case / cycle / plan inside them. " +
|
|
521
|
-
"Only owners and project admins can do this. Confirm with the user first. Params: folderId (required)",
|
|
576
|
+
description: "create(name*, parentId) | rename(folderId*, name*) | move(folderId*, parentId: omit = root) | " +
|
|
577
|
+
"delete(folderId*): deletes its contents too; confirm with the user",
|
|
522
578
|
annotations: { destructiveHint: true },
|
|
523
579
|
inputSchema: {
|
|
524
580
|
type: "object",
|
|
525
581
|
properties: {
|
|
526
|
-
projectId:
|
|
527
|
-
action: {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
section: {
|
|
533
|
-
type: "string",
|
|
534
|
-
enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
|
|
535
|
-
description: "Folder section (default: TESTCASE)",
|
|
536
|
-
},
|
|
537
|
-
name: { type: "string", description: "[create/rename] Folder name (max 50 chars)" },
|
|
538
|
-
folderId: {
|
|
539
|
-
type: "number",
|
|
540
|
-
description: "[rename/move/delete] ID of the folder to modify",
|
|
541
|
-
},
|
|
542
|
-
parentId: {
|
|
543
|
-
type: "number",
|
|
544
|
-
description: "[create] Parent folder ID for subfolder. [move] New parent ID (null for root).",
|
|
545
|
-
},
|
|
582
|
+
projectId: S,
|
|
583
|
+
action: { type: "string", enum: ["create", "rename", "move", "delete"] },
|
|
584
|
+
section: SECTION,
|
|
585
|
+
name: { type: "string", description: "max 50 chars" },
|
|
586
|
+
folderId: N,
|
|
587
|
+
parentId: N,
|
|
546
588
|
},
|
|
547
589
|
required: ["projectId", "action"],
|
|
548
590
|
},
|
|
549
591
|
};
|
|
550
592
|
const SEARCH_TEST_CYCLES_TOOL = {
|
|
551
593
|
name: "search_test_cycles",
|
|
552
|
-
description: "
|
|
594
|
+
description: "Search/list test cycles with progress.",
|
|
553
595
|
annotations: { readOnlyHint: true },
|
|
554
596
|
inputSchema: {
|
|
555
597
|
type: "object",
|
|
556
|
-
properties: {
|
|
557
|
-
projectId: { type: "string", description: "Project ID" },
|
|
558
|
-
search: { type: "string", description: "Search by title" },
|
|
559
|
-
folderId: { type: "number", description: "Filter by folder ID" },
|
|
560
|
-
status: { type: "string", enum: ["open", "in_progress", "completed"], description: "Filter by status" },
|
|
561
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
562
|
-
limit: { type: "number", description: "Items per page (default: 20, max: 100)" },
|
|
563
|
-
},
|
|
598
|
+
properties: { projectId: S, search: S, folderId: N, status: STATUS, page: N, limit: N, verbose: VERBOSE },
|
|
564
599
|
required: ["projectId"],
|
|
565
600
|
},
|
|
566
601
|
};
|
|
567
602
|
const MANAGE_TEST_CYCLE_TOOL = {
|
|
568
603
|
name: "manage_test_cycle",
|
|
569
|
-
description: "
|
|
570
|
-
"
|
|
571
|
-
" create - Create a cycle. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
572
|
-
" update - Update a cycle. Params: cycleId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
573
|
-
" delete - Delete cycle(s). Params: ids (required, comma-separated)\n" +
|
|
574
|
-
" get_details - Get cycle details. Params: cycleId (required)\n" +
|
|
575
|
-
" get_testcases - List test cases in cycle. Params: cycleId (required), search, page, limit\n" +
|
|
576
|
-
" link_testcases - Link test cases to cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
577
|
-
" unlink_testcases - Unlink test cases from cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
578
|
-
" assign_testcases - Assign test cases to user. Params: cycleId (required), testcase_ids (required), assignee_id (required)\n\n" +
|
|
579
|
-
"VALID STATUSES: open, in_progress, completed",
|
|
604
|
+
description: "create(title*, status, dates, folder_id) | update(cycleId*) | delete(ids*) | get_details(cycleId*) | get_testcases(cycleId*) | " +
|
|
605
|
+
"link_testcases/unlink_testcases(cycleId*, testcase_ids*) | assign_testcases(cycleId*, testcase_ids*, assignee_id*: member user_id). Dates YYYY-MM-DD.",
|
|
580
606
|
annotations: { destructiveHint: true },
|
|
581
607
|
inputSchema: {
|
|
582
608
|
type: "object",
|
|
583
609
|
properties: {
|
|
584
|
-
projectId:
|
|
610
|
+
projectId: S,
|
|
585
611
|
action: {
|
|
586
612
|
type: "string",
|
|
587
|
-
enum: [
|
|
588
|
-
"create",
|
|
589
|
-
"update",
|
|
590
|
-
"delete",
|
|
591
|
-
"get_details",
|
|
592
|
-
"get_testcases",
|
|
593
|
-
"link_testcases",
|
|
594
|
-
"unlink_testcases",
|
|
595
|
-
"assign_testcases",
|
|
596
|
-
],
|
|
597
|
-
description: "Action to perform",
|
|
598
|
-
},
|
|
599
|
-
// Identifiers
|
|
600
|
-
cycleId: {
|
|
601
|
-
type: "string",
|
|
602
|
-
description: "Test cycle ID (e.g. TCYCLE-1). Required for update/delete/get_details/get_testcases/link/unlink/assign.",
|
|
603
|
-
},
|
|
604
|
-
// create/update fields
|
|
605
|
-
title: { type: "string", description: "[create/update] Cycle title" },
|
|
606
|
-
summary: { type: "string", description: "[create/update] Description" },
|
|
607
|
-
status: {
|
|
608
|
-
type: "string",
|
|
609
|
-
enum: ["open", "in_progress", "completed"],
|
|
610
|
-
description: "[create/update] Status",
|
|
611
|
-
},
|
|
612
|
-
planned_start_date: {
|
|
613
|
-
type: "string",
|
|
614
|
-
description: "[create/update] Start date (YYYY-MM-DD)",
|
|
613
|
+
enum: ["create", "update", "delete", "get_details", "get_testcases", "link_testcases", "unlink_testcases", "assign_testcases"],
|
|
615
614
|
},
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
testcase_ids: {
|
|
631
|
-
type: "string",
|
|
632
|
-
description: "[link_testcases/unlink_testcases/assign_testcases] Comma-separated test case IDs",
|
|
633
|
-
},
|
|
634
|
-
assignee_id: {
|
|
635
|
-
type: "number",
|
|
636
|
-
description: "[assign_testcases] User ID to assign",
|
|
637
|
-
},
|
|
638
|
-
// get_testcases params
|
|
639
|
-
search: { type: "string", description: "[get_testcases] Search text" },
|
|
640
|
-
page: { type: "number", description: "[get_testcases] Page number" },
|
|
641
|
-
limit: { type: "number", description: "[get_testcases] Items per page" },
|
|
615
|
+
cycleId: S,
|
|
616
|
+
title: S,
|
|
617
|
+
summary: S,
|
|
618
|
+
status: STATUS,
|
|
619
|
+
planned_start_date: S,
|
|
620
|
+
planned_end_date: S,
|
|
621
|
+
folder_id: N,
|
|
622
|
+
ids: CSV,
|
|
623
|
+
testcase_ids: CSV,
|
|
624
|
+
assignee_id: N,
|
|
625
|
+
search: S,
|
|
626
|
+
page: N,
|
|
627
|
+
limit: N,
|
|
628
|
+
verbose: VERBOSE,
|
|
642
629
|
},
|
|
643
630
|
required: ["projectId", "action"],
|
|
644
631
|
},
|
|
645
632
|
};
|
|
646
633
|
const EXECUTE_TESTS_TOOL = {
|
|
647
634
|
name: "execute_tests",
|
|
648
|
-
description: "Record
|
|
649
|
-
"Pass a single result or an array of results. " +
|
|
650
|
-
"Valid execution statuses: pass, fail, blocked, not_executed. " +
|
|
651
|
-
"The test cases must already be linked to the cycle (manage_test_cycle link_testcases).",
|
|
635
|
+
description: "Record results for test cases already linked to the cycle.",
|
|
652
636
|
inputSchema: {
|
|
653
637
|
type: "object",
|
|
654
638
|
properties: {
|
|
655
|
-
projectId:
|
|
656
|
-
cycleId:
|
|
639
|
+
projectId: S,
|
|
640
|
+
cycleId: S,
|
|
657
641
|
results: {
|
|
658
|
-
type: "
|
|
659
|
-
|
|
660
|
-
"
|
|
642
|
+
type: "array",
|
|
643
|
+
items: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: { testcase_id: S, execution_status: { type: "string", enum: ["pass", "fail", "blocked", "not_executed"] }, actual_result: S },
|
|
646
|
+
required: ["testcase_id", "execution_status"],
|
|
647
|
+
},
|
|
661
648
|
},
|
|
662
649
|
mode: {
|
|
663
650
|
type: "string",
|
|
664
651
|
enum: ["manual", "automation"],
|
|
665
|
-
description: "
|
|
666
|
-
"automation: recorded as a CI/automation run, where a failure from any earlier automation run keeps the case failed. Use only for CI results.",
|
|
652
|
+
description: "automation = CI run: an earlier CI fail keeps it failed",
|
|
667
653
|
},
|
|
668
654
|
},
|
|
669
655
|
required: ["projectId", "cycleId", "results"],
|
|
@@ -671,177 +657,56 @@ const EXECUTE_TESTS_TOOL = {
|
|
|
671
657
|
};
|
|
672
658
|
const MANAGE_TEST_PLAN_TOOL = {
|
|
673
659
|
name: "manage_test_plan",
|
|
674
|
-
description: "
|
|
675
|
-
"
|
|
676
|
-
" create - Create a plan. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
677
|
-
" update - Update a plan. Params: planId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
678
|
-
" delete - Delete plan(s). Params: ids (required, comma-separated)\n" +
|
|
679
|
-
" list - List all plans. Params: search, page, limit\n" +
|
|
680
|
-
" get_details - Get plan details. Params: planId (required)\n" +
|
|
681
|
-
" link_cycles - Link cycles to plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
682
|
-
" unlink_cycles - Unlink cycles from plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
683
|
-
" get_cycles - List cycles in plan. Params: planId (required), search, page, limit\n" +
|
|
684
|
-
" get_testcases - List test cases in plan. Params: planId (required), search, page, limit\n\n" +
|
|
685
|
-
"VALID STATUSES: open, in_progress, completed",
|
|
660
|
+
description: "create(title*, status, dates, folder_id) | update(planId*) | delete(ids*) | list | get_details(planId*) | link_cycles/unlink_cycles(planId*, cycle_ids*) | " +
|
|
661
|
+
"get_cycles/get_testcases(planId*). Dates YYYY-MM-DD.",
|
|
686
662
|
annotations: { destructiveHint: true },
|
|
687
663
|
inputSchema: {
|
|
688
664
|
type: "object",
|
|
689
665
|
properties: {
|
|
690
|
-
projectId:
|
|
666
|
+
projectId: S,
|
|
691
667
|
action: {
|
|
692
668
|
type: "string",
|
|
693
|
-
enum: [
|
|
694
|
-
"create",
|
|
695
|
-
"update",
|
|
696
|
-
"delete",
|
|
697
|
-
"list",
|
|
698
|
-
"get_details",
|
|
699
|
-
"link_cycles",
|
|
700
|
-
"unlink_cycles",
|
|
701
|
-
"get_cycles",
|
|
702
|
-
"get_testcases",
|
|
703
|
-
],
|
|
704
|
-
description: "Action to perform",
|
|
705
|
-
},
|
|
706
|
-
// Identifier
|
|
707
|
-
planId: {
|
|
708
|
-
type: "string",
|
|
709
|
-
description: "Test plan ID (e.g. TPLAN-1). Required for update/delete/get_details/link_cycles/unlink_cycles/get_cycles/get_testcases.",
|
|
710
|
-
},
|
|
711
|
-
// create/update fields
|
|
712
|
-
title: { type: "string", description: "[create/update] Plan title" },
|
|
713
|
-
summary: { type: "string", description: "[create/update] Description" },
|
|
714
|
-
status: { type: "string", enum: ["open", "in_progress", "completed"], description: "[create/update] Status" },
|
|
715
|
-
planned_start_date: {
|
|
716
|
-
type: "string",
|
|
717
|
-
description: "[create/update] Start date (YYYY-MM-DD)",
|
|
669
|
+
enum: ["create", "update", "delete", "list", "get_details", "link_cycles", "unlink_cycles", "get_cycles", "get_testcases"],
|
|
718
670
|
},
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
description: "[link_cycles/unlink_cycles] Comma-separated test cycle IDs",
|
|
733
|
-
},
|
|
734
|
-
// list/get_cycles/get_testcases pagination
|
|
735
|
-
search: { type: "string", description: "[list/get_cycles/get_testcases] Search text" },
|
|
736
|
-
page: { type: "number", description: "[list/get_cycles/get_testcases] Page number" },
|
|
737
|
-
limit: { type: "number", description: "[list/get_cycles/get_testcases] Items per page" },
|
|
671
|
+
planId: S,
|
|
672
|
+
title: S,
|
|
673
|
+
summary: S,
|
|
674
|
+
status: STATUS,
|
|
675
|
+
planned_start_date: S,
|
|
676
|
+
planned_end_date: S,
|
|
677
|
+
folder_id: N,
|
|
678
|
+
ids: CSV,
|
|
679
|
+
cycle_ids: CSV,
|
|
680
|
+
search: S,
|
|
681
|
+
page: N,
|
|
682
|
+
limit: N,
|
|
683
|
+
verbose: VERBOSE,
|
|
738
684
|
},
|
|
739
685
|
required: ["projectId", "action"],
|
|
740
686
|
},
|
|
741
687
|
};
|
|
742
688
|
const GET_REPORT_TOOL = {
|
|
743
689
|
name: "get_report",
|
|
744
|
-
description: "
|
|
745
|
-
"(the insight reports are computed from your data; they do not use AI credits). " +
|
|
746
|
-
"Available report types: execution_summary, execution_by_cycle, execution_by_tester, execution_by_priority, " +
|
|
747
|
-
"execution_by_environment, execution_by_folder, requirement_coverage, traceability_matrix, " +
|
|
748
|
-
"failed_requirements, uncovered_requirements, testcase_coverage, unlinked_testcases, " +
|
|
749
|
-
"defects_by_folder, tester_workload, testcase_distribution, " +
|
|
750
|
-
"execution_trend, execution_burnup, execution_burndown, test_creation, cycle_comparison, " +
|
|
751
|
-
"scorecard_by_folder, scorecard_by_tester, created_vs_executed, execution_by_automation, " +
|
|
752
|
-
"defects_by_cycle, defects_by_tester, requirement_coverage_trend, " +
|
|
753
|
-
"flaky_tests, release_readiness, risk_heatmap_folder, risk_heatmap_feature, " +
|
|
754
|
-
"testcase_quality, predictive_failure, smart_prioritization, tester_effectiveness, " +
|
|
755
|
-
"stale_tests, defect_hotspots, cycle_health, suite_optimization, " +
|
|
756
|
-
"requirement_risk_matrix, execution_velocity, project_health.",
|
|
690
|
+
description: "Project reports (insights use no AI credits).",
|
|
757
691
|
annotations: { readOnlyHint: true },
|
|
758
692
|
inputSchema: {
|
|
759
693
|
type: "object",
|
|
760
694
|
properties: {
|
|
761
|
-
projectId:
|
|
762
|
-
report_type: {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
},
|
|
770
|
-
|
|
771
|
-
type: "string",
|
|
772
|
-
description: "Only this test plan (one ID, e.g. TPLAN-2)",
|
|
773
|
-
},
|
|
774
|
-
folder_id: { type: "number", description: "Only this folder" },
|
|
775
|
-
startDate: { type: "string", description: "Start date filter (YYYY-MM-DD)" },
|
|
776
|
-
endDate: { type: "string", description: "End date filter (YYYY-MM-DD)" },
|
|
777
|
-
granularity: {
|
|
778
|
-
type: "string",
|
|
779
|
-
enum: ["day", "week", "month"],
|
|
780
|
-
description: "Granularity for trend reports",
|
|
781
|
-
},
|
|
782
|
-
// cycle_comparison specific
|
|
783
|
-
cycle_ids: {
|
|
784
|
-
type: "string",
|
|
785
|
-
description: "[cycle_comparison] Comma-separated cycle IDs to compare",
|
|
786
|
-
},
|
|
695
|
+
projectId: S,
|
|
696
|
+
report_type: { type: "string", enum: Object.keys(REPORT_TYPE_MAP) },
|
|
697
|
+
test_cycle_id: { type: "string", description: "one id" },
|
|
698
|
+
test_plan_id: { type: "string", description: "one id" },
|
|
699
|
+
folder_id: N,
|
|
700
|
+
startDate: { type: "string", description: "YYYY-MM-DD" },
|
|
701
|
+
endDate: { type: "string", description: "YYYY-MM-DD, inclusive" },
|
|
702
|
+
granularity: { type: "string", enum: ["day", "week", "month"] },
|
|
703
|
+
cycle_ids: { type: "string", description: "cycle_comparison" },
|
|
704
|
+
verbose: VERBOSE,
|
|
787
705
|
},
|
|
788
706
|
required: ["projectId", "report_type"],
|
|
789
707
|
},
|
|
790
708
|
};
|
|
791
709
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
-
// REPORT TYPE → ENDPOINT MAPPING
|
|
793
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
794
|
-
const REPORT_TYPE_MAP = {
|
|
795
|
-
// Execution
|
|
796
|
-
execution_summary: "execution/summary",
|
|
797
|
-
execution_by_cycle: "execution/by-cycle",
|
|
798
|
-
execution_by_tester: "execution/by-tester",
|
|
799
|
-
execution_by_priority: "execution/by-priority",
|
|
800
|
-
execution_by_environment: "execution/by-environment",
|
|
801
|
-
execution_by_folder: "execution/by-folder",
|
|
802
|
-
// Coverage
|
|
803
|
-
requirement_coverage: "coverage/requirement-coverage",
|
|
804
|
-
traceability_matrix: "coverage/traceability-matrix",
|
|
805
|
-
failed_requirements: "coverage/failed-requirements",
|
|
806
|
-
uncovered_requirements: "coverage/uncovered-requirements",
|
|
807
|
-
testcase_coverage: "coverage/testcase-coverage",
|
|
808
|
-
unlinked_testcases: "coverage/unlinked-testcases",
|
|
809
|
-
// Defects
|
|
810
|
-
defects_by_folder: "defects/by-folder",
|
|
811
|
-
// Team
|
|
812
|
-
tester_workload: "team/tester-workload",
|
|
813
|
-
testcase_distribution: "team/testcase-distribution",
|
|
814
|
-
// Trends
|
|
815
|
-
execution_trend: "trends/execution-trend",
|
|
816
|
-
execution_burnup: "trends/execution-burnup",
|
|
817
|
-
execution_burndown: "trends/execution-burndown",
|
|
818
|
-
test_creation: "trends/test-creation",
|
|
819
|
-
cycle_comparison: "trends/cycle-comparison",
|
|
820
|
-
scorecard_by_folder: "trends/scorecard-by-folder",
|
|
821
|
-
scorecard_by_tester: "trends/scorecard-by-tester",
|
|
822
|
-
created_vs_executed: "trends/created-vs-executed",
|
|
823
|
-
execution_by_automation: "trends/execution-by-automation",
|
|
824
|
-
defects_by_cycle: "trends/defects-by-cycle",
|
|
825
|
-
defects_by_tester: "trends/defects-by-tester",
|
|
826
|
-
requirement_coverage_trend: "trends/requirement-coverage-trend",
|
|
827
|
-
// AI Insights
|
|
828
|
-
flaky_tests: "ai/flaky-tests",
|
|
829
|
-
release_readiness: "ai/release-readiness",
|
|
830
|
-
risk_heatmap_folder: "ai/risk-heatmap-folder",
|
|
831
|
-
risk_heatmap_feature: "ai/risk-heatmap-feature",
|
|
832
|
-
testcase_quality: "ai/testcase-quality",
|
|
833
|
-
predictive_failure: "ai/predictive-failure",
|
|
834
|
-
smart_prioritization: "ai/smart-prioritization",
|
|
835
|
-
tester_effectiveness: "ai/tester-effectiveness",
|
|
836
|
-
stale_tests: "ai/stale-tests",
|
|
837
|
-
defect_hotspots: "ai/defect-hotspots",
|
|
838
|
-
cycle_health: "ai/cycle-health",
|
|
839
|
-
suite_optimization: "ai/suite-optimization",
|
|
840
|
-
requirement_risk_matrix: "ai/requirement-risk-matrix",
|
|
841
|
-
execution_velocity: "ai/execution-velocity",
|
|
842
|
-
project_health: "ai/project-health",
|
|
843
|
-
};
|
|
844
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
845
710
|
// TOOL HANDLERS
|
|
846
711
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
847
712
|
// 1. list_projects
|
|
@@ -852,9 +717,7 @@ async function handleListProjects(args) {
|
|
|
852
717
|
limit: p(args, "limit"),
|
|
853
718
|
},
|
|
854
719
|
});
|
|
855
|
-
return
|
|
856
|
-
"Next: call get_project_structure(projectId) to see folders, labels, and allowed field values.",
|
|
857
|
-
]);
|
|
720
|
+
return listResponse(args, data, LIST_KEYS.projects);
|
|
858
721
|
}
|
|
859
722
|
// 2. get_project_structure
|
|
860
723
|
async function handleGetProjectStructure(args) {
|
|
@@ -891,9 +754,34 @@ async function handleGetProjectStructure(args) {
|
|
|
891
754
|
}));
|
|
892
755
|
}
|
|
893
756
|
await Promise.all(promises);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
757
|
+
if (p(args, "verbose"))
|
|
758
|
+
return jsonResponse(result, true);
|
|
759
|
+
const unwrap = (d) => (d && typeof d === "object" && !Array.isArray(d) && "data" in d ? d.data : d);
|
|
760
|
+
const shaped = {};
|
|
761
|
+
if (result.folders) {
|
|
762
|
+
const f = unwrap(result.folders);
|
|
763
|
+
shaped.folders = flattenFolders(Array.isArray(f) ? f : f?.folders ?? []);
|
|
764
|
+
}
|
|
765
|
+
if (result.labels) {
|
|
766
|
+
const l = unwrap(result.labels);
|
|
767
|
+
shaped.labels = (Array.isArray(l) ? l : []).map((x) => x?.labels ?? x?.name ?? x);
|
|
768
|
+
}
|
|
769
|
+
if (result.members) {
|
|
770
|
+
const m = unwrap(result.members);
|
|
771
|
+
shaped.members = (Array.isArray(m) ? m : m?.team_members ?? m?.members ?? []).map(memberRow);
|
|
772
|
+
}
|
|
773
|
+
if (result.field_options) {
|
|
774
|
+
shaped.field_options = {};
|
|
775
|
+
for (const f of Array.isArray(result.field_options) ? result.field_options : []) {
|
|
776
|
+
// List fields: allowed values. Custom free-text fields: their type, so the AI knows the custom_fields name.
|
|
777
|
+
if (Array.isArray(f?.values) && f.values.length)
|
|
778
|
+
shaped.field_options[f.fieldName] = f.values;
|
|
779
|
+
else if (f?.isSystem === false)
|
|
780
|
+
shaped.field_options[f.fieldName] = f.fieldType;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Not compacted: an empty section must read as "none" (folders: []), not vanish
|
|
784
|
+
return { content: [{ type: "text", text: JSON.stringify(shaped) }] };
|
|
897
785
|
}
|
|
898
786
|
// 3. search_testcases
|
|
899
787
|
async function handleSearchTestcases(args) {
|
|
@@ -925,7 +813,7 @@ async function handleSearchTestcases(args) {
|
|
|
925
813
|
tc.url = `${baseUrl}/${projectId}/testcases/${tc.id}`;
|
|
926
814
|
});
|
|
927
815
|
}
|
|
928
|
-
return
|
|
816
|
+
return listResponse(args, data, LIST_KEYS.testcases, "id");
|
|
929
817
|
}
|
|
930
818
|
// 4. get_testcase
|
|
931
819
|
async function handleGetTestcase(args) {
|
|
@@ -941,7 +829,7 @@ async function handleGetTestcase(args) {
|
|
|
941
829
|
if (data?.data?.testcase) {
|
|
942
830
|
data.data.testcase.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${testcaseId}`;
|
|
943
831
|
}
|
|
944
|
-
return jsonResponse(data);
|
|
832
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
945
833
|
}
|
|
946
834
|
// 5. manage_testcase
|
|
947
835
|
async function handleManageTestcase(args) {
|
|
@@ -963,9 +851,7 @@ async function handleManageTestcase(args) {
|
|
|
963
851
|
const testcaseData = await buildTestcasePayload(projectId, { ...args, title });
|
|
964
852
|
const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
|
|
965
853
|
const tcId = createResult?.testcaseId || createResult?.data?.id;
|
|
966
|
-
const hints = [
|
|
967
|
-
"To update fields, use manage_testcase(action='update', ids='<id>', field='<name>', value='<value>')",
|
|
968
|
-
];
|
|
854
|
+
const hints = [];
|
|
969
855
|
if (tcId) {
|
|
970
856
|
createResult.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${tcId}`;
|
|
971
857
|
if (testSteps?.length) {
|
|
@@ -987,9 +873,14 @@ async function handleManageTestcase(args) {
|
|
|
987
873
|
if (!Array.isArray(testcases) || testcases.length === 0) {
|
|
988
874
|
throw new Error("testcases must be a non-empty JSON array");
|
|
989
875
|
}
|
|
876
|
+
const shared = {};
|
|
877
|
+
for (const f of BULK_SHARED_FIELDS)
|
|
878
|
+
if (args[f] !== undefined && args[f] !== null && args[f] !== "")
|
|
879
|
+
shared[f] = args[f];
|
|
990
880
|
// Validate everything before creating anything
|
|
991
881
|
const items = [];
|
|
992
|
-
for (const [i,
|
|
882
|
+
for (const [i, item] of testcases.entries()) {
|
|
883
|
+
const tc = item && typeof item === "object" ? { ...shared, ...item } : item;
|
|
993
884
|
if (!tc?.title && !tc?.name)
|
|
994
885
|
throw new Error(`testcases[${i}]: title is required`);
|
|
995
886
|
try {
|
|
@@ -1114,7 +1005,7 @@ async function handleManageTestcase(args) {
|
|
|
1114
1005
|
query: { projectId },
|
|
1115
1006
|
body: { stepIds },
|
|
1116
1007
|
});
|
|
1117
|
-
return jsonResponse(data);
|
|
1008
|
+
return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
|
|
1118
1009
|
}
|
|
1119
1010
|
case "reorder_steps": {
|
|
1120
1011
|
const tcId = validateId(requireArg(args, "testcaseId", ["testcase_id", "id"]), "testcaseId");
|
|
@@ -1124,7 +1015,7 @@ async function handleManageTestcase(args) {
|
|
|
1124
1015
|
const data = await callApi("put", `projects/testcases/teststeps/${projectId}/${tcId}/reorder`, {
|
|
1125
1016
|
body: { stepIds },
|
|
1126
1017
|
});
|
|
1127
|
-
return jsonResponse(data);
|
|
1018
|
+
return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
|
|
1128
1019
|
}
|
|
1129
1020
|
default:
|
|
1130
1021
|
throw new Error(`Unknown action: ${action}. Use: create, create_bulk, update, delete, add_steps, update_step, delete_steps, reorder_steps`);
|
|
@@ -1209,7 +1100,7 @@ async function handleSearchTestCycles(args) {
|
|
|
1209
1100
|
validateId(projectId, "projectId");
|
|
1210
1101
|
const folderId = p(args, "folderId", "folder_id");
|
|
1211
1102
|
const filters = buildFilters(undefined, {
|
|
1212
|
-
status: p(args, "status"),
|
|
1103
|
+
status: normalizePlanStatus(p(args, "status")),
|
|
1213
1104
|
folder_id: folderId === undefined || folderId === null || folderId === "" ? undefined : Number(folderId),
|
|
1214
1105
|
});
|
|
1215
1106
|
const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
|
|
@@ -1228,7 +1119,7 @@ async function handleSearchTestCycles(args) {
|
|
|
1228
1119
|
cycle.url = `${baseUrl}/${projectId}/testcycles/${id}`;
|
|
1229
1120
|
});
|
|
1230
1121
|
}
|
|
1231
|
-
return
|
|
1122
|
+
return listResponse(args, data, LIST_KEYS.cycles, "test_cycle_id");
|
|
1232
1123
|
}
|
|
1233
1124
|
// 8. manage_test_cycle
|
|
1234
1125
|
async function handleManageTestCycle(args) {
|
|
@@ -1248,7 +1139,7 @@ async function handleManageTestCycle(args) {
|
|
|
1248
1139
|
if (p(args, "summary"))
|
|
1249
1140
|
body.summary = p(args, "summary");
|
|
1250
1141
|
if (p(args, "status"))
|
|
1251
|
-
body.status = p(args, "status");
|
|
1142
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1252
1143
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1253
1144
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1254
1145
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1268,7 +1159,7 @@ async function handleManageTestCycle(args) {
|
|
|
1268
1159
|
if (p(args, "summary"))
|
|
1269
1160
|
body.summary = p(args, "summary");
|
|
1270
1161
|
if (p(args, "status"))
|
|
1271
|
-
body.status = p(args, "status");
|
|
1162
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1272
1163
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1273
1164
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1274
1165
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1295,7 +1186,7 @@ async function handleManageTestCycle(args) {
|
|
|
1295
1186
|
if (data?.data) {
|
|
1296
1187
|
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testcycles/${cycleId}`;
|
|
1297
1188
|
}
|
|
1298
|
-
return jsonResponse(data);
|
|
1189
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1299
1190
|
}
|
|
1300
1191
|
case "get_testcases": {
|
|
1301
1192
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
@@ -1309,7 +1200,7 @@ async function handleManageTestCycle(args) {
|
|
|
1309
1200
|
limit: p(args, "limit"),
|
|
1310
1201
|
},
|
|
1311
1202
|
});
|
|
1312
|
-
return
|
|
1203
|
+
return listResponse(args, data, LIST_KEYS.linkedCases);
|
|
1313
1204
|
}
|
|
1314
1205
|
case "link_testcases": {
|
|
1315
1206
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
@@ -1367,7 +1258,8 @@ async function handleExecuteTests(args) {
|
|
|
1367
1258
|
throw new Error("results is required");
|
|
1368
1259
|
validateId(projectId, "projectId");
|
|
1369
1260
|
validateId(cycleId, "cycleId");
|
|
1370
|
-
const
|
|
1261
|
+
const parsedResults = safeJsonParse(resultsRaw, "results");
|
|
1262
|
+
const results = parsedResults && !Array.isArray(parsedResults) && typeof parsedResults === "object" ? [parsedResults] : parsedResults;
|
|
1371
1263
|
if (!Array.isArray(results) || results.length === 0) {
|
|
1372
1264
|
throw new Error("results must be a non-empty array");
|
|
1373
1265
|
}
|
|
@@ -1386,7 +1278,7 @@ async function handleExecuteTests(args) {
|
|
|
1386
1278
|
}
|
|
1387
1279
|
const out = {
|
|
1388
1280
|
testcase_id: validateId(testcaseId, "testcase_id"),
|
|
1389
|
-
execution_status: status,
|
|
1281
|
+
execution_status: normalizeExecutionStatus(status, `results[${i}]`),
|
|
1390
1282
|
};
|
|
1391
1283
|
const actual = r.actual_result || r.actualResult;
|
|
1392
1284
|
if (actual)
|
|
@@ -1438,7 +1330,7 @@ async function handleManageTestPlan(args) {
|
|
|
1438
1330
|
if (p(args, "summary"))
|
|
1439
1331
|
body.summary = p(args, "summary");
|
|
1440
1332
|
if (p(args, "status"))
|
|
1441
|
-
body.status = p(args, "status");
|
|
1333
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1442
1334
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1443
1335
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1444
1336
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1458,7 +1350,7 @@ async function handleManageTestPlan(args) {
|
|
|
1458
1350
|
if (p(args, "summary"))
|
|
1459
1351
|
body.summary = p(args, "summary");
|
|
1460
1352
|
if (p(args, "status"))
|
|
1461
|
-
body.status = p(args, "status");
|
|
1353
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1462
1354
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1463
1355
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1464
1356
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1492,7 +1384,7 @@ async function handleManageTestPlan(args) {
|
|
|
1492
1384
|
plan.url = `${baseUrl}/${projectId}/testplans/${id}`;
|
|
1493
1385
|
});
|
|
1494
1386
|
}
|
|
1495
|
-
return
|
|
1387
|
+
return listResponse(args, data, LIST_KEYS.plans, "test_plan_id");
|
|
1496
1388
|
}
|
|
1497
1389
|
case "get_details": {
|
|
1498
1390
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1503,7 +1395,7 @@ async function handleManageTestPlan(args) {
|
|
|
1503
1395
|
if (data?.data) {
|
|
1504
1396
|
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testplans/${planId}`;
|
|
1505
1397
|
}
|
|
1506
|
-
return jsonResponse(data);
|
|
1398
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1507
1399
|
}
|
|
1508
1400
|
case "link_cycles": {
|
|
1509
1401
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1541,7 +1433,7 @@ async function handleManageTestPlan(args) {
|
|
|
1541
1433
|
limit: p(args, "limit"),
|
|
1542
1434
|
},
|
|
1543
1435
|
});
|
|
1544
|
-
return
|
|
1436
|
+
return listResponse(args, data, LIST_KEYS.cycles);
|
|
1545
1437
|
}
|
|
1546
1438
|
case "get_testcases": {
|
|
1547
1439
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1555,7 +1447,7 @@ async function handleManageTestPlan(args) {
|
|
|
1555
1447
|
limit: p(args, "limit"),
|
|
1556
1448
|
},
|
|
1557
1449
|
});
|
|
1558
|
-
return
|
|
1450
|
+
return listResponse(args, data, LIST_KEYS.linkedCases);
|
|
1559
1451
|
}
|
|
1560
1452
|
default:
|
|
1561
1453
|
throw new Error(`Unknown action: ${action}. Use: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases`);
|
|
@@ -1603,7 +1495,7 @@ async function handleGetReport(args) {
|
|
|
1603
1495
|
const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
|
|
1604
1496
|
query,
|
|
1605
1497
|
});
|
|
1606
|
-
return jsonResponse(data);
|
|
1498
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1607
1499
|
}
|
|
1608
1500
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1609
1501
|
// SERVER SETUP
|