zephyr-enterprise-tools 1.2.4 → 1.2.6

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
@@ -44,12 +44,13 @@ const TOOLS = [
44
44
  },
45
45
  {
46
46
  name: 'test_plan_analysis',
47
- description: 'Analyze test planning status. Threshold: <80% = NO GO, 80-90% = CONDITIONAL, ≥90% = GO.',
47
+ 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
48
  inputSchema: {
49
49
  type: 'object',
50
50
  properties: {
51
51
  projectId: { type: 'number', description: 'Zephyr project ID' },
52
52
  releaseId: { type: 'number', description: 'Zephyr release ID' },
53
+ query: { type: 'string', description: 'Optional Zephyr ZQL expression, e.g. priority = "P1"' },
53
54
  },
54
55
  required: ['projectId', 'releaseId'],
55
56
  },
@@ -142,13 +143,13 @@ const TOOLS = [
142
143
  },
143
144
  {
144
145
  name: 'search_test_cases',
145
- description: 'Search test cases by query string.',
146
+ description: 'Search test cases using a Zephyr ZQL expression, such as priority = "P1".',
146
147
  inputSchema: {
147
148
  type: 'object',
148
149
  properties: {
149
150
  projectId: { type: 'number', description: 'Zephyr project ID' },
150
151
  releaseId: { type: 'number', description: 'Zephyr release ID' },
151
- query: { type: 'string', description: 'Search query string' },
152
+ query: { type: 'string', description: 'Zephyr ZQL expression, e.g. priority = "P1"' },
152
153
  limit: { type: 'number', description: 'Maximum results (default: 50)' },
153
154
  },
154
155
  required: ['projectId', 'releaseId'],
@@ -272,7 +273,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
272
273
  break;
273
274
 
274
275
  case 'test_plan_analysis':
275
- result = await tools.testPlanAnalysisGate(projectId, releaseId);
276
+ result = await tools.testPlanAnalysisGate(projectId, releaseId, { query: args.query });
276
277
  break;
277
278
 
278
279
  case 'test_execution':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zephyr-enterprise-tools",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
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;
@@ -133,6 +138,7 @@ export interface TestTrendsResult {
133
138
  }
134
139
 
135
140
  export interface SearchTestCasesOptions {
141
+ /** Zephyr ZQL expression, e.g. `priority = "P1"`. */
136
142
  query?: string;
137
143
  limit?: number;
138
144
  }
@@ -176,7 +182,7 @@ export declare class QualityGates {
176
182
 
177
183
  // Quality Gates (Release Readiness)
178
184
  requirementCoverageGate(projectId: number, releaseId: number): Promise<RequirementCoverageResult>;
179
- testPlanAnalysisGate(projectId: number, releaseId: number): Promise<TestPlanResult>;
185
+ testPlanAnalysisGate(projectId: number, releaseId: number, options?: TestPlanOptions): Promise<TestPlanResult>;
180
186
  testExecutionGate(projectId: number, releaseId: number): Promise<TestExecutionResult>;
181
187
  defectQualityGate(projectId: number, releaseId: number): Promise<DefectQualityResult>;
182
188
  runAllGates(projectId: number, releaseId: number): Promise<ReleaseReadinessResult>;
@@ -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,
@@ -1031,53 +1090,65 @@ export class QualityGates {
1031
1090
  async searchTestCases(projectId, releaseId, options = {}) {
1032
1091
  const { query = '', status, priority, limit = 50 } = options;
1033
1092
 
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
1093
  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);
1094
+ if (query) {
1095
+ const pageSize = 5000;
1096
+ let firstResult = 0;
1097
+
1098
+ while (true) {
1099
+ const tcData = await this.POSTv3("/advancesearch/zql", {
1100
+ firstresult: firstResult,
1101
+ maxresults: pageSize,
1102
+ entitytype: "testcase",
1103
+ order: "orderId",
1104
+ isascorder: true,
1105
+ is_cfield: false,
1106
+ releaseid: String(releaseId),
1107
+ projectid: String(projectId),
1108
+ word: query,
1109
+ zql: true,
1110
+ isOld: false,
1111
+ });
1112
+ const page = tcData[0]?.results || tcData.results || [];
1113
+
1114
+ if (!Array.isArray(page) || page.length === 0) break;
1115
+
1116
+ testcases.push(...page);
1117
+ firstResult += page.length;
1052
1118
  }
1053
- } catch (e) {
1054
- // Try alternative endpoint
1119
+ testcases = [...new Map(testcases.map(testcase => [testcase.testcase?.id || testcase.id, testcase])).values()];
1120
+ } else {
1121
+ const params = {
1122
+ projectId,
1123
+ releaseId,
1124
+ offset: 0,
1125
+ maxRecords: 500,
1126
+ };
1055
1127
  try {
1056
- const tcData = await this.GET("/testcase", params);
1128
+ const tcData = await this.GET("/testcase/tree", params);
1057
1129
  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
- };
1130
+ if (!Array.isArray(testcases)) {
1131
+ testcases = this.flattenTestcaseTree(tcData);
1132
+ }
1133
+ } catch (e) {
1134
+ try {
1135
+ const tcData = await this.GET("/testcase", params);
1136
+ testcases = tcData.results || tcData || [];
1137
+ } catch (e2) {
1138
+ return {
1139
+ tool: "Search Test Cases",
1140
+ projectId,
1141
+ releaseId,
1142
+ error: "Unable to fetch test cases",
1143
+ results: [],
1144
+ };
1145
+ }
1066
1146
  }
1067
1147
  }
1068
1148
 
1069
1149
  // Filter results
1070
1150
  let filtered = testcases;
1071
1151
 
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
1152
  if (status) {
1082
1153
  filtered = filtered.filter(tc =>
1083
1154
  (tc.status || '').toLowerCase() === status.toLowerCase()
@@ -1091,17 +1162,20 @@ export class QualityGates {
1091
1162
  }
1092
1163
 
1093
1164
  // 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,
1165
+ const results = filtered.slice(0, limit).map(tc => {
1166
+ const testcase = tc.testcase || tc;
1167
+ return {
1168
+ id: testcase.id,
1169
+ key: testcase.testcaseKey || testcase.alternateId || testcase.externalId,
1170
+ name: testcase.name,
1171
+ status: testcase.status,
1172
+ priority: testcase.priority,
1173
+ automated: testcase.automated || testcase.isAutomated || false,
1101
1174
  folder: tc.folderPath || tc.tcrCatalogTreeId?.name,
1102
- estimatedTime: tc.estimatedTime,
1103
- tags: tc.tags || [],
1104
- }));
1175
+ estimatedTime: testcase.estimatedTime,
1176
+ tags: testcase.tags || [],
1177
+ };
1178
+ });
1105
1179
 
1106
1180
  return {
1107
1181
  tool: "Search Test Cases",
@@ -1293,9 +1367,10 @@ export class QualityGates {
1293
1367
 
1294
1368
  // Fetch all executions for the release
1295
1369
  const executionData = await this.GET('/execution', {
1296
- projectId,
1297
- releaseId,
1298
- maxResults: 10000,
1370
+ releaseid: releaseId,
1371
+ offset: 0,
1372
+ pagesize: 10000,
1373
+ includeanyoneuser: true,
1299
1374
  });
1300
1375
  const executions = executionData.results || executionData || [];
1301
1376
  const total = executions.length;