zephyr-enterprise-tools 1.0.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/LICENSE +21 -0
- package/README.md +260 -0
- package/cli.js +465 -0
- package/package.json +53 -0
- package/zephyr-enterprise-tools.js +1129 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zephyr Enterprise Tools Module
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive tools for Zephyr Enterprise:
|
|
5
|
+
*
|
|
6
|
+
* RELEASE READINESS (Quality Gates):
|
|
7
|
+
* 1. Requirement Coverage Gate - Are requirements covered by tests?
|
|
8
|
+
* 2. Test Plan Analysis Gate - Are tests planned and assigned?
|
|
9
|
+
* 3. Test Execution Gate - Have tests been executed?
|
|
10
|
+
* 4. Defect Quality Gate - Are critical defects resolved?
|
|
11
|
+
*
|
|
12
|
+
* ANALYTICS & INSIGHTS:
|
|
13
|
+
* 5. Project Health - Overall project health metrics
|
|
14
|
+
* 6. Test Coverage - Detailed test coverage analysis
|
|
15
|
+
* 7. Failed Tests - Analyze and list failed tests
|
|
16
|
+
* 8. Requirement Coverage - Requirements without test coverage
|
|
17
|
+
* 9. Test Case Trends - Test case creation/execution trends
|
|
18
|
+
* 10. Search Test Cases - Search test cases by criteria
|
|
19
|
+
* 11. User Activity - User activity and productivity metrics
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* import { ZephyrTools } from './quality-gates.js';
|
|
23
|
+
* const tools = new ZephyrTools({ baseUrl, username, password });
|
|
24
|
+
* const report = await tools.runAllGates(projectId, releaseId);
|
|
25
|
+
* const health = await tools.getProjectHealth(projectId, releaseId);
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ─── Configuration & Thresholds ───────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export const THRESHOLDS = {
|
|
31
|
+
requirementCoverage: {
|
|
32
|
+
go: 70,
|
|
33
|
+
description: "≥70% coverage = GO, <70% = NO GO"
|
|
34
|
+
},
|
|
35
|
+
testPlanAnalysis: {
|
|
36
|
+
noGo: 80,
|
|
37
|
+
conditionalGo: 90,
|
|
38
|
+
description: "<80% = NO GO, 80-90% = CONDITIONAL GO, ≥90% = GO"
|
|
39
|
+
},
|
|
40
|
+
testExecution: {
|
|
41
|
+
noGo: 90,
|
|
42
|
+
conditionalGo: 97,
|
|
43
|
+
description: "<90% = NO GO, 90-97% = CONDITIONAL GO, ≥97% = GO"
|
|
44
|
+
},
|
|
45
|
+
defectQuality: {
|
|
46
|
+
blockerLimit: 0,
|
|
47
|
+
highRiskLimit: 10,
|
|
48
|
+
description: "Blocker >0 = NO GO, High-risk >10 = NO GO, 1-10 = CONDITIONAL, 0 = GO"
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const RESOLVED_STATUSES = ['done', 'closed', 'ready for release', 'verified', 'resolved', 'fixed'];
|
|
53
|
+
export const BLOCKER_PRIORITIES = ['blocker', 'critical', 'p1', '1', 'highest'];
|
|
54
|
+
export const HIGH_RISK_PRIORITIES = ['high', 'medium', 'p2', 'p3', '2', '3'];
|
|
55
|
+
export const LOW_RISK_PRIORITIES = ['low', 'trivial', 'p4', 'p5', '4', '5', 'lowest', 'minor'];
|
|
56
|
+
|
|
57
|
+
// ─── Quality Gates Class ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export class QualityGates {
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.baseUrl = (config.baseUrl || process.env.ZEPHYR_BASE_URL || "").replace(/\/$/, "");
|
|
62
|
+
this.username = config.username || process.env.ZEPHYR_USERNAME || "";
|
|
63
|
+
this.password = config.password || process.env.ZEPHYR_PASSWORD || "";
|
|
64
|
+
this.token = config.token || process.env.ZEPHYR_TOKEN || "";
|
|
65
|
+
|
|
66
|
+
if (!this.baseUrl) {
|
|
67
|
+
throw new Error("ZEPHYR_BASE_URL is required");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── HTTP Helper ────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
authHeader() {
|
|
74
|
+
if (this.token) return { Authorization: `Bearer ${this.token}` };
|
|
75
|
+
return { Authorization: `Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async request(method, path, params = {}) {
|
|
79
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
80
|
+
for (const [k, v] of Object.entries(params)) {
|
|
81
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
82
|
+
}
|
|
83
|
+
const res = await fetch(url.toString(), {
|
|
84
|
+
method,
|
|
85
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", ...this.authHeader() },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const text = await res.text().catch(() => "");
|
|
89
|
+
throw new Error(`Zephyr API ${res.status}: ${text}`);
|
|
90
|
+
}
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async GET(path, params) {
|
|
95
|
+
return this.request("GET", path, params);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async PUT(path, params, body) {
|
|
99
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
100
|
+
for (const [k, v] of Object.entries(params)) {
|
|
101
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
102
|
+
}
|
|
103
|
+
const res = await fetch(url.toString(), {
|
|
104
|
+
method: "PUT",
|
|
105
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", ...this.authHeader() },
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const text = await res.text().catch(() => "");
|
|
110
|
+
throw new Error(`Zephyr API ${res.status}: ${text}`);
|
|
111
|
+
}
|
|
112
|
+
return res.json();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Gate 1: Requirement Coverage ───────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
async requirementCoverageGate(projectId, releaseId) {
|
|
118
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: true });
|
|
119
|
+
|
|
120
|
+
const reqSummary = summary.requirement || {};
|
|
121
|
+
const total = reqSummary.totalRequirementCount || 0;
|
|
122
|
+
const covered = reqSummary.mappedRequirementCount || 0;
|
|
123
|
+
const notCovered = reqSummary.unmappedRequirementCount || 0;
|
|
124
|
+
const coveragePercentage = total > 0 ? Math.round((covered / total) * 100 * 100) / 100 : 0;
|
|
125
|
+
|
|
126
|
+
const threshold = THRESHOLDS.requirementCoverage.go;
|
|
127
|
+
const status = coveragePercentage >= threshold ? "GO" : "NO GO";
|
|
128
|
+
const statusMessage = coveragePercentage >= threshold
|
|
129
|
+
? `Coverage is ${coveragePercentage}% (≥${threshold}%) - Ready for release`
|
|
130
|
+
: `Coverage is ${coveragePercentage}% (<${threshold}%) - Not ready. ${notCovered} requirements need test coverage.`;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
gate: "Requirement Coverage",
|
|
134
|
+
projectId,
|
|
135
|
+
releaseId,
|
|
136
|
+
totalRequirements: total,
|
|
137
|
+
coveredRequirements: covered,
|
|
138
|
+
notCoveredRequirements: notCovered,
|
|
139
|
+
coveragePercentage,
|
|
140
|
+
threshold,
|
|
141
|
+
status,
|
|
142
|
+
statusMessage,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Gate 2: Test Plan Analysis ─────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
async testPlanAnalysisGate(projectId, releaseId) {
|
|
149
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
|
|
150
|
+
|
|
151
|
+
const totalTestcases = summary.testcase?.totalTestcaseCount || 0;
|
|
152
|
+
const mappedRequirements = summary.requirement?.mappedRequirementCount || 0;
|
|
153
|
+
const totalRequirements = summary.requirement?.totalRequirementCount || 0;
|
|
154
|
+
|
|
155
|
+
// Get all executions for the release
|
|
156
|
+
const executionData = await this.GET("/execution", {
|
|
157
|
+
releaseid: releaseId,
|
|
158
|
+
offset: 0,
|
|
159
|
+
pagesize: 10000,
|
|
160
|
+
includeanyoneuser: true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const executions = executionData.results || executionData || [];
|
|
164
|
+
const totalExecutions = Array.isArray(executions) ? executions.length : 0;
|
|
165
|
+
|
|
166
|
+
// Count assigned executions
|
|
167
|
+
const assignedExecutions = Array.isArray(executions)
|
|
168
|
+
? executions.filter(e => e.testerId && e.testerId > 0).length
|
|
169
|
+
: 0;
|
|
170
|
+
|
|
171
|
+
// Count unique planned test cases
|
|
172
|
+
const uniquePlannedTestcases = new Set(
|
|
173
|
+
Array.isArray(executions)
|
|
174
|
+
? executions.map(e => e.tcrTreeTestcase?.testcase?.id || e.testcaseId).filter(Boolean)
|
|
175
|
+
: []
|
|
176
|
+
).size;
|
|
177
|
+
|
|
178
|
+
// Calculate metrics
|
|
179
|
+
const testcasePlanningPct = totalTestcases > 0
|
|
180
|
+
? Math.min(100, Math.round((uniquePlannedTestcases / totalTestcases) * 100 * 100) / 100)
|
|
181
|
+
: 0;
|
|
182
|
+
|
|
183
|
+
const executionAssignmentPct = totalExecutions > 0
|
|
184
|
+
? Math.round((assignedExecutions / totalExecutions) * 100 * 100) / 100
|
|
185
|
+
: 0;
|
|
186
|
+
|
|
187
|
+
const overallPlanningPct = Math.round(((testcasePlanningPct + executionAssignmentPct) / 2) * 100) / 100;
|
|
188
|
+
|
|
189
|
+
// Determine status
|
|
190
|
+
const { noGo, conditionalGo } = THRESHOLDS.testPlanAnalysis;
|
|
191
|
+
let status, statusMessage;
|
|
192
|
+
|
|
193
|
+
if (overallPlanningPct < noGo) {
|
|
194
|
+
status = "NO GO";
|
|
195
|
+
statusMessage = `Overall planning is ${overallPlanningPct}% (<${noGo}%) - Not ready.`;
|
|
196
|
+
} else if (overallPlanningPct < conditionalGo) {
|
|
197
|
+
status = "CONDITIONAL GO";
|
|
198
|
+
statusMessage = `Overall planning is ${overallPlanningPct}% (${noGo}-${conditionalGo}%) - Proceed with caution.`;
|
|
199
|
+
} else {
|
|
200
|
+
status = "GO";
|
|
201
|
+
statusMessage = `Overall planning is ${overallPlanningPct}% (≥${conditionalGo}%) - Ready for execution.`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
gate: "Test Plan Analysis",
|
|
206
|
+
projectId,
|
|
207
|
+
releaseId,
|
|
208
|
+
analysis: {
|
|
209
|
+
testcasePlanning: {
|
|
210
|
+
totalTestcases,
|
|
211
|
+
plannedTestcases: uniquePlannedTestcases,
|
|
212
|
+
percentage: testcasePlanningPct,
|
|
213
|
+
},
|
|
214
|
+
executionAssignment: {
|
|
215
|
+
totalExecutions,
|
|
216
|
+
assignedExecutions,
|
|
217
|
+
percentage: executionAssignmentPct,
|
|
218
|
+
},
|
|
219
|
+
requirementCoverage: {
|
|
220
|
+
totalRequirements,
|
|
221
|
+
mappedRequirements,
|
|
222
|
+
percentage: totalRequirements > 0 ? Math.round((mappedRequirements / totalRequirements) * 100 * 100) / 100 : 0,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
overallPlanningPercentage: overallPlanningPct,
|
|
226
|
+
thresholds: { noGo, conditionalGo },
|
|
227
|
+
status,
|
|
228
|
+
statusMessage,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Gate 3: Test Execution Gate ────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async testExecutionGate(projectId, releaseId) {
|
|
235
|
+
const executionData = await this.GET("/execution", {
|
|
236
|
+
releaseid: releaseId,
|
|
237
|
+
offset: 0,
|
|
238
|
+
pagesize: 10000,
|
|
239
|
+
includeanyoneuser: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const executions = executionData.results || executionData || [];
|
|
243
|
+
const totalExecutions = Array.isArray(executions) ? executions.length : 0;
|
|
244
|
+
|
|
245
|
+
if (totalExecutions === 0) {
|
|
246
|
+
return {
|
|
247
|
+
gate: "Test Execution",
|
|
248
|
+
projectId,
|
|
249
|
+
releaseId,
|
|
250
|
+
totalPlannedTests: 0,
|
|
251
|
+
completedTests: 0,
|
|
252
|
+
executionPercentage: 0,
|
|
253
|
+
status: "NO GO",
|
|
254
|
+
statusMessage: "No test executions found for this release.",
|
|
255
|
+
breakdown: { passed: 0, failed: 0, notApplicable: 0, wip: 0, blocked: 0, notExecuted: 0 },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Count by status (from lastTestResult.executionStatus)
|
|
260
|
+
let passed = 0, failed = 0, notApplicable = 0, wip = 0, blocked = 0, notExecuted = 0;
|
|
261
|
+
|
|
262
|
+
for (const exec of executions) {
|
|
263
|
+
const status = exec.lastTestResult?.executionStatus || exec.status || exec.executionStatus || 0;
|
|
264
|
+
switch (Number(status)) {
|
|
265
|
+
case 1: passed++; break;
|
|
266
|
+
case 2: failed++; break;
|
|
267
|
+
case 3: wip++; break;
|
|
268
|
+
case 4: blocked++; break;
|
|
269
|
+
case 5: notApplicable++; break;
|
|
270
|
+
default: notExecuted++; break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const completedTests = passed + failed + notApplicable;
|
|
275
|
+
const incompleteTests = wip + blocked + notExecuted;
|
|
276
|
+
const executionPct = Math.round((completedTests / totalExecutions) * 100 * 100) / 100;
|
|
277
|
+
|
|
278
|
+
// Determine status
|
|
279
|
+
const { noGo, conditionalGo } = THRESHOLDS.testExecution;
|
|
280
|
+
let status, statusMessage;
|
|
281
|
+
|
|
282
|
+
if (executionPct < noGo) {
|
|
283
|
+
status = "NO GO";
|
|
284
|
+
statusMessage = `Execution is ${executionPct}% (<${noGo}%) - ${incompleteTests} tests need resolution.`;
|
|
285
|
+
} else if (executionPct < conditionalGo) {
|
|
286
|
+
status = "CONDITIONAL GO";
|
|
287
|
+
statusMessage = `Execution is ${executionPct}% (${noGo}-${conditionalGo}%) - ${incompleteTests} tests incomplete.`;
|
|
288
|
+
} else {
|
|
289
|
+
status = "GO";
|
|
290
|
+
statusMessage = `Execution is ${executionPct}% (≥${conditionalGo}%) - Gate passed.`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
gate: "Test Execution",
|
|
295
|
+
projectId,
|
|
296
|
+
releaseId,
|
|
297
|
+
totalPlannedTests: totalExecutions,
|
|
298
|
+
completedTests,
|
|
299
|
+
incompleteTests,
|
|
300
|
+
executionPercentage: executionPct,
|
|
301
|
+
thresholds: { noGo, conditionalGo },
|
|
302
|
+
status,
|
|
303
|
+
statusMessage,
|
|
304
|
+
breakdown: { passed, failed, notApplicable, wip, blocked, notExecuted },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Gate 4: Defect Quality Gate ────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
async defectQualityGate(projectId, releaseId) {
|
|
311
|
+
// Step 1: Get defect IDs from release summary
|
|
312
|
+
let defectIds = [];
|
|
313
|
+
try {
|
|
314
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, {});
|
|
315
|
+
const defectSummary = summary.defect || {};
|
|
316
|
+
defectIds = defectSummary.totalDefectIds || [];
|
|
317
|
+
|
|
318
|
+
if (defectIds.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
gate: "Defect Quality",
|
|
321
|
+
projectId,
|
|
322
|
+
releaseId,
|
|
323
|
+
totalDefectsAnalyzed: 0,
|
|
324
|
+
unresolvedDefects: 0,
|
|
325
|
+
status: "GO",
|
|
326
|
+
statusMessage: "No defects found. Quality gate passed.",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
} catch (e) {
|
|
330
|
+
return {
|
|
331
|
+
gate: "Defect Quality",
|
|
332
|
+
projectId,
|
|
333
|
+
releaseId,
|
|
334
|
+
status: "UNKNOWN",
|
|
335
|
+
statusMessage: "Unable to retrieve defect data from release summary.",
|
|
336
|
+
error: e.message,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Step 2: Get full defect details using PUT /v3/defect with JQL search
|
|
341
|
+
// Note: Defect API uses v3, not latest - we need to adjust the path
|
|
342
|
+
let defects = [];
|
|
343
|
+
try {
|
|
344
|
+
const searchString = `issuekey in (${defectIds.join(',')})`;
|
|
345
|
+
// Build v3 URL by replacing /latest/ with /v3/ in baseUrl
|
|
346
|
+
const v3BaseUrl = this.baseUrl.replace('/latest', '/v3');
|
|
347
|
+
const url = new URL(`${v3BaseUrl}/defect`);
|
|
348
|
+
url.searchParams.set('offset', '0');
|
|
349
|
+
url.searchParams.set('maxresult', '100');
|
|
350
|
+
url.searchParams.set('searchtype', '1');
|
|
351
|
+
url.searchParams.set('maptc', 'true');
|
|
352
|
+
url.searchParams.set('projectId', String(projectId));
|
|
353
|
+
url.searchParams.set('order', 'id');
|
|
354
|
+
url.searchParams.set('isascorder', 'true');
|
|
355
|
+
|
|
356
|
+
const res = await fetch(url.toString(), {
|
|
357
|
+
method: 'PUT',
|
|
358
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...this.authHeader() },
|
|
359
|
+
body: JSON.stringify({ searchString }),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (!res.ok) {
|
|
363
|
+
throw new Error(`Defect API ${res.status}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const defectData = await res.json();
|
|
367
|
+
// Response format: { bugsList: [...], maxResult: N }
|
|
368
|
+
defects = defectData?.bugsList || [];
|
|
369
|
+
if (!Array.isArray(defects)) defects = [];
|
|
370
|
+
} catch (e) {
|
|
371
|
+
// Fallback: return with IDs only (treat all as high-risk)
|
|
372
|
+
return {
|
|
373
|
+
gate: "Defect Quality",
|
|
374
|
+
projectId,
|
|
375
|
+
releaseId,
|
|
376
|
+
totalDefectsAnalyzed: defectIds.length,
|
|
377
|
+
unresolvedDefects: defectIds.length,
|
|
378
|
+
status: "CONDITIONAL GO",
|
|
379
|
+
statusMessage: `${defectIds.length} defect(s) found but details unavailable. Review manually.`,
|
|
380
|
+
defectIds,
|
|
381
|
+
note: "Could not fetch defect details. Treat as high-risk.",
|
|
382
|
+
error: e.message,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Step 3: Classify defects by status and priority
|
|
387
|
+
let blockerCount = 0, highRiskCount = 0, lowRiskCount = 0, resolvedCount = 0;
|
|
388
|
+
const blockerDefects = [], highRiskDefects = [], lowRiskDefects = [], resolvedDefects = [];
|
|
389
|
+
|
|
390
|
+
for (const defect of defects) {
|
|
391
|
+
const status = (defect.status || '').toString().toLowerCase();
|
|
392
|
+
const priority = (defect.priority || '').toString().toLowerCase();
|
|
393
|
+
const isResolved = RESOLVED_STATUSES.some(rs => status.includes(rs));
|
|
394
|
+
|
|
395
|
+
const defectInfo = {
|
|
396
|
+
id: defect.alternateId || defect.id,
|
|
397
|
+
summary: defect.shortDesc || defect.name || defect.summary,
|
|
398
|
+
priority: defect.priority,
|
|
399
|
+
status: defect.status,
|
|
400
|
+
resolution: defect.resolution,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (isResolved) {
|
|
404
|
+
resolvedCount++;
|
|
405
|
+
resolvedDefects.push(defectInfo);
|
|
406
|
+
} else if (BLOCKER_PRIORITIES.some(p => priority.includes(p))) {
|
|
407
|
+
blockerCount++;
|
|
408
|
+
blockerDefects.push(defectInfo);
|
|
409
|
+
} else if (HIGH_RISK_PRIORITIES.some(p => priority.includes(p))) {
|
|
410
|
+
highRiskCount++;
|
|
411
|
+
highRiskDefects.push(defectInfo);
|
|
412
|
+
} else if (LOW_RISK_PRIORITIES.some(p => priority.includes(p))) {
|
|
413
|
+
lowRiskCount++;
|
|
414
|
+
lowRiskDefects.push(defectInfo);
|
|
415
|
+
} else {
|
|
416
|
+
// Unknown priority - treat as high risk
|
|
417
|
+
highRiskCount++;
|
|
418
|
+
highRiskDefects.push({ ...defectInfo, note: 'Unknown priority - treated as high-risk' });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const unresolvedCount = blockerCount + highRiskCount + lowRiskCount;
|
|
423
|
+
|
|
424
|
+
// Step 4: Determine GO status
|
|
425
|
+
const { highRiskLimit } = THRESHOLDS.defectQuality;
|
|
426
|
+
let status, statusMessage;
|
|
427
|
+
|
|
428
|
+
if (blockerCount > 0) {
|
|
429
|
+
status = "NO GO";
|
|
430
|
+
statusMessage = `${blockerCount} blocker defect(s) found (${blockerDefects.map(d => d.id).join(', ')}). All must be resolved before release.`;
|
|
431
|
+
} else if (highRiskCount > highRiskLimit) {
|
|
432
|
+
status = "NO GO";
|
|
433
|
+
statusMessage = `${highRiskCount} high-risk defects (>${highRiskLimit} limit). Reduce before release.`;
|
|
434
|
+
} else if (highRiskCount > 0) {
|
|
435
|
+
status = "CONDITIONAL GO";
|
|
436
|
+
statusMessage = `${highRiskCount} high-risk defect(s) found (1-${highRiskLimit} allowed). Plan fixes.`;
|
|
437
|
+
} else if (lowRiskCount > 0) {
|
|
438
|
+
status = "GO";
|
|
439
|
+
statusMessage = `No blocker or high-risk defects. ${lowRiskCount} low-risk defect(s) are acceptable.`;
|
|
440
|
+
} else {
|
|
441
|
+
status = "GO";
|
|
442
|
+
statusMessage = `All ${resolvedCount} defect(s) are resolved. Quality gate passed.`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
gate: "Defect Quality",
|
|
447
|
+
projectId,
|
|
448
|
+
releaseId,
|
|
449
|
+
totalDefectsAnalyzed: defects.length,
|
|
450
|
+
unresolvedDefects: unresolvedCount,
|
|
451
|
+
resolvedDefects: resolvedCount,
|
|
452
|
+
status,
|
|
453
|
+
statusMessage,
|
|
454
|
+
breakdown: {
|
|
455
|
+
blocker: { count: blockerCount, defects: blockerDefects },
|
|
456
|
+
highRisk: { count: highRiskCount, defects: highRiskDefects },
|
|
457
|
+
lowRisk: { count: lowRiskCount, defects: lowRiskDefects },
|
|
458
|
+
resolved: { count: resolvedCount, defects: resolvedDefects },
|
|
459
|
+
},
|
|
460
|
+
defectIds,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─── Run All Gates ──────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
async runAllGates(projectId, releaseId) {
|
|
467
|
+
const results = await Promise.all([
|
|
468
|
+
this.requirementCoverageGate(projectId, releaseId),
|
|
469
|
+
this.testPlanAnalysisGate(projectId, releaseId),
|
|
470
|
+
this.testExecutionGate(projectId, releaseId),
|
|
471
|
+
this.defectQualityGate(projectId, releaseId),
|
|
472
|
+
]);
|
|
473
|
+
|
|
474
|
+
const gates = {
|
|
475
|
+
requirementCoverage: results[0],
|
|
476
|
+
testPlanAnalysis: results[1],
|
|
477
|
+
testExecution: results[2],
|
|
478
|
+
defectQuality: results[3],
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Calculate overall status
|
|
482
|
+
const statuses = results.map(r => r.status);
|
|
483
|
+
let overallStatus;
|
|
484
|
+
|
|
485
|
+
if (statuses.includes("NO GO")) {
|
|
486
|
+
overallStatus = "NO GO";
|
|
487
|
+
} else if (statuses.includes("CONDITIONAL GO")) {
|
|
488
|
+
overallStatus = "CONDITIONAL GO";
|
|
489
|
+
} else if (statuses.includes("UNKNOWN")) {
|
|
490
|
+
overallStatus = "CONDITIONAL GO";
|
|
491
|
+
} else {
|
|
492
|
+
overallStatus = "GO";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const passedGates = statuses.filter(s => s === "GO").length;
|
|
496
|
+
const failedGates = statuses.filter(s => s === "NO GO").length;
|
|
497
|
+
const conditionalGates = statuses.filter(s => s === "CONDITIONAL GO" || s === "UNKNOWN").length;
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
projectId,
|
|
501
|
+
releaseId,
|
|
502
|
+
timestamp: new Date().toISOString(),
|
|
503
|
+
overallStatus,
|
|
504
|
+
summary: {
|
|
505
|
+
passed: passedGates,
|
|
506
|
+
failed: failedGates,
|
|
507
|
+
conditional: conditionalGates,
|
|
508
|
+
total: 4,
|
|
509
|
+
},
|
|
510
|
+
gates,
|
|
511
|
+
recommendation: this.getRecommendation(overallStatus, gates),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
getRecommendation(status, gates) {
|
|
516
|
+
const issues = [];
|
|
517
|
+
|
|
518
|
+
if (gates.requirementCoverage.status === "NO GO") {
|
|
519
|
+
issues.push(`• Map tests to ${gates.requirementCoverage.notCoveredRequirements} uncovered requirements`);
|
|
520
|
+
}
|
|
521
|
+
if (gates.testPlanAnalysis.status === "NO GO") {
|
|
522
|
+
issues.push(`• Plan and assign test executions (current: ${gates.testPlanAnalysis.overallPlanningPercentage}%)`);
|
|
523
|
+
}
|
|
524
|
+
if (gates.testExecution.status === "NO GO") {
|
|
525
|
+
issues.push(`• Execute ${gates.testExecution.incompleteTests} incomplete tests`);
|
|
526
|
+
}
|
|
527
|
+
if (gates.defectQuality.status === "NO GO") {
|
|
528
|
+
const blocker = gates.defectQuality.breakdown?.blocker?.count || 0;
|
|
529
|
+
const highRisk = gates.defectQuality.breakdown?.highRisk?.count || 0;
|
|
530
|
+
if (blocker > 0) issues.push(`• Resolve ${blocker} blocker defect(s)`);
|
|
531
|
+
if (highRisk > 10) issues.push(`• Reduce high-risk defects from ${highRisk} to ≤10`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (status === "GO") {
|
|
535
|
+
return "✅ All quality gates passed. Release is ready.";
|
|
536
|
+
} else if (status === "CONDITIONAL GO") {
|
|
537
|
+
return "⚠️ Proceed with caution. Minor issues exist but release is possible.";
|
|
538
|
+
} else {
|
|
539
|
+
return `🚨 Release blocked. Action items:\n${issues.join('\n')}`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
544
|
+
// TOOL 5: PROJECT HEALTH
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
async getProjectHealth(projectId, releaseId) {
|
|
548
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
|
|
549
|
+
|
|
550
|
+
// Requirements metrics
|
|
551
|
+
const reqData = summary.requirement || {};
|
|
552
|
+
const totalReqs = reqData.totalRequirementCount || 0;
|
|
553
|
+
const mappedReqs = reqData.mappedRequirementCount || 0;
|
|
554
|
+
const reqCoverage = totalReqs > 0 ? Math.round((mappedReqs / totalReqs) * 100 * 100) / 100 : 0;
|
|
555
|
+
|
|
556
|
+
// Test case metrics
|
|
557
|
+
const tcData = summary.testcase || {};
|
|
558
|
+
const totalTestcases = tcData.totalTestcaseCount || 0;
|
|
559
|
+
|
|
560
|
+
// Execution metrics
|
|
561
|
+
const execData = summary.execution || {};
|
|
562
|
+
const totalExecutions = execData.totalExecutionCount || 0;
|
|
563
|
+
const passedCount = execData.passedExecutionCount || 0;
|
|
564
|
+
const failedCount = execData.failedExecutionCount || 0;
|
|
565
|
+
const blockedCount = execData.blockedExecutionCount || 0;
|
|
566
|
+
const wipCount = execData.wipExecutionCount || 0;
|
|
567
|
+
const notExecutedCount = execData.unexecutedCount || 0;
|
|
568
|
+
|
|
569
|
+
const completedExecutions = passedCount + failedCount;
|
|
570
|
+
const executionRate = totalExecutions > 0 ? Math.round((completedExecutions / totalExecutions) * 100 * 100) / 100 : 0;
|
|
571
|
+
const passRate = completedExecutions > 0 ? Math.round((passedCount / completedExecutions) * 100 * 100) / 100 : 0;
|
|
572
|
+
|
|
573
|
+
// Defect metrics
|
|
574
|
+
const defectData = summary.defect || {};
|
|
575
|
+
const totalDefects = defectData.totalDefectCount || 0;
|
|
576
|
+
const openDefects = defectData.openDefectCount || totalDefects;
|
|
577
|
+
|
|
578
|
+
// Calculate health score (0-100)
|
|
579
|
+
const reqScore = reqCoverage;
|
|
580
|
+
const execScore = executionRate;
|
|
581
|
+
const passScore = passRate;
|
|
582
|
+
const defectPenalty = Math.min(30, openDefects * 3); // Penalty for open defects
|
|
583
|
+
|
|
584
|
+
const healthScore = Math.max(0, Math.round(
|
|
585
|
+
(reqScore * 0.25 + execScore * 0.35 + passScore * 0.40) - defectPenalty
|
|
586
|
+
));
|
|
587
|
+
|
|
588
|
+
let healthStatus;
|
|
589
|
+
if (healthScore >= 80) healthStatus = "HEALTHY";
|
|
590
|
+
else if (healthScore >= 60) healthStatus = "MODERATE";
|
|
591
|
+
else if (healthScore >= 40) healthStatus = "AT RISK";
|
|
592
|
+
else healthStatus = "CRITICAL";
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
tool: "Project Health",
|
|
596
|
+
projectId,
|
|
597
|
+
releaseId,
|
|
598
|
+
timestamp: new Date().toISOString(),
|
|
599
|
+
healthScore,
|
|
600
|
+
healthStatus,
|
|
601
|
+
metrics: {
|
|
602
|
+
requirements: {
|
|
603
|
+
total: totalReqs,
|
|
604
|
+
covered: mappedReqs,
|
|
605
|
+
uncovered: totalReqs - mappedReqs,
|
|
606
|
+
coveragePercentage: reqCoverage,
|
|
607
|
+
},
|
|
608
|
+
testCases: {
|
|
609
|
+
total: totalTestcases,
|
|
610
|
+
},
|
|
611
|
+
executions: {
|
|
612
|
+
total: totalExecutions,
|
|
613
|
+
passed: passedCount,
|
|
614
|
+
failed: failedCount,
|
|
615
|
+
blocked: blockedCount,
|
|
616
|
+
wip: wipCount,
|
|
617
|
+
notExecuted: notExecutedCount,
|
|
618
|
+
executionRate,
|
|
619
|
+
passRate,
|
|
620
|
+
},
|
|
621
|
+
defects: {
|
|
622
|
+
total: totalDefects,
|
|
623
|
+
open: openDefects,
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
recommendations: this.getHealthRecommendations(healthScore, reqCoverage, executionRate, passRate, openDefects),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
getHealthRecommendations(score, reqCoverage, execRate, passRate, openDefects) {
|
|
631
|
+
const recs = [];
|
|
632
|
+
if (reqCoverage < 70) recs.push(`Improve requirement coverage from ${reqCoverage}% to ≥70%`);
|
|
633
|
+
if (execRate < 90) recs.push(`Execute more tests - current rate is ${execRate}%`);
|
|
634
|
+
if (passRate < 80) recs.push(`Investigate failing tests - pass rate is ${passRate}%`);
|
|
635
|
+
if (openDefects > 5) recs.push(`Resolve ${openDefects} open defects`);
|
|
636
|
+
if (recs.length === 0) recs.push("Project is in good health!");
|
|
637
|
+
return recs;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
641
|
+
// TOOL 6: TEST COVERAGE
|
|
642
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
643
|
+
|
|
644
|
+
async getTestCoverage(projectId, releaseId) {
|
|
645
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
|
|
646
|
+
|
|
647
|
+
const reqData = summary.requirement || {};
|
|
648
|
+
const tcData = summary.testcase || {};
|
|
649
|
+
const execData = summary.execution || {};
|
|
650
|
+
|
|
651
|
+
const totalRequirements = reqData.totalRequirementCount || 0;
|
|
652
|
+
const coveredRequirements = reqData.mappedRequirementCount || 0;
|
|
653
|
+
const uncoveredRequirements = reqData.unmappedRequirementCount || 0;
|
|
654
|
+
|
|
655
|
+
const totalTestcases = tcData.totalTestcaseCount || 0;
|
|
656
|
+
const totalExecutions = execData.totalExecutionCount || 0;
|
|
657
|
+
|
|
658
|
+
// Calculate coverage metrics
|
|
659
|
+
const requirementCoverage = totalRequirements > 0
|
|
660
|
+
? Math.round((coveredRequirements / totalRequirements) * 100 * 100) / 100
|
|
661
|
+
: 0;
|
|
662
|
+
|
|
663
|
+
const testcaseToReqRatio = totalRequirements > 0
|
|
664
|
+
? Math.round((totalTestcases / totalRequirements) * 100) / 100
|
|
665
|
+
: 0;
|
|
666
|
+
|
|
667
|
+
const executionCoverage = totalTestcases > 0
|
|
668
|
+
? Math.round((totalExecutions / totalTestcases) * 100 * 100) / 100
|
|
669
|
+
: 0;
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
tool: "Test Coverage",
|
|
673
|
+
projectId,
|
|
674
|
+
releaseId,
|
|
675
|
+
timestamp: new Date().toISOString(),
|
|
676
|
+
summary: {
|
|
677
|
+
requirementCoveragePercentage: requirementCoverage,
|
|
678
|
+
executionCoveragePercentage: executionCoverage,
|
|
679
|
+
testcaseToRequirementRatio: testcaseToReqRatio,
|
|
680
|
+
},
|
|
681
|
+
details: {
|
|
682
|
+
requirements: {
|
|
683
|
+
total: totalRequirements,
|
|
684
|
+
covered: coveredRequirements,
|
|
685
|
+
uncovered: uncoveredRequirements,
|
|
686
|
+
},
|
|
687
|
+
testCases: {
|
|
688
|
+
total: totalTestcases,
|
|
689
|
+
avgPerRequirement: testcaseToReqRatio,
|
|
690
|
+
},
|
|
691
|
+
executions: {
|
|
692
|
+
total: totalExecutions,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
status: requirementCoverage >= 70 ? "ADEQUATE" : requirementCoverage >= 50 ? "PARTIAL" : "INSUFFICIENT",
|
|
696
|
+
message: requirementCoverage >= 70
|
|
697
|
+
? `Good coverage at ${requirementCoverage}%`
|
|
698
|
+
: `Coverage is ${requirementCoverage}%. ${uncoveredRequirements} requirements need test cases.`,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
703
|
+
// TOOL 7: FAILED TESTS
|
|
704
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
705
|
+
|
|
706
|
+
async getFailedTests(projectId, releaseId, options = {}) {
|
|
707
|
+
const { limit = 50, includeSteps = false } = options;
|
|
708
|
+
|
|
709
|
+
// Get all executions
|
|
710
|
+
const executionData = await this.GET("/execution", {
|
|
711
|
+
releaseid: releaseId,
|
|
712
|
+
offset: 0,
|
|
713
|
+
pagesize: 10000,
|
|
714
|
+
includeanyoneuser: true,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const executions = executionData.results || executionData || [];
|
|
718
|
+
|
|
719
|
+
// Filter to failed tests (status = 2)
|
|
720
|
+
const failedExecutions = executions.filter(exec => {
|
|
721
|
+
const status = exec.lastTestResult?.executionStatus || exec.status || exec.executionStatus || 0;
|
|
722
|
+
return Number(status) === 2;
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Build failed test list
|
|
726
|
+
const failedTests = failedExecutions.slice(0, limit).map(exec => {
|
|
727
|
+
const tc = exec.tcrTreeTestcase?.testcase || {};
|
|
728
|
+
return {
|
|
729
|
+
executionId: exec.id,
|
|
730
|
+
testcaseId: tc.id || exec.testcaseId,
|
|
731
|
+
testcaseName: tc.name || exec.name || "Unknown",
|
|
732
|
+
testcaseKey: tc.testcaseKey || tc.alternateId,
|
|
733
|
+
lastExecutedOn: exec.lastTestResult?.executedOn || exec.executedOn,
|
|
734
|
+
executedBy: exec.lastTestResult?.testerName || exec.testerName,
|
|
735
|
+
cycleName: exec.tcrTreeTestcase?.tcrCatalogTreeId?.name || exec.cycleName,
|
|
736
|
+
cyclePhase: exec.cyclePhase?.name,
|
|
737
|
+
defects: exec.lastTestResult?.defects || [],
|
|
738
|
+
};
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Summary stats
|
|
742
|
+
const totalExecutions = executions.length;
|
|
743
|
+
const failedCount = failedExecutions.length;
|
|
744
|
+
const passedCount = executions.filter(e => Number(e.lastTestResult?.executionStatus || e.status || 0) === 1).length;
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
tool: "Failed Tests",
|
|
748
|
+
projectId,
|
|
749
|
+
releaseId,
|
|
750
|
+
timestamp: new Date().toISOString(),
|
|
751
|
+
summary: {
|
|
752
|
+
totalExecutions,
|
|
753
|
+
failedCount,
|
|
754
|
+
passedCount,
|
|
755
|
+
failureRate: totalExecutions > 0 ? Math.round((failedCount / totalExecutions) * 100 * 100) / 100 : 0,
|
|
756
|
+
},
|
|
757
|
+
failedTests,
|
|
758
|
+
message: failedCount === 0
|
|
759
|
+
? "No failed tests found!"
|
|
760
|
+
: `${failedCount} test(s) failed out of ${totalExecutions} (${Math.round((failedCount / totalExecutions) * 100)}%)`,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
765
|
+
// TOOL 8: REQUIREMENT COVERAGE (Detailed)
|
|
766
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
767
|
+
|
|
768
|
+
async getRequirementCoverage(projectId, releaseId) {
|
|
769
|
+
// Get release summary
|
|
770
|
+
const summary = await this.GET(`/summary/release/${releaseId}`, {});
|
|
771
|
+
|
|
772
|
+
const reqData = summary.requirement || {};
|
|
773
|
+
const totalRequirements = reqData.totalRequirementCount || 0;
|
|
774
|
+
const coveredRequirements = reqData.mappedRequirementCount || 0;
|
|
775
|
+
const uncoveredRequirements = reqData.unmappedRequirementCount || 0;
|
|
776
|
+
|
|
777
|
+
// Try to get requirements list
|
|
778
|
+
let requirements = [];
|
|
779
|
+
try {
|
|
780
|
+
const reqList = await this.GET("/requirement", {
|
|
781
|
+
projectId: projectId,
|
|
782
|
+
releaseId: releaseId,
|
|
783
|
+
offset: 0,
|
|
784
|
+
maxRecords: 500,
|
|
785
|
+
});
|
|
786
|
+
requirements = reqList.results || reqList || [];
|
|
787
|
+
} catch (e) {
|
|
788
|
+
// Requirement list may not be available
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Categorize requirements
|
|
792
|
+
const covered = [];
|
|
793
|
+
const uncovered = [];
|
|
794
|
+
|
|
795
|
+
for (const req of requirements) {
|
|
796
|
+
const reqInfo = {
|
|
797
|
+
id: req.id,
|
|
798
|
+
key: req.externalId || req.alternateId || req.requirementKey,
|
|
799
|
+
name: req.name,
|
|
800
|
+
priority: req.priority,
|
|
801
|
+
testcaseCount: req.testcaseCount || 0,
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
if (req.testcaseCount > 0) {
|
|
805
|
+
covered.push(reqInfo);
|
|
806
|
+
} else {
|
|
807
|
+
uncovered.push(reqInfo);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const coveragePercentage = totalRequirements > 0
|
|
812
|
+
? Math.round((coveredRequirements / totalRequirements) * 100 * 100) / 100
|
|
813
|
+
: 0;
|
|
814
|
+
|
|
815
|
+
return {
|
|
816
|
+
tool: "Requirement Coverage",
|
|
817
|
+
projectId,
|
|
818
|
+
releaseId,
|
|
819
|
+
timestamp: new Date().toISOString(),
|
|
820
|
+
summary: {
|
|
821
|
+
total: totalRequirements,
|
|
822
|
+
covered: coveredRequirements,
|
|
823
|
+
uncovered: uncoveredRequirements,
|
|
824
|
+
coveragePercentage,
|
|
825
|
+
},
|
|
826
|
+
status: coveragePercentage >= 70 ? "GO" : "NO GO",
|
|
827
|
+
message: coveragePercentage >= 70
|
|
828
|
+
? `Coverage is ${coveragePercentage}% - meets threshold`
|
|
829
|
+
: `Coverage is ${coveragePercentage}% - ${uncoveredRequirements} requirements need test coverage`,
|
|
830
|
+
details: {
|
|
831
|
+
coveredRequirements: covered.slice(0, 20),
|
|
832
|
+
uncoveredRequirements: uncovered.slice(0, 20),
|
|
833
|
+
note: requirements.length > 40 ? "Showing first 20 of each category" : undefined,
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
839
|
+
// TOOL 9: TEST CASE TRENDS
|
|
840
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
841
|
+
|
|
842
|
+
async getTestCaseTrends(projectId, releaseId, options = {}) {
|
|
843
|
+
const { days = 30 } = options;
|
|
844
|
+
|
|
845
|
+
// Get executions
|
|
846
|
+
const executionData = await this.GET("/execution", {
|
|
847
|
+
releaseid: releaseId,
|
|
848
|
+
offset: 0,
|
|
849
|
+
pagesize: 10000,
|
|
850
|
+
includeanyoneuser: true,
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const executions = executionData.results || executionData || [];
|
|
854
|
+
|
|
855
|
+
// Group executions by date
|
|
856
|
+
const trendsByDate = {};
|
|
857
|
+
const now = new Date();
|
|
858
|
+
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
859
|
+
|
|
860
|
+
for (const exec of executions) {
|
|
861
|
+
const executedOn = exec.lastTestResult?.executedOn || exec.executedOn;
|
|
862
|
+
if (!executedOn) continue;
|
|
863
|
+
|
|
864
|
+
const execDate = new Date(executedOn);
|
|
865
|
+
if (execDate < startDate) continue;
|
|
866
|
+
|
|
867
|
+
const dateKey = execDate.toISOString().split('T')[0];
|
|
868
|
+
|
|
869
|
+
if (!trendsByDate[dateKey]) {
|
|
870
|
+
trendsByDate[dateKey] = { passed: 0, failed: 0, blocked: 0, wip: 0, total: 0 };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const status = exec.lastTestResult?.executionStatus || exec.status || 0;
|
|
874
|
+
trendsByDate[dateKey].total++;
|
|
875
|
+
|
|
876
|
+
switch (Number(status)) {
|
|
877
|
+
case 1: trendsByDate[dateKey].passed++; break;
|
|
878
|
+
case 2: trendsByDate[dateKey].failed++; break;
|
|
879
|
+
case 3: trendsByDate[dateKey].wip++; break;
|
|
880
|
+
case 4: trendsByDate[dateKey].blocked++; break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Convert to sorted array
|
|
885
|
+
const trends = Object.entries(trendsByDate)
|
|
886
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
887
|
+
.map(([date, data]) => ({ date, ...data }));
|
|
888
|
+
|
|
889
|
+
// Calculate totals
|
|
890
|
+
const totals = trends.reduce((acc, day) => ({
|
|
891
|
+
passed: acc.passed + day.passed,
|
|
892
|
+
failed: acc.failed + day.failed,
|
|
893
|
+
blocked: acc.blocked + day.blocked,
|
|
894
|
+
wip: acc.wip + day.wip,
|
|
895
|
+
total: acc.total + day.total,
|
|
896
|
+
}), { passed: 0, failed: 0, blocked: 0, wip: 0, total: 0 });
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
tool: "Test Case Trends",
|
|
900
|
+
projectId,
|
|
901
|
+
releaseId,
|
|
902
|
+
timestamp: new Date().toISOString(),
|
|
903
|
+
period: {
|
|
904
|
+
days,
|
|
905
|
+
from: startDate.toISOString().split('T')[0],
|
|
906
|
+
to: now.toISOString().split('T')[0],
|
|
907
|
+
},
|
|
908
|
+
summary: {
|
|
909
|
+
totalExecutionsInPeriod: totals.total,
|
|
910
|
+
passed: totals.passed,
|
|
911
|
+
failed: totals.failed,
|
|
912
|
+
blocked: totals.blocked,
|
|
913
|
+
wip: totals.wip,
|
|
914
|
+
avgExecutionsPerDay: trends.length > 0 ? Math.round(totals.total / trends.length) : 0,
|
|
915
|
+
},
|
|
916
|
+
dailyTrends: trends,
|
|
917
|
+
insight: totals.failed > totals.passed
|
|
918
|
+
? "⚠️ More failures than passes - investigate test stability"
|
|
919
|
+
: totals.total === 0
|
|
920
|
+
? "No test activity in this period"
|
|
921
|
+
: `✅ ${Math.round((totals.passed / totals.total) * 100)}% pass rate over ${trends.length} active days`,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
926
|
+
// TOOL 10: SEARCH TEST CASES
|
|
927
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
928
|
+
|
|
929
|
+
async searchTestCases(projectId, releaseId, options = {}) {
|
|
930
|
+
const { query = '', status, priority, limit = 50 } = options;
|
|
931
|
+
|
|
932
|
+
// Get test cases
|
|
933
|
+
const params = {
|
|
934
|
+
projectId: projectId,
|
|
935
|
+
releaseId: releaseId,
|
|
936
|
+
offset: 0,
|
|
937
|
+
maxRecords: 500,
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
if (query) params.word = query;
|
|
941
|
+
|
|
942
|
+
let testcases = [];
|
|
943
|
+
try {
|
|
944
|
+
const tcData = await this.GET("/testcase/tree", params);
|
|
945
|
+
testcases = tcData.results || tcData || [];
|
|
946
|
+
|
|
947
|
+
// Flatten tree structure if needed
|
|
948
|
+
if (!Array.isArray(testcases)) {
|
|
949
|
+
testcases = this.flattenTestcaseTree(tcData);
|
|
950
|
+
}
|
|
951
|
+
} catch (e) {
|
|
952
|
+
// Try alternative endpoint
|
|
953
|
+
try {
|
|
954
|
+
const tcData = await this.GET("/testcase", params);
|
|
955
|
+
testcases = tcData.results || tcData || [];
|
|
956
|
+
} catch (e2) {
|
|
957
|
+
return {
|
|
958
|
+
tool: "Search Test Cases",
|
|
959
|
+
projectId,
|
|
960
|
+
releaseId,
|
|
961
|
+
error: "Unable to fetch test cases",
|
|
962
|
+
results: [],
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Filter results
|
|
968
|
+
let filtered = testcases;
|
|
969
|
+
|
|
970
|
+
if (query) {
|
|
971
|
+
const q = query.toLowerCase();
|
|
972
|
+
filtered = filtered.filter(tc =>
|
|
973
|
+
(tc.name || '').toLowerCase().includes(q) ||
|
|
974
|
+
(tc.testcaseKey || tc.alternateId || '').toLowerCase().includes(q) ||
|
|
975
|
+
(tc.description || '').toLowerCase().includes(q)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (status) {
|
|
980
|
+
filtered = filtered.filter(tc =>
|
|
981
|
+
(tc.status || '').toLowerCase() === status.toLowerCase()
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (priority) {
|
|
986
|
+
filtered = filtered.filter(tc =>
|
|
987
|
+
(tc.priority || '').toLowerCase() === priority.toLowerCase()
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Map to clean output
|
|
992
|
+
const results = filtered.slice(0, limit).map(tc => ({
|
|
993
|
+
id: tc.id,
|
|
994
|
+
key: tc.testcaseKey || tc.alternateId,
|
|
995
|
+
name: tc.name,
|
|
996
|
+
status: tc.status,
|
|
997
|
+
priority: tc.priority,
|
|
998
|
+
automated: tc.automated || tc.isAutomated || false,
|
|
999
|
+
folder: tc.folderPath || tc.tcrCatalogTreeId?.name,
|
|
1000
|
+
estimatedTime: tc.estimatedTime,
|
|
1001
|
+
tags: tc.tags || [],
|
|
1002
|
+
}));
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
tool: "Search Test Cases",
|
|
1006
|
+
projectId,
|
|
1007
|
+
releaseId,
|
|
1008
|
+
timestamp: new Date().toISOString(),
|
|
1009
|
+
query: { text: query, status, priority },
|
|
1010
|
+
totalMatches: filtered.length,
|
|
1011
|
+
returned: results.length,
|
|
1012
|
+
results,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
flattenTestcaseTree(node, results = []) {
|
|
1017
|
+
if (node.testcase) results.push(node.testcase);
|
|
1018
|
+
if (node.testcases) results.push(...node.testcases);
|
|
1019
|
+
if (node.children) {
|
|
1020
|
+
for (const child of node.children) {
|
|
1021
|
+
this.flattenTestcaseTree(child, results);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return results;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1028
|
+
// TOOL 11: USER ACTIVITY
|
|
1029
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1030
|
+
|
|
1031
|
+
async getUserActivity(projectId, releaseId, options = {}) {
|
|
1032
|
+
const { days = 30 } = options;
|
|
1033
|
+
|
|
1034
|
+
// Get executions to analyze user activity
|
|
1035
|
+
const executionData = await this.GET("/execution", {
|
|
1036
|
+
releaseid: releaseId,
|
|
1037
|
+
offset: 0,
|
|
1038
|
+
pagesize: 10000,
|
|
1039
|
+
includeanyoneuser: true,
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const executions = executionData.results || executionData || [];
|
|
1043
|
+
|
|
1044
|
+
// Aggregate by user
|
|
1045
|
+
const userStats = {};
|
|
1046
|
+
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
1047
|
+
|
|
1048
|
+
for (const exec of executions) {
|
|
1049
|
+
const testerName = exec.lastTestResult?.testerName || exec.testerName || "Unassigned";
|
|
1050
|
+
const testerId = exec.testerId || exec.lastTestResult?.testerId;
|
|
1051
|
+
const executedOn = exec.lastTestResult?.executedOn || exec.executedOn;
|
|
1052
|
+
|
|
1053
|
+
if (!userStats[testerName]) {
|
|
1054
|
+
userStats[testerName] = {
|
|
1055
|
+
userId: testerId,
|
|
1056
|
+
name: testerName,
|
|
1057
|
+
assigned: 0,
|
|
1058
|
+
executed: 0,
|
|
1059
|
+
passed: 0,
|
|
1060
|
+
failed: 0,
|
|
1061
|
+
blocked: 0,
|
|
1062
|
+
lastActivity: null,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
userStats[testerName].assigned++;
|
|
1067
|
+
|
|
1068
|
+
const status = exec.lastTestResult?.executionStatus || exec.status || 0;
|
|
1069
|
+
if (Number(status) > 0) {
|
|
1070
|
+
userStats[testerName].executed++;
|
|
1071
|
+
|
|
1072
|
+
switch (Number(status)) {
|
|
1073
|
+
case 1: userStats[testerName].passed++; break;
|
|
1074
|
+
case 2: userStats[testerName].failed++; break;
|
|
1075
|
+
case 4: userStats[testerName].blocked++; break;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (executedOn) {
|
|
1079
|
+
const execDate = new Date(executedOn);
|
|
1080
|
+
if (!userStats[testerName].lastActivity || execDate > new Date(userStats[testerName].lastActivity)) {
|
|
1081
|
+
userStats[testerName].lastActivity = executedOn;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Convert to array and calculate metrics
|
|
1088
|
+
const users = Object.values(userStats)
|
|
1089
|
+
.map(user => ({
|
|
1090
|
+
...user,
|
|
1091
|
+
completionRate: user.assigned > 0 ? Math.round((user.executed / user.assigned) * 100) : 0,
|
|
1092
|
+
passRate: user.executed > 0 ? Math.round((user.passed / user.executed) * 100) : 0,
|
|
1093
|
+
}))
|
|
1094
|
+
.sort((a, b) => b.executed - a.executed);
|
|
1095
|
+
|
|
1096
|
+
// Team summary
|
|
1097
|
+
const teamSummary = users.reduce((acc, user) => ({
|
|
1098
|
+
totalAssigned: acc.totalAssigned + user.assigned,
|
|
1099
|
+
totalExecuted: acc.totalExecuted + user.executed,
|
|
1100
|
+
totalPassed: acc.totalPassed + user.passed,
|
|
1101
|
+
totalFailed: acc.totalFailed + user.failed,
|
|
1102
|
+
}), { totalAssigned: 0, totalExecuted: 0, totalPassed: 0, totalFailed: 0 });
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
tool: "User Activity",
|
|
1106
|
+
projectId,
|
|
1107
|
+
releaseId,
|
|
1108
|
+
timestamp: new Date().toISOString(),
|
|
1109
|
+
period: { days },
|
|
1110
|
+
teamSummary: {
|
|
1111
|
+
...teamSummary,
|
|
1112
|
+
activeUsers: users.filter(u => u.executed > 0).length,
|
|
1113
|
+
totalUsers: users.length,
|
|
1114
|
+
teamCompletionRate: teamSummary.totalAssigned > 0
|
|
1115
|
+
? Math.round((teamSummary.totalExecuted / teamSummary.totalAssigned) * 100)
|
|
1116
|
+
: 0,
|
|
1117
|
+
teamPassRate: teamSummary.totalExecuted > 0
|
|
1118
|
+
? Math.round((teamSummary.totalPassed / teamSummary.totalExecuted) * 100)
|
|
1119
|
+
: 0,
|
|
1120
|
+
},
|
|
1121
|
+
users,
|
|
1122
|
+
topPerformers: users.filter(u => u.executed > 0).slice(0, 5),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Export with both names for backward compatibility
|
|
1128
|
+
export { QualityGates as ZephyrTools };
|
|
1129
|
+
export default QualityGates;
|