zephyr-enterprise-tools 1.0.0 ā 1.0.2
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 +109 -2
- package/cli.js +80 -1
- package/package.json +1 -1
- package/zephyr-enterprise-tools.js +202 -64
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ Comprehensive tools for Zephyr Enterprise - Release Readiness, Project Health, T
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
# Install from npm
|
|
35
|
-
npm install -g zephyr-
|
|
35
|
+
npm install -g zephyr-enterprise-tools
|
|
36
36
|
|
|
37
37
|
# Or install locally
|
|
38
|
-
npm install zephyr-
|
|
38
|
+
npm install zephyr-enterprise-tools
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
---
|
|
@@ -55,6 +55,113 @@ export ZEPHYR_TOKEN="your-token"
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
+
## š¤ MCP Integration
|
|
59
|
+
|
|
60
|
+
Use zephyr-enterprise-tools as an MCP (Model Context Protocol) server with your AI assistant.
|
|
61
|
+
|
|
62
|
+
### VS Code with GitHub Copilot
|
|
63
|
+
|
|
64
|
+
Create or edit `.vscode/mcp.json` in your workspace:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"servers": {
|
|
69
|
+
"zephyr-enterprise": {
|
|
70
|
+
"type": "stdio",
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": [
|
|
73
|
+
"-y",
|
|
74
|
+
"zephyr-enterprise-tools@latest"
|
|
75
|
+
],
|
|
76
|
+
"env": {
|
|
77
|
+
"ZEPHYR_BASE_URL": "${input:zephyr_base_url}",
|
|
78
|
+
"ZEPHYR_USERNAME": "${input:zephyr_username}",
|
|
79
|
+
"ZEPHYR_PASSWORD": "${input:zephyr_password}",
|
|
80
|
+
"ZEPHYR_TOKEN": "${input:zephyr_token}"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"inputs": [
|
|
85
|
+
{
|
|
86
|
+
"id": "zephyr_base_url",
|
|
87
|
+
"type": "promptString",
|
|
88
|
+
"description": "Zephyr Enterprise API Base URL (e.g. https://your-zephyr.com/flex/services/rest/latest)",
|
|
89
|
+
"password": false
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "zephyr_username",
|
|
93
|
+
"type": "promptString",
|
|
94
|
+
"description": "Zephyr Enterprise Username (for Basic Auth)",
|
|
95
|
+
"password": false
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "zephyr_password",
|
|
99
|
+
"type": "promptString",
|
|
100
|
+
"description": "Zephyr Enterprise Password (for Basic Auth)",
|
|
101
|
+
"password": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "zephyr_token",
|
|
105
|
+
"type": "promptString",
|
|
106
|
+
"description": "Zephyr Enterprise Bearer Token (alternative to username/password)",
|
|
107
|
+
"password": true
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Cursor
|
|
114
|
+
|
|
115
|
+
Add to your `mcp.json` configuration:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"zephyr-enterprise": {
|
|
121
|
+
"command": "npx",
|
|
122
|
+
"args": [
|
|
123
|
+
"-y",
|
|
124
|
+
"zephyr-enterprise-tools@latest"
|
|
125
|
+
],
|
|
126
|
+
"env": {
|
|
127
|
+
"ZEPHYR_BASE_URL": "https://your-zephyr.com/flex/services/rest/latest",
|
|
128
|
+
"ZEPHYR_USERNAME": "your-username",
|
|
129
|
+
"ZEPHYR_PASSWORD": "your-password",
|
|
130
|
+
"ZEPHYR_TOKEN": "your-token"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Claude Desktop
|
|
138
|
+
|
|
139
|
+
Edit your `claude_desktop_config.json` file:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"mcpServers": {
|
|
144
|
+
"zephyr-enterprise": {
|
|
145
|
+
"command": "npx",
|
|
146
|
+
"args": [
|
|
147
|
+
"-y",
|
|
148
|
+
"zephyr-enterprise-tools@latest"
|
|
149
|
+
],
|
|
150
|
+
"env": {
|
|
151
|
+
"ZEPHYR_BASE_URL": "https://your-zephyr.com/flex/services/rest/latest",
|
|
152
|
+
"ZEPHYR_USERNAME": "your-username",
|
|
153
|
+
"ZEPHYR_PASSWORD": "your-password",
|
|
154
|
+
"ZEPHYR_TOKEN": "your-token"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> **Note:** Use either `ZEPHYR_USERNAME` + `ZEPHYR_PASSWORD` for Basic Auth, or `ZEPHYR_TOKEN` for Bearer token authentication.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
58
165
|
## š„ļø CLI Usage
|
|
59
166
|
|
|
60
167
|
```bash
|
package/cli.js
CHANGED
|
@@ -251,6 +251,23 @@ function formatGenericResult(result) {
|
|
|
251
251
|
console.log(`\nHealth Score: ${icon} ${result.healthScore}/100 (${result.healthStatus})`);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// Project Summary from v3 API (new section)
|
|
255
|
+
if (result.projectSummary) {
|
|
256
|
+
const ps = result.projectSummary;
|
|
257
|
+
console.log('\nš PROJECT SUMMARY (from v3 API):');
|
|
258
|
+
console.log(` āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā`);
|
|
259
|
+
console.log(` ā Total Releases ā ${String(ps.totalReleaseCount).padStart(12)} ā`);
|
|
260
|
+
console.log(` ā Visible Releases ā ${String(ps.visibleReleaseCount).padStart(12)} ā`);
|
|
261
|
+
console.log(` ā Total Test Cases ā ${String(ps.testcaseCount).padStart(12)} ā`);
|
|
262
|
+
console.log(` ā Total Executions ā ${String(ps.executionCount).padStart(12)} ā`);
|
|
263
|
+
console.log(` ā Team Members ā ${String(ps.teamCount).padStart(12)} ā`);
|
|
264
|
+
console.log(` āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāā¤`);
|
|
265
|
+
console.log(` ā Automated Test Cases ā ${String(ps.automatedTestCaseCount).padStart(12)} ā`);
|
|
266
|
+
console.log(` ā Manual Test Cases ā ${String(ps.manualTestCaseCount).padStart(12)} ā`);
|
|
267
|
+
console.log(` ā Automation Percentage ā ${String(ps.automationPercentage + '%').padStart(12)} ā`);
|
|
268
|
+
console.log(` āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā`);
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
// Summary section
|
|
255
272
|
if (result.summary) {
|
|
256
273
|
console.log('\nš SUMMARY:');
|
|
@@ -271,6 +288,40 @@ function formatGenericResult(result) {
|
|
|
271
288
|
}
|
|
272
289
|
}
|
|
273
290
|
|
|
291
|
+
// Project info section
|
|
292
|
+
if (result.project) {
|
|
293
|
+
const p = result.project;
|
|
294
|
+
console.log(`\nš¢ PROJECT INFO:`);
|
|
295
|
+
console.log(` Name: ${p.name}`);
|
|
296
|
+
if (p.description) console.log(` Description: ${p.description}`);
|
|
297
|
+
if (p.startDate) console.log(` Start Date: ${p.startDate}`);
|
|
298
|
+
if (p.endDate) console.log(` End Date: ${p.endDate}`);
|
|
299
|
+
if (p.teamCount) console.log(` Team Count: ${p.teamCount}`);
|
|
300
|
+
|
|
301
|
+
// Members
|
|
302
|
+
console.log(`\nš„ PROJECT MEMBERS (${p.totalMembers}):`);
|
|
303
|
+
if (p.members && p.members.length > 0) {
|
|
304
|
+
for (const member of p.members.slice(0, 10)) {
|
|
305
|
+
console.log(` ⢠${member.name} (${member.role})`);
|
|
306
|
+
}
|
|
307
|
+
if (p.totalMembers > 10) console.log(` ... and ${p.totalMembers - 10} more`);
|
|
308
|
+
} else {
|
|
309
|
+
console.log(` No members assigned to this project`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Releases
|
|
313
|
+
console.log(`\nš¦ RELEASES (${p.totalReleases}${p.visibleReleases ? `, ${p.visibleReleases} visible` : ''}):`);
|
|
314
|
+
if (p.releases && p.releases.length > 0) {
|
|
315
|
+
for (const rel of p.releases) {
|
|
316
|
+
const current = rel.isCurrent ? ' ā¬
CURRENT' : '';
|
|
317
|
+
const dates = rel.startDate && rel.endDate ? ` (${rel.startDate} - ${rel.endDate})` : '';
|
|
318
|
+
console.log(` ⢠${rel.name} [ID: ${rel.id}]${dates}${current}`);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
console.log(` No releases found`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
274
325
|
// Team summary
|
|
275
326
|
if (result.teamSummary) {
|
|
276
327
|
console.log('\nš„ TEAM SUMMARY:');
|
|
@@ -279,7 +330,35 @@ function formatGenericResult(result) {
|
|
|
279
330
|
}
|
|
280
331
|
}
|
|
281
332
|
|
|
282
|
-
//
|
|
333
|
+
// Assigned To list (for User Activity)
|
|
334
|
+
if (result.assignedTo && result.assignedTo.length > 0) {
|
|
335
|
+
console.log('\nš ASSIGNED TO:');
|
|
336
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā');
|
|
337
|
+
console.log(' ā User ā Assign ā Exec ā Pass% ā Comp% ā');
|
|
338
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¤');
|
|
339
|
+
for (const user of result.assignedTo.slice(0, 10)) {
|
|
340
|
+
const name = (user.name || 'Unknown').substring(0, 21).padEnd(23);
|
|
341
|
+
console.log(` ā ${name} ā ${String(user.assigned).padEnd(6)} ā ${String(user.executed).padEnd(6)} ā ${String(user.passRate + '%').padEnd(6)} ā ${String(user.completionRate + '%').padEnd(6)} ā`);
|
|
342
|
+
}
|
|
343
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāā');
|
|
344
|
+
if (result.assignedTo.length > 10) console.log(` ... and ${result.assignedTo.length - 10} more`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Executed By list (for User Activity)
|
|
348
|
+
if (result.executedBy && result.executedBy.length > 0) {
|
|
349
|
+
console.log('\nš EXECUTED BY:');
|
|
350
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāāā');
|
|
351
|
+
console.log(' ā User ā Exec ā Passed ā Failed ā Pass% ā');
|
|
352
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāāā¤');
|
|
353
|
+
for (const user of result.executedBy.slice(0, 10)) {
|
|
354
|
+
const name = (user.name || 'Unknown').substring(0, 21).padEnd(23);
|
|
355
|
+
console.log(` ā ${name} ā ${String(user.executed).padEnd(6)} ā ${String(user.passed).padEnd(6)} ā ${String(user.failed).padEnd(6)} ā ${String(user.passRate + '%').padEnd(7)} ā`);
|
|
356
|
+
}
|
|
357
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāāā');
|
|
358
|
+
if (result.executedBy.length > 10) console.log(` ... and ${result.executedBy.length - 10} more`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Users list (legacy/other tools)
|
|
283
362
|
if (result.users && result.users.length > 0) {
|
|
284
363
|
console.log('\nš¤ USERS:');
|
|
285
364
|
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā');
|
package/package.json
CHANGED
|
@@ -95,6 +95,24 @@ export class QualityGates {
|
|
|
95
95
|
return this.request("GET", path, params);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
async GETv3(path, params = {}) {
|
|
99
|
+
// Build v3 URL by replacing /latest with /v3 in baseUrl
|
|
100
|
+
const v3BaseUrl = this.baseUrl.replace('/latest', '/v3');
|
|
101
|
+
const url = new URL(`${v3BaseUrl}${path}`);
|
|
102
|
+
for (const [k, v] of Object.entries(params)) {
|
|
103
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
104
|
+
}
|
|
105
|
+
const res = await fetch(url.toString(), {
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", ...this.authHeader() },
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const text = await res.text().catch(() => "");
|
|
111
|
+
throw new Error(`Zephyr v3 API ${res.status}: ${text}`);
|
|
112
|
+
}
|
|
113
|
+
return res.json();
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
async PUT(path, params, body) {
|
|
99
117
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
100
118
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -545,44 +563,78 @@ export class QualityGates {
|
|
|
545
563
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
546
564
|
|
|
547
565
|
async getProjectHealth(projectId, releaseId) {
|
|
548
|
-
|
|
566
|
+
// Fetch project details using v3 summary API, project info, and releases in parallel
|
|
567
|
+
const [projectSummary, project, allReleases, releaseSummary] = await Promise.all([
|
|
568
|
+
this.GETv3(`/summary/project/${projectId}`, { isLite: false }),
|
|
569
|
+
this.GET(`/project/${projectId}`),
|
|
570
|
+
this.GET('/release', { projectid: projectId, isaliasallowed: false }).catch(() => []),
|
|
571
|
+
this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false }),
|
|
572
|
+
]);
|
|
549
573
|
|
|
550
|
-
//
|
|
551
|
-
const
|
|
574
|
+
// Filter releases for this project (exclude "Project Test Repository")
|
|
575
|
+
const projectReleases = (Array.isArray(allReleases) ? allReleases : [])
|
|
576
|
+
.filter(r => r.projectId === projectId && !r.projectRelease);
|
|
577
|
+
|
|
578
|
+
// Get project members and fetch their details
|
|
579
|
+
const projectMembers = project.members || [];
|
|
580
|
+
const memberDetails = await Promise.all(
|
|
581
|
+
projectMembers.slice(0, 20).map(async (m) => {
|
|
582
|
+
try {
|
|
583
|
+
const user = await this.GET(`/user/${m.userId}`);
|
|
584
|
+
return {
|
|
585
|
+
userId: m.userId,
|
|
586
|
+
name: user.fullName || `${user.firstName} ${user.lastName}`.trim(),
|
|
587
|
+
username: user.username,
|
|
588
|
+
role: m.role?.name || 'Unknown',
|
|
589
|
+
};
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return {
|
|
592
|
+
userId: m.userId,
|
|
593
|
+
name: `User ${m.userId}`,
|
|
594
|
+
role: m.role?.name || 'Unknown',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
// Project-level metrics from v3 API
|
|
601
|
+
const totalTestcases = projectSummary.testcaseCount || 0;
|
|
602
|
+
const totalExecutions = projectSummary.executionCount || 0;
|
|
603
|
+
const automatedTestCases = projectSummary.automatedTestCaseCount || 0;
|
|
604
|
+
const manualTestCases = projectSummary.manualTestCaseCount || 0;
|
|
605
|
+
const automationPercentage = projectSummary.automationPercentage || 0;
|
|
606
|
+
const teamCount = projectSummary.teamCount || 0;
|
|
607
|
+
const totalReleaseCount = projectSummary.totalReleaseCount || 0;
|
|
608
|
+
const visibleReleaseCount = projectSummary.visibleReleaseCount || 0;
|
|
609
|
+
|
|
610
|
+
// Requirements metrics from release summary
|
|
611
|
+
const reqData = releaseSummary.requirement || {};
|
|
552
612
|
const totalReqs = reqData.totalRequirementCount || 0;
|
|
553
613
|
const mappedReqs = reqData.mappedRequirementCount || 0;
|
|
554
614
|
const reqCoverage = totalReqs > 0 ? Math.round((mappedReqs / totalReqs) * 100 * 100) / 100 : 0;
|
|
555
615
|
|
|
556
|
-
//
|
|
557
|
-
const
|
|
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 || {};
|
|
616
|
+
// Defect metrics from release summary
|
|
617
|
+
const defectData = releaseSummary.defect || {};
|
|
575
618
|
const totalDefects = defectData.totalDefectCount || 0;
|
|
576
|
-
|
|
619
|
+
let openDefects = 0;
|
|
620
|
+
if (defectData.statuses) {
|
|
621
|
+
for (const s of defectData.statuses) {
|
|
622
|
+
const statusName = (s.status || '').toLowerCase();
|
|
623
|
+
if (!statusName.includes('done') && !statusName.includes('closed') && !statusName.includes('resolved')) {
|
|
624
|
+
openDefects += s.defectCount || 0;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
openDefects = totalDefects;
|
|
629
|
+
}
|
|
577
630
|
|
|
578
631
|
// Calculate health score (0-100)
|
|
632
|
+
const automationScore = automationPercentage;
|
|
579
633
|
const reqScore = reqCoverage;
|
|
580
|
-
const
|
|
581
|
-
const passScore = passRate;
|
|
582
|
-
const defectPenalty = Math.min(30, openDefects * 3); // Penalty for open defects
|
|
634
|
+
const defectPenalty = Math.min(20, openDefects * 2);
|
|
583
635
|
|
|
584
636
|
const healthScore = Math.max(0, Math.round(
|
|
585
|
-
(
|
|
637
|
+
(automationScore * 0.35 + reqScore * 0.35 + (100 - defectPenalty * 2) * 0.30)
|
|
586
638
|
));
|
|
587
639
|
|
|
588
640
|
let healthStatus;
|
|
@@ -598,6 +650,16 @@ export class QualityGates {
|
|
|
598
650
|
timestamp: new Date().toISOString(),
|
|
599
651
|
healthScore,
|
|
600
652
|
healthStatus,
|
|
653
|
+
projectSummary: {
|
|
654
|
+
totalReleaseCount,
|
|
655
|
+
visibleReleaseCount,
|
|
656
|
+
testcaseCount: totalTestcases,
|
|
657
|
+
executionCount: totalExecutions,
|
|
658
|
+
teamCount,
|
|
659
|
+
automatedTestCaseCount: automatedTestCases,
|
|
660
|
+
manualTestCaseCount: manualTestCases,
|
|
661
|
+
automationPercentage,
|
|
662
|
+
},
|
|
601
663
|
metrics: {
|
|
602
664
|
requirements: {
|
|
603
665
|
total: totalReqs,
|
|
@@ -607,23 +669,38 @@ export class QualityGates {
|
|
|
607
669
|
},
|
|
608
670
|
testCases: {
|
|
609
671
|
total: totalTestcases,
|
|
672
|
+
automated: automatedTestCases,
|
|
673
|
+
manual: manualTestCases,
|
|
674
|
+
automationPercentage,
|
|
610
675
|
},
|
|
611
676
|
executions: {
|
|
612
677
|
total: totalExecutions,
|
|
613
|
-
passed: passedCount,
|
|
614
|
-
failed: failedCount,
|
|
615
|
-
blocked: blockedCount,
|
|
616
|
-
wip: wipCount,
|
|
617
|
-
notExecuted: notExecutedCount,
|
|
618
|
-
executionRate,
|
|
619
|
-
passRate,
|
|
620
678
|
},
|
|
621
679
|
defects: {
|
|
622
680
|
total: totalDefects,
|
|
623
681
|
open: openDefects,
|
|
624
682
|
},
|
|
625
683
|
},
|
|
626
|
-
|
|
684
|
+
project: {
|
|
685
|
+
id: projectId,
|
|
686
|
+
name: project.name || 'Unknown',
|
|
687
|
+
description: project.description || '',
|
|
688
|
+
startDate: project.projectStartDate,
|
|
689
|
+
endDate: project.projectEndDate,
|
|
690
|
+
totalMembers: projectMembers.length,
|
|
691
|
+
members: memberDetails,
|
|
692
|
+
teamCount,
|
|
693
|
+
totalReleases: totalReleaseCount,
|
|
694
|
+
visibleReleases: visibleReleaseCount,
|
|
695
|
+
releases: projectReleases.map(r => ({
|
|
696
|
+
id: r.id,
|
|
697
|
+
name: r.name,
|
|
698
|
+
startDate: r.releaseStartDate,
|
|
699
|
+
endDate: r.releaseEndDate,
|
|
700
|
+
isCurrent: r.id === releaseId,
|
|
701
|
+
})),
|
|
702
|
+
},
|
|
703
|
+
recommendations: this.getHealthRecommendations(healthScore, reqCoverage, automationPercentage, 0, openDefects),
|
|
627
704
|
};
|
|
628
705
|
}
|
|
629
706
|
|
|
@@ -1041,19 +1118,56 @@ export class QualityGates {
|
|
|
1041
1118
|
|
|
1042
1119
|
const executions = executionData.results || executionData || [];
|
|
1043
1120
|
|
|
1044
|
-
//
|
|
1045
|
-
const
|
|
1121
|
+
// Cache for user names (to avoid multiple API calls)
|
|
1122
|
+
const userCache = {};
|
|
1123
|
+
|
|
1124
|
+
// Helper to get user name by ID
|
|
1125
|
+
const getUserName = async (userId) => {
|
|
1126
|
+
if (!userId || userId < 0) return null;
|
|
1127
|
+
if (userCache[userId]) return userCache[userId];
|
|
1128
|
+
try {
|
|
1129
|
+
const user = await this.GET(`/user/${userId}`);
|
|
1130
|
+
userCache[userId] = user.fullName || `${user.firstName} ${user.lastName}`.trim() || user.username;
|
|
1131
|
+
return userCache[userId];
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// Aggregate by assigned user and executor
|
|
1138
|
+
const assignedStats = {};
|
|
1139
|
+
const executorStats = {};
|
|
1046
1140
|
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
1047
1141
|
|
|
1142
|
+
// Collect unique executor IDs first
|
|
1143
|
+
const executorIds = new Set();
|
|
1048
1144
|
for (const exec of executions) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1145
|
+
if (exec.executedBy && exec.executedBy > 0) executorIds.add(exec.executedBy);
|
|
1146
|
+
if (exec.lastTestResult?.testerId && exec.lastTestResult.testerId > 0) {
|
|
1147
|
+
executorIds.add(exec.lastTestResult.testerId);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Fetch all executor names in parallel
|
|
1152
|
+
await Promise.all([...executorIds].map(id => getUserName(id)));
|
|
1153
|
+
|
|
1154
|
+
for (const exec of executions) {
|
|
1155
|
+
// Assigned user
|
|
1156
|
+
const assignedName = exec.testerIdName?.trim() || "Unassigned";
|
|
1157
|
+
const assignedId = exec.testerId;
|
|
1158
|
+
|
|
1159
|
+
// Executor (who actually ran the test)
|
|
1160
|
+
const executorId = exec.lastTestResult?.testerId || exec.executedBy;
|
|
1161
|
+
const executorName = userCache[executorId] || "Unknown";
|
|
1162
|
+
|
|
1051
1163
|
const executedOn = exec.lastTestResult?.executedOn || exec.executedOn;
|
|
1164
|
+
const status = exec.lastTestResult?.executionStatus || exec.status || 0;
|
|
1052
1165
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1166
|
+
// Track assigned stats
|
|
1167
|
+
if (!assignedStats[assignedName]) {
|
|
1168
|
+
assignedStats[assignedName] = {
|
|
1169
|
+
userId: assignedId,
|
|
1170
|
+
name: assignedName,
|
|
1057
1171
|
assigned: 0,
|
|
1058
1172
|
executed: 0,
|
|
1059
1173
|
passed: 0,
|
|
@@ -1062,44 +1176,69 @@ export class QualityGates {
|
|
|
1062
1176
|
lastActivity: null,
|
|
1063
1177
|
};
|
|
1064
1178
|
}
|
|
1179
|
+
assignedStats[assignedName].assigned++;
|
|
1065
1180
|
|
|
1066
|
-
userStats[testerName].assigned++;
|
|
1067
|
-
|
|
1068
|
-
const status = exec.lastTestResult?.executionStatus || exec.status || 0;
|
|
1069
1181
|
if (Number(status) > 0) {
|
|
1070
|
-
|
|
1182
|
+
assignedStats[assignedName].executed++;
|
|
1183
|
+
switch (Number(status)) {
|
|
1184
|
+
case 1: assignedStats[assignedName].passed++; break;
|
|
1185
|
+
case 2: assignedStats[assignedName].failed++; break;
|
|
1186
|
+
case 4: assignedStats[assignedName].blocked++; break;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Track executor stats (who actually ran tests)
|
|
1191
|
+
if (Number(status) > 0 && executorId && executorId > 0) {
|
|
1192
|
+
if (!executorStats[executorName]) {
|
|
1193
|
+
executorStats[executorName] = {
|
|
1194
|
+
userId: executorId,
|
|
1195
|
+
name: executorName,
|
|
1196
|
+
executed: 0,
|
|
1197
|
+
passed: 0,
|
|
1198
|
+
failed: 0,
|
|
1199
|
+
blocked: 0,
|
|
1200
|
+
lastActivity: null,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1071
1203
|
|
|
1204
|
+
executorStats[executorName].executed++;
|
|
1072
1205
|
switch (Number(status)) {
|
|
1073
|
-
case 1:
|
|
1074
|
-
case 2:
|
|
1075
|
-
case 4:
|
|
1206
|
+
case 1: executorStats[executorName].passed++; break;
|
|
1207
|
+
case 2: executorStats[executorName].failed++; break;
|
|
1208
|
+
case 4: executorStats[executorName].blocked++; break;
|
|
1076
1209
|
}
|
|
1077
1210
|
|
|
1078
1211
|
if (executedOn) {
|
|
1079
1212
|
const execDate = new Date(executedOn);
|
|
1080
|
-
if (!
|
|
1081
|
-
|
|
1213
|
+
if (!executorStats[executorName].lastActivity || execDate > new Date(executorStats[executorName].lastActivity)) {
|
|
1214
|
+
executorStats[executorName].lastActivity = executedOn;
|
|
1082
1215
|
}
|
|
1083
1216
|
}
|
|
1084
1217
|
}
|
|
1085
1218
|
}
|
|
1086
1219
|
|
|
1087
|
-
// Convert to
|
|
1088
|
-
const
|
|
1220
|
+
// Convert to arrays and calculate metrics
|
|
1221
|
+
const assignedUsers = Object.values(assignedStats)
|
|
1089
1222
|
.map(user => ({
|
|
1090
1223
|
...user,
|
|
1091
1224
|
completionRate: user.assigned > 0 ? Math.round((user.executed / user.assigned) * 100) : 0,
|
|
1092
1225
|
passRate: user.executed > 0 ? Math.round((user.passed / user.executed) * 100) : 0,
|
|
1093
1226
|
}))
|
|
1227
|
+
.sort((a, b) => b.assigned - a.assigned);
|
|
1228
|
+
|
|
1229
|
+
const executors = Object.values(executorStats)
|
|
1230
|
+
.map(user => ({
|
|
1231
|
+
...user,
|
|
1232
|
+
passRate: user.executed > 0 ? Math.round((user.passed / user.executed) * 100) : 0,
|
|
1233
|
+
}))
|
|
1094
1234
|
.sort((a, b) => b.executed - a.executed);
|
|
1095
1235
|
|
|
1096
1236
|
// Team summary
|
|
1097
|
-
const teamSummary =
|
|
1098
|
-
totalAssigned: acc.totalAssigned + user.assigned,
|
|
1237
|
+
const teamSummary = executors.reduce((acc, user) => ({
|
|
1099
1238
|
totalExecuted: acc.totalExecuted + user.executed,
|
|
1100
1239
|
totalPassed: acc.totalPassed + user.passed,
|
|
1101
1240
|
totalFailed: acc.totalFailed + user.failed,
|
|
1102
|
-
}), {
|
|
1241
|
+
}), { totalExecuted: 0, totalPassed: 0, totalFailed: 0 });
|
|
1103
1242
|
|
|
1104
1243
|
return {
|
|
1105
1244
|
tool: "User Activity",
|
|
@@ -1108,18 +1247,17 @@ export class QualityGates {
|
|
|
1108
1247
|
timestamp: new Date().toISOString(),
|
|
1109
1248
|
period: { days },
|
|
1110
1249
|
teamSummary: {
|
|
1250
|
+
totalAssigned: executions.length,
|
|
1111
1251
|
...teamSummary,
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
teamCompletionRate: teamSummary.totalAssigned > 0
|
|
1115
|
-
? Math.round((teamSummary.totalExecuted / teamSummary.totalAssigned) * 100)
|
|
1116
|
-
: 0,
|
|
1252
|
+
activeExecutors: executors.length,
|
|
1253
|
+
totalAssignees: assignedUsers.length,
|
|
1117
1254
|
teamPassRate: teamSummary.totalExecuted > 0
|
|
1118
1255
|
? Math.round((teamSummary.totalPassed / teamSummary.totalExecuted) * 100)
|
|
1119
1256
|
: 0,
|
|
1120
1257
|
},
|
|
1121
|
-
|
|
1122
|
-
|
|
1258
|
+
assignedTo: assignedUsers,
|
|
1259
|
+
executedBy: executors,
|
|
1260
|
+
topExecutors: executors.slice(0, 5),
|
|
1123
1261
|
};
|
|
1124
1262
|
}
|
|
1125
1263
|
}
|