zephyr-enterprise-tools 1.2.5 → 1.2.7

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
@@ -25,6 +25,7 @@ Comprehensive tools for Zephyr Enterprise - Release Readiness, Project Health, T
25
25
  | `test-trends` | Test execution trends over time |
26
26
  | `search-tests` | Search test cases by query |
27
27
  | `user-activity` | User activity and productivity metrics |
28
+ | `user-trend` | Full audit log history for a user — every action across the system |
28
29
  | `execution-burndown` | Day-by-day execution burndown (remaining vs ideal) |
29
30
 
30
31
  ---
@@ -73,6 +74,7 @@ Use zephyr-enterprise-tools as an MCP (Model Context Protocol) server with your
73
74
  | `test_trends` | Get execution trends over time |
74
75
  | `search_test_cases` | Search test cases by query |
75
76
  | `user_activity` | Get user activity metrics |
77
+ | `user_trend` | Full audit log history for a user — filter by date range, entity type, and operation |
76
78
  | `execution_burndown` | Day-by-day execution burndown chart data (remaining vs ideal) |
77
79
 
78
80
  ### Claude Desktop
package/mcp-server.js CHANGED
@@ -20,12 +20,13 @@ import QualityGates from './zephyr-enterprise-tools.js';
20
20
  const TOOLS = [
21
21
  {
22
22
  name: 'release_readiness',
23
- description: 'Run all 4 quality gates to assess release readiness. Returns GO, CONDITIONAL GO, or NO GO status.',
23
+ description: 'Run all 4 quality gates with calculation details and action items. Optionally scope Test Plan Analysis to a Zephyr ZQL expression. Returns GO, CONDITIONAL GO, or NO GO status.',
24
24
  inputSchema: {
25
25
  type: 'object',
26
26
  properties: {
27
27
  projectId: { type: 'number', description: 'Zephyr project ID' },
28
28
  releaseId: { type: 'number', description: 'Zephyr release ID' },
29
+ query: { type: 'string', description: 'Optional Zephyr ZQL expression for Test Plan Analysis, e.g. priority = "P1"' },
29
30
  },
30
31
  required: ['projectId', 'releaseId'],
31
32
  },
@@ -44,12 +45,13 @@ const TOOLS = [
44
45
  },
45
46
  {
46
47
  name: 'test_plan_analysis',
47
- description: 'Analyze test planning status. Threshold: <80% = NO GO, 80-90% = CONDITIONAL, ≥90% = GO.',
48
+ description: 'Analyze test planning status, optionally scoped to testcase IDs returned by a Zephyr ZQL expression. Threshold: <80% = NO GO, 80-90% = CONDITIONAL, ≥90% = GO.',
48
49
  inputSchema: {
49
50
  type: 'object',
50
51
  properties: {
51
52
  projectId: { type: 'number', description: 'Zephyr project ID' },
52
53
  releaseId: { type: 'number', description: 'Zephyr release ID' },
54
+ query: { type: 'string', description: 'Optional Zephyr ZQL expression, e.g. priority = "P1"' },
53
55
  },
54
56
  required: ['projectId', 'releaseId'],
55
57
  },
@@ -142,13 +144,13 @@ const TOOLS = [
142
144
  },
143
145
  {
144
146
  name: 'search_test_cases',
145
- description: 'Search test cases by query string.',
147
+ description: 'Search test cases using a Zephyr ZQL expression, such as priority = "P1".',
146
148
  inputSchema: {
147
149
  type: 'object',
148
150
  properties: {
149
151
  projectId: { type: 'number', description: 'Zephyr project ID' },
150
152
  releaseId: { type: 'number', description: 'Zephyr release ID' },
151
- query: { type: 'string', description: 'Search query string' },
153
+ query: { type: 'string', description: 'Zephyr ZQL expression, e.g. priority = "P1"' },
152
154
  limit: { type: 'number', description: 'Maximum results (default: 50)' },
153
155
  },
154
156
  required: ['projectId', 'releaseId'],
@@ -264,7 +266,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
264
266
 
265
267
  switch (name) {
266
268
  case 'release_readiness':
267
- result = await tools.runAllGates(projectId, releaseId);
269
+ result = await tools.runAllGates(projectId, releaseId, { query: args.query });
268
270
  break;
269
271
 
270
272
  case 'requirement_coverage':
@@ -272,7 +274,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
272
274
  break;
273
275
 
274
276
  case 'test_plan_analysis':
275
- result = await tools.testPlanAnalysisGate(projectId, releaseId);
277
+ result = await tools.testPlanAnalysisGate(projectId, releaseId, { query: args.query });
276
278
  break;
277
279
 
278
280
  case 'test_execution':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zephyr-enterprise-tools",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Comprehensive Zephyr Enterprise Tools - Release Readiness, Project Health, Test Analytics & More",
5
5
  "main": "zephyr-enterprise-tools.js",
6
6
  "types": "zephyr-enterprise-tools.d.ts",
@@ -38,6 +38,11 @@ export interface TestPlanResult extends GateResult {
38
38
  assignedTests: number;
39
39
  }
40
40
 
41
+ export interface TestPlanOptions {
42
+ /** Optional Zephyr ZQL expression, e.g. `priority = "P1"`. */
43
+ query?: string;
44
+ }
45
+
41
46
  export interface TestExecutionResult extends GateResult {
42
47
  executionPercentage: number;
43
48
  completedTests: number;
@@ -65,8 +70,15 @@ export interface ReleaseReadinessResult {
65
70
  projectId: number;
66
71
  releaseId: number;
67
72
  timestamp: string;
73
+ report: {
74
+ projectName: string;
75
+ releaseName: string;
76
+ generatedAt: string;
77
+ fileName: string;
78
+ };
68
79
  overallStatus: GateStatus;
69
80
  summary: { passed: number; failed: number; conditional: number };
81
+ details: Record<string, unknown>;
70
82
  recommendation: string;
71
83
  gates: {
72
84
  requirementCoverage: RequirementCoverageResult;
@@ -133,6 +145,7 @@ export interface TestTrendsResult {
133
145
  }
134
146
 
135
147
  export interface SearchTestCasesOptions {
148
+ /** Zephyr ZQL expression, e.g. `priority = "P1"`. */
136
149
  query?: string;
137
150
  limit?: number;
138
151
  }
@@ -176,10 +189,10 @@ export declare class QualityGates {
176
189
 
177
190
  // Quality Gates (Release Readiness)
178
191
  requirementCoverageGate(projectId: number, releaseId: number): Promise<RequirementCoverageResult>;
179
- testPlanAnalysisGate(projectId: number, releaseId: number): Promise<TestPlanResult>;
192
+ testPlanAnalysisGate(projectId: number, releaseId: number, options?: TestPlanOptions): Promise<TestPlanResult>;
180
193
  testExecutionGate(projectId: number, releaseId: number): Promise<TestExecutionResult>;
181
194
  defectQualityGate(projectId: number, releaseId: number): Promise<DefectQualityResult>;
182
- runAllGates(projectId: number, releaseId: number): Promise<ReleaseReadinessResult>;
195
+ runAllGates(projectId: number, releaseId: number, options?: TestPlanOptions): Promise<ReleaseReadinessResult>;
183
196
 
184
197
  // Analytics & Insights
185
198
  getProjectHealth(projectId: number, releaseId: number): Promise<ProjectHealthResult>;
@@ -113,6 +113,50 @@ export class QualityGates {
113
113
  return res.json();
114
114
  }
115
115
 
116
+ async POSTv3(path, body) {
117
+ const v3BaseUrl = this.baseUrl.replace('/latest', '/v3');
118
+ const res = await fetch(`${v3BaseUrl}${path}`, {
119
+ method: "POST",
120
+ headers: { Accept: "application/json", "Content-Type": "application/json", ...this.authHeader() },
121
+ body: JSON.stringify(body),
122
+ });
123
+ if (!res.ok) {
124
+ const text = await res.text().catch(() => "");
125
+ throw new Error(`Zephyr v3 API ${res.status}: ${text}`);
126
+ }
127
+ return res.json();
128
+ }
129
+
130
+ async searchExecutionsByZql(releaseId, query) {
131
+ const pageSize = 50;
132
+ let firstResult = 0;
133
+ const executions = [];
134
+
135
+ while (true) {
136
+ const response = await this.POSTv3("/advancesearch/zql", {
137
+ firstresult: firstResult,
138
+ maxresults: pageSize,
139
+ entitytype: "execution",
140
+ order: "testcaseId",
141
+ isascorder: true,
142
+ is_cfield: false,
143
+ releaseid: String(releaseId),
144
+ projectid: "",
145
+ word: query,
146
+ zql: true,
147
+ isOld: false,
148
+ });
149
+ const page = response[0]?.results || response.results || [];
150
+
151
+ if (!Array.isArray(page) || page.length === 0) break;
152
+
153
+ executions.push(...page);
154
+ firstResult += page.length;
155
+ }
156
+
157
+ return [...new Map(executions.map(execution => [execution.id, execution])).values()];
158
+ }
159
+
116
160
  async PUT(path, params, body) {
117
161
  const url = new URL(`${this.baseUrl}${path}`);
118
162
  for (const [k, v] of Object.entries(params)) {
@@ -163,22 +207,36 @@ export class QualityGates {
163
207
 
164
208
  // ─── Gate 2: Test Plan Analysis ─────────────────────────────────────────────
165
209
 
166
- async testPlanAnalysisGate(projectId, releaseId) {
167
- const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
168
-
169
- const totalTestcases = summary.testcase?.totalTestcaseCount || 0;
170
- const mappedRequirements = summary.requirement?.mappedRequirementCount || 0;
171
- const totalRequirements = summary.requirement?.totalRequirementCount || 0;
172
-
173
- // Get all executions for the release
174
- const executionData = await this.GET("/execution", {
175
- releaseid: releaseId,
176
- offset: 0,
177
- pagesize: 10000,
178
- includeanyoneuser: true,
179
- });
180
-
181
- const executions = executionData.results || executionData || [];
210
+ async testPlanAnalysisGate(projectId, releaseId, options = {}) {
211
+ const { query } = options;
212
+ let totalTestcases;
213
+ let mappedRequirements;
214
+ let totalRequirements;
215
+ let executions;
216
+
217
+ if (query) {
218
+ const [matchingTestcases, matchingExecutions] = await Promise.all([
219
+ this.searchTestCases(projectId, releaseId, {
220
+ query,
221
+ limit: Number.MAX_SAFE_INTEGER,
222
+ }),
223
+ this.searchExecutionsByZql(releaseId, query),
224
+ ]);
225
+ totalTestcases = matchingTestcases.results.length;
226
+ executions = matchingExecutions;
227
+ } else {
228
+ const summary = await this.GET(`/summary/release/${releaseId}`, { isHideCycleEnabled: false });
229
+ totalTestcases = summary.testcase?.totalTestcaseCount || 0;
230
+ mappedRequirements = summary.requirement?.mappedRequirementCount || 0;
231
+ totalRequirements = summary.requirement?.totalRequirementCount || 0;
232
+ const executionData = await this.GET("/execution", {
233
+ releaseid: releaseId,
234
+ offset: 0,
235
+ pagesize: 10000,
236
+ includeanyoneuser: true,
237
+ });
238
+ executions = executionData.results || executionData || [];
239
+ }
182
240
  const totalExecutions = Array.isArray(executions) ? executions.length : 0;
183
241
 
184
242
  // Count assigned executions
@@ -223,6 +281,7 @@ export class QualityGates {
223
281
  gate: "Test Plan Analysis",
224
282
  projectId,
225
283
  releaseId,
284
+ query: query || undefined,
226
285
  analysis: {
227
286
  testcasePlanning: {
228
287
  totalTestcases,
@@ -234,7 +293,7 @@ export class QualityGates {
234
293
  assignedExecutions,
235
294
  percentage: executionAssignmentPct,
236
295
  },
237
- requirementCoverage: {
296
+ requirementCoverage: query ? undefined : {
238
297
  totalRequirements,
239
298
  mappedRequirements,
240
299
  percentage: totalRequirements > 0 ? Math.round((mappedRequirements / totalRequirements) * 100 * 100) / 100 : 0,
@@ -481,13 +540,20 @@ export class QualityGates {
481
540
 
482
541
  // ─── Run All Gates ──────────────────────────────────────────────────────────
483
542
 
484
- async runAllGates(projectId, releaseId) {
485
- const results = await Promise.all([
486
- this.requirementCoverageGate(projectId, releaseId),
487
- this.testPlanAnalysisGate(projectId, releaseId),
488
- this.testExecutionGate(projectId, releaseId),
489
- this.defectQualityGate(projectId, releaseId),
543
+ async runAllGates(projectId, releaseId, options = {}) {
544
+ const { query } = options;
545
+ const [results, project, releases] = await Promise.all([
546
+ Promise.all([
547
+ this.requirementCoverageGate(projectId, releaseId),
548
+ this.testPlanAnalysisGate(projectId, releaseId, { query }),
549
+ this.testExecutionGate(projectId, releaseId),
550
+ this.defectQualityGate(projectId, releaseId),
551
+ ]),
552
+ this.GET(`/project/${projectId}`).catch(() => ({})),
553
+ this.GET('/release', { projectid: projectId, isaliasallowed: false }).catch(() => []),
490
554
  ]);
555
+ const release = Array.isArray(releases) ? releases.find(item => Number(item.id) === Number(releaseId)) : undefined;
556
+ const generatedAt = new Date().toISOString();
491
557
 
492
558
  const gates = {
493
559
  requirementCoverage: results[0],
@@ -517,7 +583,13 @@ export class QualityGates {
517
583
  return {
518
584
  projectId,
519
585
  releaseId,
520
- timestamp: new Date().toISOString(),
586
+ timestamp: generatedAt,
587
+ report: {
588
+ projectName: project.name || `Project ${projectId}`,
589
+ releaseName: release?.name || `Release ${releaseId}`,
590
+ generatedAt,
591
+ fileName: this.getReportFileName(project.name || `Project ${projectId}`, release?.name || `Release ${releaseId}`, generatedAt),
592
+ },
521
593
  overallStatus,
522
594
  summary: {
523
595
  passed: passedGates,
@@ -526,10 +598,72 @@ export class QualityGates {
526
598
  total: 4,
527
599
  },
528
600
  gates,
601
+ details: this.getReadinessDetails(gates, query),
529
602
  recommendation: this.getRecommendation(overallStatus, gates),
530
603
  };
531
604
  }
532
605
 
606
+ getReportFileName(projectName, releaseName, generatedAt) {
607
+ const safeSegment = value => String(value)
608
+ .trim()
609
+ .replace(/[^a-zA-Z0-9]+/g, '_')
610
+ .replace(/^_+|_+$/g, '');
611
+ const timestamp = new Date(generatedAt).toISOString();
612
+ const date = timestamp.slice(0, 10);
613
+ const time = timestamp.slice(11, 19).replace(/:/g, '-');
614
+
615
+ return `Release_Rediness_${safeSegment(projectName)}_${safeSegment(releaseName)}_${date}_${time}_UTC.pdf`;
616
+ }
617
+
618
+ getReadinessDetails(gates, query) {
619
+ const plan = gates.testPlanAnalysis;
620
+ const coverage = gates.requirementCoverage;
621
+ const execution = gates.testExecution;
622
+ const defects = gates.defectQuality;
623
+ const planAnalysis = plan.analysis;
624
+ const remainingToPlan = Math.max(0, planAnalysis.testcasePlanning.totalTestcases - planAnalysis.testcasePlanning.plannedTestcases);
625
+ const remainingToExecute = execution.incompleteTests;
626
+ const additionalTestsForGo = Math.max(0, Math.ceil((0.97 * execution.totalPlannedTests) - execution.completedTests));
627
+
628
+ return {
629
+ testPlanAnalysis: {
630
+ scope: query ? "ZQL-filtered test cases" : "Release-wide",
631
+ query: query || undefined,
632
+ calculation: {
633
+ testcasePlanning: `${planAnalysis.testcasePlanning.plannedTestcases} / ${planAnalysis.testcasePlanning.totalTestcases} = ${planAnalysis.testcasePlanning.percentage}%`,
634
+ executionAssignment: `${planAnalysis.executionAssignment.assignedExecutions} / ${planAnalysis.executionAssignment.totalExecutions} = ${planAnalysis.executionAssignment.percentage}%`,
635
+ overallPlanning: `(${planAnalysis.testcasePlanning.percentage}% + ${planAnalysis.executionAssignment.percentage}%) / 2 = ${plan.overallPlanningPercentage}%`,
636
+ },
637
+ actionItems: remainingToPlan > 0
638
+ ? [`Plan the remaining ${remainingToPlan} test cases.`, "Maintain execution assignment as tests are planned."]
639
+ : ["Maintain complete test planning and execution assignment."],
640
+ },
641
+ requirementCoverage: {
642
+ scope: "Release-wide",
643
+ calculation: `${coverage.coveredRequirements} / ${coverage.totalRequirements} = ${coverage.coveragePercentage}%`,
644
+ uncoveredRequirementIds: [],
645
+ detailAvailability: "Zephyr release summaries provide the uncovered count but not requirement IDs.",
646
+ actionItems: coverage.notCoveredRequirements > 0
647
+ ? [`Create and map tests for ${coverage.notCoveredRequirements} uncovered requirements.`, "Confirm requirement mappings before release sign-off."]
648
+ : ["Maintain requirement-to-test traceability through release sign-off."],
649
+ },
650
+ testExecution: {
651
+ scope: "Release-wide",
652
+ calculation: `${execution.completedTests} / ${execution.totalPlannedTests} = ${execution.executionPercentage}%`,
653
+ actionItems: remainingToExecute > 0
654
+ ? [`Execute or resolve ${remainingToExecute} incomplete tests.`, `Complete at least ${additionalTestsForGo} additional tests to reach the 97% GO threshold.`, ...(execution.breakdown.failed > 0 ? [`Investigate and rerun ${execution.breakdown.failed} failed test(s).`] : [])]
655
+ : ["Maintain execution completion through release sign-off."],
656
+ },
657
+ defectQuality: {
658
+ scope: "Release-wide",
659
+ defects: defects.breakdown,
660
+ actionItems: defects.unresolvedDefects > 0
661
+ ? ["Resolve all blocker defects.", "Reduce unresolved high-risk defects to 10 or fewer before approval."]
662
+ : ["Verify resolved defect fixes with targeted regression coverage.", "Confirm product-owner approval for any Won't Fix resolutions."],
663
+ },
664
+ };
665
+ }
666
+
533
667
  getRecommendation(status, gates) {
534
668
  const issues = [];
535
669
 
@@ -1031,53 +1165,65 @@ export class QualityGates {
1031
1165
  async searchTestCases(projectId, releaseId, options = {}) {
1032
1166
  const { query = '', status, priority, limit = 50 } = options;
1033
1167
 
1034
- // Get test cases
1035
- const params = {
1036
- projectId: projectId,
1037
- releaseId: releaseId,
1038
- offset: 0,
1039
- maxRecords: 500,
1040
- };
1041
-
1042
- if (query) params.word = query;
1043
-
1044
1168
  let testcases = [];
1045
- try {
1046
- const tcData = await this.GET("/testcase/tree", params);
1047
- testcases = tcData.results || tcData || [];
1048
-
1049
- // Flatten tree structure if needed
1050
- if (!Array.isArray(testcases)) {
1051
- testcases = this.flattenTestcaseTree(tcData);
1169
+ if (query) {
1170
+ const pageSize = 5000;
1171
+ let firstResult = 0;
1172
+
1173
+ while (true) {
1174
+ const tcData = await this.POSTv3("/advancesearch/zql", {
1175
+ firstresult: firstResult,
1176
+ maxresults: pageSize,
1177
+ entitytype: "testcase",
1178
+ order: "orderId",
1179
+ isascorder: true,
1180
+ is_cfield: false,
1181
+ releaseid: String(releaseId),
1182
+ projectid: String(projectId),
1183
+ word: query,
1184
+ zql: true,
1185
+ isOld: false,
1186
+ });
1187
+ const page = tcData[0]?.results || tcData.results || [];
1188
+
1189
+ if (!Array.isArray(page) || page.length === 0) break;
1190
+
1191
+ testcases.push(...page);
1192
+ firstResult += page.length;
1052
1193
  }
1053
- } catch (e) {
1054
- // Try alternative endpoint
1194
+ testcases = [...new Map(testcases.map(testcase => [testcase.testcase?.id || testcase.id, testcase])).values()];
1195
+ } else {
1196
+ const params = {
1197
+ projectId,
1198
+ releaseId,
1199
+ offset: 0,
1200
+ maxRecords: 500,
1201
+ };
1055
1202
  try {
1056
- const tcData = await this.GET("/testcase", params);
1203
+ const tcData = await this.GET("/testcase/tree", params);
1057
1204
  testcases = tcData.results || tcData || [];
1058
- } catch (e2) {
1059
- return {
1060
- tool: "Search Test Cases",
1061
- projectId,
1062
- releaseId,
1063
- error: "Unable to fetch test cases",
1064
- results: [],
1065
- };
1205
+ if (!Array.isArray(testcases)) {
1206
+ testcases = this.flattenTestcaseTree(tcData);
1207
+ }
1208
+ } catch (e) {
1209
+ try {
1210
+ const tcData = await this.GET("/testcase", params);
1211
+ testcases = tcData.results || tcData || [];
1212
+ } catch (e2) {
1213
+ return {
1214
+ tool: "Search Test Cases",
1215
+ projectId,
1216
+ releaseId,
1217
+ error: "Unable to fetch test cases",
1218
+ results: [],
1219
+ };
1220
+ }
1066
1221
  }
1067
1222
  }
1068
1223
 
1069
1224
  // Filter results
1070
1225
  let filtered = testcases;
1071
1226
 
1072
- if (query) {
1073
- const q = query.toLowerCase();
1074
- filtered = filtered.filter(tc =>
1075
- (tc.name || '').toLowerCase().includes(q) ||
1076
- (tc.testcaseKey || tc.alternateId || '').toLowerCase().includes(q) ||
1077
- (tc.description || '').toLowerCase().includes(q)
1078
- );
1079
- }
1080
-
1081
1227
  if (status) {
1082
1228
  filtered = filtered.filter(tc =>
1083
1229
  (tc.status || '').toLowerCase() === status.toLowerCase()
@@ -1091,17 +1237,20 @@ export class QualityGates {
1091
1237
  }
1092
1238
 
1093
1239
  // Map to clean output
1094
- const results = filtered.slice(0, limit).map(tc => ({
1095
- id: tc.id,
1096
- key: tc.testcaseKey || tc.alternateId,
1097
- name: tc.name,
1098
- status: tc.status,
1099
- priority: tc.priority,
1100
- automated: tc.automated || tc.isAutomated || false,
1240
+ const results = filtered.slice(0, limit).map(tc => {
1241
+ const testcase = tc.testcase || tc;
1242
+ return {
1243
+ id: testcase.id,
1244
+ key: testcase.testcaseKey || testcase.alternateId || testcase.externalId,
1245
+ name: testcase.name,
1246
+ status: testcase.status,
1247
+ priority: testcase.priority,
1248
+ automated: testcase.automated || testcase.isAutomated || false,
1101
1249
  folder: tc.folderPath || tc.tcrCatalogTreeId?.name,
1102
- estimatedTime: tc.estimatedTime,
1103
- tags: tc.tags || [],
1104
- }));
1250
+ estimatedTime: testcase.estimatedTime,
1251
+ tags: testcase.tags || [],
1252
+ };
1253
+ });
1105
1254
 
1106
1255
  return {
1107
1256
  tool: "Search Test Cases",