zephyr-scale-mcp-server 0.3.3 → 0.4.1
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 +14 -0
- package/build/tool-handlers.js +45 -47
- package/build/utils.js +37 -14
- package/package.json +2 -2
- package/src/tool-handlers.ts +45 -58
- package/src/utils.ts +46 -18
package/README.md
CHANGED
|
@@ -142,6 +142,20 @@ The server provides access to various resources through URI schemes:
|
|
|
142
142
|
- `ZEPHYR_BASE_URL`: `https://your-company.atlassian.net`
|
|
143
143
|
- `ZEPHYR_API_KEY`: Your Zephyr Scale API key (JWT). Generate it in Jira by clicking your profile picture (bottom left) → **Zephyr API keys**.
|
|
144
144
|
|
|
145
|
+
### Jira Cloud – regional API endpoint (optional)
|
|
146
|
+
Zephyr Scale Cloud has regional API hosts. By default the server uses the **US** endpoint (`https://api.zephyrscale.smartbear.com/v2`). If your instance uses the **EU** API (e.g. for EU data residency), set:
|
|
147
|
+
- **`ZEPHYR_API_BASE_URL`**: Full base URL for the Zephyr Scale Cloud API (no trailing slash).
|
|
148
|
+
|
|
149
|
+
**Example – EU:**
|
|
150
|
+
```json
|
|
151
|
+
"env": {
|
|
152
|
+
"ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
|
|
153
|
+
"ZEPHYR_API_KEY": "your-zephyr-api-token",
|
|
154
|
+
"ZEPHYR_API_BASE_URL": "https://eu.api.zephyrscale.smartbear.com/v2"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
If `ZEPHYR_API_BASE_URL` is not set, the US URL is used. Omit this variable for US instances.
|
|
158
|
+
|
|
145
159
|
### Jira Data Center Configuration
|
|
146
160
|
- `ZEPHYR_BASE_URL`: `https://your-jira-server.com`
|
|
147
161
|
- `ZEPHYR_API_KEY`: Your Zephyr Scale API token from your Jira profile settings.
|
package/build/tool-handlers.js
CHANGED
|
@@ -20,6 +20,14 @@ export class ZephyrToolHandlers {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
async createTestCase(args) {
|
|
23
|
+
// Some MCP clients (e.g. Claude Code) pass nested object parameters as JSON strings.
|
|
24
|
+
// Parse test_script if it arrived as a string.
|
|
25
|
+
if (typeof args.test_script === 'string') {
|
|
26
|
+
try {
|
|
27
|
+
args.test_script = JSON.parse(args.test_script);
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
23
31
|
if (this.jiraConfig.type === 'cloud') {
|
|
24
32
|
return this.createTestCaseCloud(args);
|
|
25
33
|
}
|
|
@@ -169,40 +177,22 @@ export class ZephyrToolHandlers {
|
|
|
169
177
|
}
|
|
170
178
|
async updateTestCaseBddCloud(args) {
|
|
171
179
|
const { test_case_key, bdd_content, name } = args;
|
|
180
|
+
const converted = convertToGherkin(bdd_content);
|
|
181
|
+
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
172
182
|
try {
|
|
173
|
-
// Step 1: GET existing test case to extract required fields for PUT
|
|
174
|
-
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
175
|
-
const tc = getResponse.data;
|
|
176
|
-
// Cloud v2 PUT requires: id, key, name, project, priority, status
|
|
177
|
-
const requiredFields = ['id', 'key', 'name', 'project', 'priority', 'status'];
|
|
178
|
-
for (const field of requiredFields) {
|
|
179
|
-
if (tc[field] === undefined || tc[field] === null) {
|
|
180
|
-
throw new McpError(ErrorCode.InternalError, `Existing test case is missing required field '${field}' needed for Cloud v2 update.`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const putPayload = {
|
|
184
|
-
id: tc.id,
|
|
185
|
-
key: tc.key,
|
|
186
|
-
name: typeof name === 'string' && name.trim().length > 0 ? name : tc.name,
|
|
187
|
-
project: tc.project,
|
|
188
|
-
priority: tc.priority,
|
|
189
|
-
status: tc.status,
|
|
190
|
-
};
|
|
191
|
-
// Preserve optional fields
|
|
192
|
-
for (const field of ['objective', 'precondition', 'estimatedTime', 'folder', 'component', 'owner']) {
|
|
193
|
-
if (tc[field] !== undefined)
|
|
194
|
-
putPayload[field] = tc[field];
|
|
195
|
-
}
|
|
196
|
-
if (Array.isArray(tc.labels))
|
|
197
|
-
putPayload.labels = tc.labels;
|
|
198
|
-
if (tc.customFields)
|
|
199
|
-
putPayload.customFields = tc.customFields;
|
|
200
|
-
// Step 2: PUT to update metadata
|
|
201
|
-
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
202
|
-
// Step 3: POST testscript with BDD content
|
|
203
|
-
const converted = convertToGherkin(bdd_content);
|
|
204
|
-
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
205
183
|
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`, { type: 'bdd', text: finalText });
|
|
184
|
+
// Only fetch and PUT metadata when the caller also wants to rename the test case
|
|
185
|
+
if (typeof name === 'string' && name.trim().length > 0) {
|
|
186
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
187
|
+
const tc = getResponse.data;
|
|
188
|
+
const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
189
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
|
|
190
|
+
projectKey,
|
|
191
|
+
name,
|
|
192
|
+
status: tc.status,
|
|
193
|
+
priority: tc.priority,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
206
196
|
return {
|
|
207
197
|
content: [{
|
|
208
198
|
type: 'text',
|
|
@@ -224,8 +214,9 @@ export class ZephyrToolHandlers {
|
|
|
224
214
|
const converted = convertToGherkin(bdd_content);
|
|
225
215
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
226
216
|
const payload = {};
|
|
217
|
+
const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
227
218
|
const requiredFields = [
|
|
228
|
-
['projectKey',
|
|
219
|
+
['projectKey', projectKey],
|
|
229
220
|
['name', testCaseData.name],
|
|
230
221
|
['status', testCaseData.status],
|
|
231
222
|
['priority', testCaseData.priority]
|
|
@@ -458,9 +449,16 @@ export class ZephyrToolHandlers {
|
|
|
458
449
|
if (response.status !== 201)
|
|
459
450
|
throw new Error(`Unexpected status code: ${response.status}`);
|
|
460
451
|
const cycleKey = response.data.key || 'Unknown';
|
|
461
|
-
// Step 2: add test cases
|
|
452
|
+
// Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
|
|
462
453
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
463
|
-
|
|
454
|
+
for (const testCaseKey of test_case_keys) {
|
|
455
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
456
|
+
projectKey: project_key,
|
|
457
|
+
testCaseKey,
|
|
458
|
+
testCycleKey: cycleKey,
|
|
459
|
+
statusName: 'Not Executed',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
464
462
|
}
|
|
465
463
|
return {
|
|
466
464
|
content: [{
|
|
@@ -734,24 +732,24 @@ export class ZephyrToolHandlers {
|
|
|
734
732
|
throw new McpError(ErrorCode.InvalidRequest, 'add_test_cases_to_run is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not provide an endpoint to modify test runs after creation.');
|
|
735
733
|
}
|
|
736
734
|
const { test_run_key, test_case_keys } = args;
|
|
735
|
+
// Derive project key from the test run key (e.g. PROJ-R123 → PROJ)
|
|
736
|
+
const project_key = test_run_key.split('-')[0];
|
|
737
737
|
try {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
};
|
|
738
|
+
for (const testCaseKey of test_case_keys) {
|
|
739
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
740
|
+
projectKey: project_key,
|
|
741
|
+
testCaseKey,
|
|
742
|
+
testCycleKey: test_run_key,
|
|
743
|
+
statusName: 'Not Executed',
|
|
744
|
+
});
|
|
746
745
|
}
|
|
746
|
+
return {
|
|
747
|
+
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
748
|
+
};
|
|
747
749
|
}
|
|
748
750
|
catch (error) {
|
|
749
751
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
750
752
|
}
|
|
751
|
-
return {
|
|
752
|
-
content: [{ type: 'text', text: 'An unexpected error occurred.' }],
|
|
753
|
-
isError: true,
|
|
754
|
-
};
|
|
755
753
|
}
|
|
756
754
|
formatError(error) {
|
|
757
755
|
if (error instanceof Error && 'response' in error) {
|
package/build/utils.js
CHANGED
|
@@ -30,28 +30,48 @@ export async function resolveFolderIdByPath(axiosInstance, projectKey, folderPat
|
|
|
30
30
|
export function convertToGherkin(bddContent) {
|
|
31
31
|
const bddLines = [];
|
|
32
32
|
const lines = bddContent.split('\n');
|
|
33
|
+
// Bold-markdown step keywords (e.g. **Given**, **When**, etc.)
|
|
34
|
+
const boldStepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
35
|
+
// Plain step keyword prefixes
|
|
36
|
+
const stepKeywords = ['Given ', 'When ', 'Then ', 'And ', 'But '];
|
|
37
|
+
// Zephyr Scale Cloud only accepts steps and table rows — Feature:/Scenario: wrappers
|
|
38
|
+
// cause a 400 "Invalid Gherkin script" error and must be stripped.
|
|
39
|
+
const strippedPrefixes = [
|
|
40
|
+
'Feature:',
|
|
41
|
+
'Background:',
|
|
42
|
+
'Scenario Outline:',
|
|
43
|
+
'Scenario:',
|
|
44
|
+
'Examples:',
|
|
45
|
+
];
|
|
33
46
|
for (const line of lines) {
|
|
34
47
|
const trimmedLine = line.trim();
|
|
35
48
|
if (!trimmedLine || trimmedLine.startsWith('---'))
|
|
36
49
|
continue;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
// Strip structural Gherkin keywords — not accepted by the Zephyr Scale Cloud testscript API
|
|
51
|
+
if (strippedPrefixes.some(p => trimmedLine.startsWith(p)))
|
|
52
|
+
continue;
|
|
53
|
+
// Convert **Keyword** markdown bold to plain Gherkin keyword
|
|
54
|
+
let matchedBold = false;
|
|
55
|
+
for (const kw of boldStepKeywords) {
|
|
56
|
+
if (trimmedLine.startsWith(`**${kw}**`)) {
|
|
57
|
+
bddLines.push(`${kw} ${trimmedLine.replace(`**${kw}**`, '').trim()}`);
|
|
58
|
+
matchedBold = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
45
61
|
}
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
if (matchedBold)
|
|
63
|
+
continue;
|
|
64
|
+
// Plain step keywords — pass through unchanged
|
|
65
|
+
if (stepKeywords.some(k => trimmedLine.startsWith(k))) {
|
|
66
|
+
bddLines.push(trimmedLine);
|
|
67
|
+
continue;
|
|
48
68
|
}
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
// Table rows — pass through unchanged
|
|
70
|
+
if (trimmedLine.startsWith('|')) {
|
|
51
71
|
bddLines.push(trimmedLine);
|
|
52
72
|
}
|
|
53
73
|
}
|
|
54
|
-
return bddLines.
|
|
74
|
+
return bddLines.join('\n');
|
|
55
75
|
}
|
|
56
76
|
export const customPriorityMapping = {
|
|
57
77
|
'High': 'P0',
|
|
@@ -111,8 +131,11 @@ export function createJiraConfig() {
|
|
|
111
131
|
}
|
|
112
132
|
const type = detectJiraType(jiraBaseUrl);
|
|
113
133
|
const apiEndpoints = getApiEndpoints(type);
|
|
134
|
+
// Cloud: use ZEPHYR_API_BASE_URL if set (e.g. for EU: https://eu.api.zephyrscale.smartbear.com/v2), else default US
|
|
135
|
+
const defaultCloudBaseUrl = 'https://api.zephyrscale.smartbear.com/v2';
|
|
136
|
+
const cloudBaseUrl = process.env.ZEPHYR_API_BASE_URL?.trim();
|
|
114
137
|
const baseUrl = type === 'cloud'
|
|
115
|
-
?
|
|
138
|
+
? (cloudBaseUrl || defaultCloudBaseUrl)
|
|
116
139
|
: jiraBaseUrl;
|
|
117
140
|
const authHeaders = {
|
|
118
141
|
'Authorization': `Bearer ${apiKey}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@modelcontextprotocol/sdk": "0.6.0",
|
|
60
|
-
"axios": "^1.
|
|
60
|
+
"axios": "^1.15.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^20.11.24",
|
package/src/tool-handlers.ts
CHANGED
|
@@ -32,6 +32,11 @@ export class ZephyrToolHandlers {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async createTestCase(args: TestCaseArgs) {
|
|
35
|
+
// Some MCP clients (e.g. Claude Code) pass nested object parameters as JSON strings.
|
|
36
|
+
// Parse test_script if it arrived as a string.
|
|
37
|
+
if (typeof (args as any).test_script === 'string') {
|
|
38
|
+
try { (args as any).test_script = JSON.parse((args as any).test_script); } catch {}
|
|
39
|
+
}
|
|
35
40
|
if (this.jiraConfig.type === 'cloud') {
|
|
36
41
|
return this.createTestCaseCloud(args);
|
|
37
42
|
}
|
|
@@ -185,47 +190,28 @@ export class ZephyrToolHandlers {
|
|
|
185
190
|
private async updateTestCaseBddCloud(args: UpdateBddArgs) {
|
|
186
191
|
const { test_case_key, bdd_content, name } = args;
|
|
187
192
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
191
|
-
const tc = getResponse.data;
|
|
192
|
-
|
|
193
|
-
// Cloud v2 PUT requires: id, key, name, project, priority, status
|
|
194
|
-
const requiredFields = ['id', 'key', 'name', 'project', 'priority', 'status'];
|
|
195
|
-
for (const field of requiredFields) {
|
|
196
|
-
if (tc[field] === undefined || tc[field] === null) {
|
|
197
|
-
throw new McpError(ErrorCode.InternalError,
|
|
198
|
-
`Existing test case is missing required field '${field}' needed for Cloud v2 update.`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const putPayload: any = {
|
|
203
|
-
id: tc.id,
|
|
204
|
-
key: tc.key,
|
|
205
|
-
name: typeof name === 'string' && name.trim().length > 0 ? name : tc.name,
|
|
206
|
-
project: tc.project,
|
|
207
|
-
priority: tc.priority,
|
|
208
|
-
status: tc.status,
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// Preserve optional fields
|
|
212
|
-
for (const field of ['objective', 'precondition', 'estimatedTime', 'folder', 'component', 'owner']) {
|
|
213
|
-
if (tc[field] !== undefined) putPayload[field] = tc[field];
|
|
214
|
-
}
|
|
215
|
-
if (Array.isArray(tc.labels)) putPayload.labels = tc.labels;
|
|
216
|
-
if (tc.customFields) putPayload.customFields = tc.customFields;
|
|
217
|
-
|
|
218
|
-
// Step 2: PUT to update metadata
|
|
219
|
-
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
193
|
+
const converted = convertToGherkin(bdd_content);
|
|
194
|
+
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
220
195
|
|
|
221
|
-
|
|
222
|
-
const converted = convertToGherkin(bdd_content);
|
|
223
|
-
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
196
|
+
try {
|
|
224
197
|
await this.axiosInstance.post(
|
|
225
198
|
`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`,
|
|
226
199
|
{ type: 'bdd', text: finalText }
|
|
227
200
|
);
|
|
228
201
|
|
|
202
|
+
// Only fetch and PUT metadata when the caller also wants to rename the test case
|
|
203
|
+
if (typeof name === 'string' && name.trim().length > 0) {
|
|
204
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
205
|
+
const tc = getResponse.data;
|
|
206
|
+
const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
207
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
|
|
208
|
+
projectKey,
|
|
209
|
+
name,
|
|
210
|
+
status: tc.status,
|
|
211
|
+
priority: tc.priority,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
229
215
|
return {
|
|
230
216
|
content: [{
|
|
231
217
|
type: 'text',
|
|
@@ -249,8 +235,9 @@ export class ZephyrToolHandlers {
|
|
|
249
235
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
250
236
|
|
|
251
237
|
const payload: any = {};
|
|
238
|
+
const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
252
239
|
const requiredFields: Array<[string, any]> = [
|
|
253
|
-
['projectKey',
|
|
240
|
+
['projectKey', projectKey],
|
|
254
241
|
['name', testCaseData.name],
|
|
255
242
|
['status', testCaseData.status],
|
|
256
243
|
['priority', testCaseData.priority]
|
|
@@ -513,12 +500,16 @@ export class ZephyrToolHandlers {
|
|
|
513
500
|
|
|
514
501
|
const cycleKey = response.data.key || 'Unknown';
|
|
515
502
|
|
|
516
|
-
// Step 2: add test cases
|
|
503
|
+
// Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
|
|
517
504
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
505
|
+
for (const testCaseKey of test_case_keys) {
|
|
506
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
507
|
+
projectKey: project_key,
|
|
508
|
+
testCaseKey,
|
|
509
|
+
testCycleKey: cycleKey,
|
|
510
|
+
statusName: 'Not Executed',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
522
513
|
}
|
|
523
514
|
|
|
524
515
|
return {
|
|
@@ -813,28 +804,24 @@ export class ZephyrToolHandlers {
|
|
|
813
804
|
|
|
814
805
|
const { test_run_key, test_case_keys } = args;
|
|
815
806
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
items: test_case_keys.map(key => ({ testCaseKey: key }))
|
|
819
|
-
};
|
|
820
|
-
const response = await this.axiosInstance.post(
|
|
821
|
-
`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`,
|
|
822
|
-
payload
|
|
823
|
-
);
|
|
807
|
+
// Derive project key from the test run key (e.g. PROJ-R123 → PROJ)
|
|
808
|
+
const project_key = test_run_key.split('-')[0];
|
|
824
809
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
810
|
+
try {
|
|
811
|
+
for (const testCaseKey of test_case_keys) {
|
|
812
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
813
|
+
projectKey: project_key,
|
|
814
|
+
testCaseKey,
|
|
815
|
+
testCycleKey: test_run_key,
|
|
816
|
+
statusName: 'Not Executed',
|
|
817
|
+
});
|
|
829
818
|
}
|
|
819
|
+
return {
|
|
820
|
+
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
821
|
+
};
|
|
830
822
|
} catch (error) {
|
|
831
823
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
832
824
|
}
|
|
833
|
-
|
|
834
|
-
return {
|
|
835
|
-
content: [{ type: 'text', text: 'An unexpected error occurred.' }],
|
|
836
|
-
isError: true,
|
|
837
|
-
};
|
|
838
825
|
}
|
|
839
826
|
|
|
840
827
|
private formatError(error: unknown): string {
|
package/src/utils.ts
CHANGED
|
@@ -43,28 +43,53 @@ export async function resolveFolderIdByPath(
|
|
|
43
43
|
export function convertToGherkin(bddContent: string): string {
|
|
44
44
|
const bddLines: string[] = [];
|
|
45
45
|
const lines = bddContent.split('\n');
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
// Bold-markdown step keywords (e.g. **Given**, **When**, etc.)
|
|
48
|
+
const boldStepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
49
|
+
// Plain step keyword prefixes
|
|
50
|
+
const stepKeywords = ['Given ', 'When ', 'Then ', 'And ', 'But '];
|
|
51
|
+
// Zephyr Scale Cloud only accepts steps and table rows — Feature:/Scenario: wrappers
|
|
52
|
+
// cause a 400 "Invalid Gherkin script" error and must be stripped.
|
|
53
|
+
const strippedPrefixes = [
|
|
54
|
+
'Feature:',
|
|
55
|
+
'Background:',
|
|
56
|
+
'Scenario Outline:',
|
|
57
|
+
'Scenario:',
|
|
58
|
+
'Examples:',
|
|
59
|
+
];
|
|
60
|
+
|
|
47
61
|
for (const line of lines) {
|
|
48
62
|
const trimmedLine = line.trim();
|
|
49
63
|
if (!trimmedLine || trimmedLine.startsWith('---')) continue;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
|
|
65
|
+
// Strip structural Gherkin keywords — not accepted by the Zephyr Scale Cloud testscript API
|
|
66
|
+
if (strippedPrefixes.some(p => trimmedLine.startsWith(p))) continue;
|
|
67
|
+
|
|
68
|
+
// Convert **Keyword** markdown bold to plain Gherkin keyword
|
|
69
|
+
let matchedBold = false;
|
|
70
|
+
for (const kw of boldStepKeywords) {
|
|
71
|
+
if (trimmedLine.startsWith(`**${kw}**`)) {
|
|
72
|
+
bddLines.push(`${kw} ${trimmedLine.replace(`**${kw}**`, '').trim()}`);
|
|
73
|
+
matchedBold = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (matchedBold) continue;
|
|
78
|
+
|
|
79
|
+
// Plain step keywords — pass through unchanged
|
|
80
|
+
if (stepKeywords.some(k => trimmedLine.startsWith(k))) {
|
|
81
|
+
bddLines.push(trimmedLine);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Table rows — pass through unchanged
|
|
86
|
+
if (trimmedLine.startsWith('|')) {
|
|
61
87
|
bddLines.push(trimmedLine);
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
|
-
|
|
65
|
-
return bddLines.length > 0 ? ' ' + bddLines.join('\n ') : '';
|
|
66
|
-
}
|
|
67
90
|
|
|
91
|
+
return bddLines.join('\n');
|
|
92
|
+
}
|
|
68
93
|
export const customPriorityMapping: { [key: string]: string } = {
|
|
69
94
|
'High': 'P0',
|
|
70
95
|
'Normal': 'P1',
|
|
@@ -128,9 +153,12 @@ export function createJiraConfig() {
|
|
|
128
153
|
|
|
129
154
|
const type = detectJiraType(jiraBaseUrl);
|
|
130
155
|
const apiEndpoints = getApiEndpoints(type);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
|
|
157
|
+
// Cloud: use ZEPHYR_API_BASE_URL if set (e.g. for EU: https://eu.api.zephyrscale.smartbear.com/v2), else default US
|
|
158
|
+
const defaultCloudBaseUrl = 'https://api.zephyrscale.smartbear.com/v2';
|
|
159
|
+
const cloudBaseUrl = process.env.ZEPHYR_API_BASE_URL?.trim();
|
|
160
|
+
const baseUrl = type === 'cloud'
|
|
161
|
+
? (cloudBaseUrl || defaultCloudBaseUrl)
|
|
134
162
|
: jiraBaseUrl;
|
|
135
163
|
|
|
136
164
|
const authHeaders = {
|