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 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-Enterprise-tools
35
+ npm install -g zephyr-enterprise-tools
36
36
 
37
37
  # Or install locally
38
- npm install zephyr-Enterprise-tools
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
- // Users list
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zephyr-enterprise-tools",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Comprehensive Zephyr Enterprise Tools - Release Readiness, Project Health, Test Analytics & More",
5
5
  "main": "zephyr-enterprise-tools.js",
6
6
  "exports": {
@@ -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
- const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
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
- // Requirements metrics
551
- const reqData = summary.requirement || {};
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
- // 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 || {};
616
+ // Defect metrics from release summary
617
+ const defectData = releaseSummary.defect || {};
575
618
  const totalDefects = defectData.totalDefectCount || 0;
576
- const openDefects = defectData.openDefectCount || totalDefects;
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 execScore = executionRate;
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
- (reqScore * 0.25 + execScore * 0.35 + passScore * 0.40) - defectPenalty
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
- recommendations: this.getHealthRecommendations(healthScore, reqCoverage, executionRate, passRate, openDefects),
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
- // Aggregate by user
1045
- const userStats = {};
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
- const testerName = exec.lastTestResult?.testerName || exec.testerName || "Unassigned";
1050
- const testerId = exec.testerId || exec.lastTestResult?.testerId;
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
- if (!userStats[testerName]) {
1054
- userStats[testerName] = {
1055
- userId: testerId,
1056
- name: testerName,
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
- userStats[testerName].executed++;
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: userStats[testerName].passed++; break;
1074
- case 2: userStats[testerName].failed++; break;
1075
- case 4: userStats[testerName].blocked++; break;
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 (!userStats[testerName].lastActivity || execDate > new Date(userStats[testerName].lastActivity)) {
1081
- userStats[testerName].lastActivity = executedOn;
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 array and calculate metrics
1088
- const users = Object.values(userStats)
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 = users.reduce((acc, user) => ({
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
- }), { totalAssigned: 0, totalExecuted: 0, totalPassed: 0, totalFailed: 0 });
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
- 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,
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
- users,
1122
- topPerformers: users.filter(u => u.executed > 0).slice(0, 5),
1258
+ assignedTo: assignedUsers,
1259
+ executedBy: executors,
1260
+ topExecutors: executors.slice(0, 5),
1123
1261
  };
1124
1262
  }
1125
1263
  }