@smartbear/mcp 0.18.3 → 0.19.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/dist/package.json.js +1 -1
- package/dist/pactflow/client/ai.js +2 -1
- package/dist/reflect/client.js +25 -8
- package/dist/reflect/config/constants.js +7 -1
- package/dist/reflect/tool/recording/connect-to-session.js +1 -1
- package/dist/reflect/tool/recording/get-screenshot.js +12 -3
- package/dist/reflect/tool/tests/list-segments.js +4 -6
- package/dist/reflect/websocket-manager.js +5 -5
- package/dist/swagger/client/api.js +5 -2
- package/dist/swagger/client/registry-types.js +4 -1
- package/dist/swagger/client/tools.js +1 -1
- package/dist/zephyr/common/rest-api-schemas.js +8 -28
- package/dist/zephyr/tool/test-case/update-test-case.js +23 -1
- package/dist/zephyr/tool/test-cycle/update-test-cycle.js +17 -2
- package/package.json +1 -1
package/dist/package.json.js
CHANGED
package/dist/reflect/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
3
|
import { getRequestHeader } from "../common/request-context.js";
|
|
4
4
|
import { ToolError } from "../common/tools.js";
|
|
5
|
-
import { API_KEY_HEADER } from "./config/constants.js";
|
|
5
|
+
import { API_KEY_HEADER, REFLECT_API_TOKEN_HEADER, AUTHORIZATION_HEADER } from "./config/constants.js";
|
|
6
6
|
import { SapTest } from "./prompt/sap-test.js";
|
|
7
7
|
import { AddPromptStep } from "./tool/recording/add-prompt-step.js";
|
|
8
8
|
import { AddSegment } from "./tool/recording/add-segment.js";
|
|
@@ -34,7 +34,7 @@ class ReflectClient {
|
|
|
34
34
|
this._apiToken = config.api_token;
|
|
35
35
|
}
|
|
36
36
|
getAuthToken() {
|
|
37
|
-
const contextHeader = getRequestHeader(
|
|
37
|
+
const contextHeader = getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER) || getRequestHeader(AUTHORIZATION_HEADER);
|
|
38
38
|
if (contextHeader) {
|
|
39
39
|
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
40
40
|
if (token.startsWith("Bearer ")) {
|
|
@@ -47,16 +47,30 @@ class ReflectClient {
|
|
|
47
47
|
isConfigured() {
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
isOAuthRequest() {
|
|
51
|
+
if (getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const authHeader = getRequestHeader(AUTHORIZATION_HEADER);
|
|
55
|
+
if (!authHeader) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
59
|
+
return headerValue.toLowerCase().startsWith("bearer ");
|
|
52
60
|
}
|
|
53
|
-
|
|
61
|
+
getAuthHeader() {
|
|
54
62
|
const token = this.getAuthToken();
|
|
55
63
|
if (!token) {
|
|
56
64
|
throw new Error("Reflect API token not found");
|
|
57
65
|
}
|
|
66
|
+
if (this.isOAuthRequest()) {
|
|
67
|
+
return { Authorization: `Bearer ${token}` };
|
|
68
|
+
}
|
|
69
|
+
return { [API_KEY_HEADER]: token };
|
|
70
|
+
}
|
|
71
|
+
getHeaders() {
|
|
58
72
|
return {
|
|
59
|
-
|
|
73
|
+
...this.getAuthHeader(),
|
|
60
74
|
"Content-Type": "application/json",
|
|
61
75
|
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
62
76
|
};
|
|
@@ -99,7 +113,7 @@ class ReflectClient {
|
|
|
99
113
|
this.mcpSessionConnections.delete(mcpSessionId);
|
|
100
114
|
}
|
|
101
115
|
async registerTools(register, _getInput) {
|
|
102
|
-
const
|
|
116
|
+
const apiOnlyTools = [
|
|
103
117
|
new ListSuites(this),
|
|
104
118
|
new ListSuiteExecutions(this),
|
|
105
119
|
new GetSuiteExecutionStatus(this),
|
|
@@ -107,7 +121,9 @@ class ReflectClient {
|
|
|
107
121
|
new CancelSuiteExecution(this),
|
|
108
122
|
new ListTests(this),
|
|
109
123
|
new RunTest(this),
|
|
110
|
-
new GetTestStatus(this)
|
|
124
|
+
new GetTestStatus(this)
|
|
125
|
+
];
|
|
126
|
+
const oAuthAndAPISupportedTools = [
|
|
111
127
|
new ListSegments(this),
|
|
112
128
|
new ConnectToSession(this),
|
|
113
129
|
new AddPromptStep(this),
|
|
@@ -115,6 +131,7 @@ class ReflectClient {
|
|
|
115
131
|
new DeletePreviousStep(this),
|
|
116
132
|
new AddSegment(this)
|
|
117
133
|
];
|
|
134
|
+
const tools = this.isOAuthRequest() ? oAuthAndAPISupportedTools : [...oAuthAndAPISupportedTools, ...apiOnlyTools];
|
|
118
135
|
for (const tool of tools) {
|
|
119
136
|
register(tool.specification, tool.handle);
|
|
120
137
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
const API_KEY_HEADER = "X-API-KEY";
|
|
2
|
+
const REFLECT_API_TOKEN_HEADER = "Reflect-Api-Token";
|
|
3
|
+
const AUTHORIZATION_HEADER = "Authorization";
|
|
2
4
|
const API_HOSTNAME = "api.reflect.run";
|
|
3
5
|
const WEBSOCKET_HOSTNAME = "recording.us-east-1.reflect.run";
|
|
6
|
+
const WEB_APP_HOSTNAME = "app.reflect.run";
|
|
4
7
|
export {
|
|
5
8
|
API_HOSTNAME,
|
|
6
9
|
API_KEY_HEADER,
|
|
7
|
-
|
|
10
|
+
AUTHORIZATION_HEADER,
|
|
11
|
+
REFLECT_API_TOKEN_HEADER,
|
|
12
|
+
WEBSOCKET_HOSTNAME,
|
|
13
|
+
WEB_APP_HOSTNAME
|
|
8
14
|
};
|
|
@@ -13,17 +13,25 @@ class GetScreenshot extends Tool {
|
|
|
13
13
|
type: z.string(),
|
|
14
14
|
description: "The ID of the Reflect recording session",
|
|
15
15
|
required: true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "format",
|
|
19
|
+
type: z.enum(["png", "jpeg"]),
|
|
20
|
+
description: "The image format for the screenshot (png or jpeg)",
|
|
21
|
+
required: false
|
|
16
22
|
}
|
|
17
23
|
]
|
|
18
24
|
};
|
|
19
25
|
handle = async (args) => {
|
|
20
|
-
const { sessionId } = args;
|
|
26
|
+
const { sessionId, format } = args;
|
|
21
27
|
if (!sessionId) throw new ToolError("sessionId argument is required");
|
|
28
|
+
const imageFormat = format ?? "png";
|
|
22
29
|
const wsManager = this.client.getConnectedSession(sessionId);
|
|
23
30
|
const id = randomUUID();
|
|
24
31
|
const responsePromise = wsManager.waitForResponse(id);
|
|
25
32
|
await wsManager.sendMcpMessage({
|
|
26
33
|
type: "mcp:get-screenshot",
|
|
34
|
+
format: imageFormat,
|
|
27
35
|
id
|
|
28
36
|
});
|
|
29
37
|
const response = await responsePromise;
|
|
@@ -36,14 +44,15 @@ class GetScreenshot extends Tool {
|
|
|
36
44
|
{
|
|
37
45
|
type: "image",
|
|
38
46
|
data: imageBase64,
|
|
39
|
-
mimeType:
|
|
47
|
+
mimeType: `image/${imageFormat}`
|
|
40
48
|
},
|
|
41
49
|
{
|
|
42
50
|
type: "text",
|
|
43
51
|
text: JSON.stringify({
|
|
44
52
|
success: true,
|
|
45
53
|
message: "Screenshot captured",
|
|
46
|
-
state
|
|
54
|
+
state,
|
|
55
|
+
format: imageFormat
|
|
47
56
|
})
|
|
48
57
|
}
|
|
49
58
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { Tool, ToolError } from "../../../common/tools.js";
|
|
3
|
-
import {
|
|
3
|
+
import { WEB_APP_HOSTNAME, API_HOSTNAME } from "../../config/constants.js";
|
|
4
4
|
class ListSegments extends Tool {
|
|
5
5
|
specification = {
|
|
6
6
|
title: "List Segments",
|
|
@@ -34,13 +34,11 @@ class ListSegments extends Tool {
|
|
|
34
34
|
offset = 0,
|
|
35
35
|
limit = 25
|
|
36
36
|
} = args;
|
|
37
|
-
const
|
|
37
|
+
const urlPath = this.client.isOAuthRequest() ? `https://${WEB_APP_HOSTNAME}/api/mcp` : `https://${API_HOSTNAME}/v1`;
|
|
38
|
+
const url = `${urlPath}/segments?type=${platform}&offset=${offset}&limit=${limit}`;
|
|
38
39
|
const response = await fetch(url, {
|
|
39
40
|
method: "GET",
|
|
40
|
-
headers:
|
|
41
|
-
[API_KEY_HEADER]: this.client.getApiToken(),
|
|
42
|
-
"Content-Type": "application/json"
|
|
43
|
-
}
|
|
41
|
+
headers: this.client.getHeaders()
|
|
44
42
|
});
|
|
45
43
|
if (!response.ok) {
|
|
46
44
|
throw new ToolError(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { WEBSOCKET_HOSTNAME
|
|
2
|
+
import { WEBSOCKET_HOSTNAME } from "./config/constants.js";
|
|
3
3
|
class WebSocketManager {
|
|
4
4
|
sessionId;
|
|
5
|
-
|
|
5
|
+
authHeader;
|
|
6
6
|
mcpSocket = null;
|
|
7
7
|
pendingResponses = /* @__PURE__ */ new Map();
|
|
8
|
-
constructor(sessionId,
|
|
8
|
+
constructor(sessionId, authHeader) {
|
|
9
9
|
this.sessionId = sessionId;
|
|
10
|
-
this.
|
|
10
|
+
this.authHeader = authHeader;
|
|
11
11
|
}
|
|
12
12
|
async connect() {
|
|
13
13
|
if (this.mcpSocket?.readyState === WebSocket.OPEN) {
|
|
@@ -26,7 +26,7 @@ class WebSocketManager {
|
|
|
26
26
|
const url = `wss://${WEBSOCKET_HOSTNAME}/websocket/v2/recordings/${this.sessionId}/topics/mcp?sid=${this.sessionId}`;
|
|
27
27
|
this.mcpSocket = new WebSocket(url, {
|
|
28
28
|
headers: {
|
|
29
|
-
|
|
29
|
+
...this.authHeader
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
32
|
this.mcpSocket.on("open", () => {
|
|
@@ -623,15 +623,18 @@ class SwaggerAPI {
|
|
|
623
623
|
}
|
|
624
624
|
/**
|
|
625
625
|
* Standardize and fix an API definition using AI
|
|
626
|
-
* @param params Parameters including owner, API name, and
|
|
626
|
+
* @param params Parameters including owner, API name, version, and optional newVersion
|
|
627
627
|
* @returns Standardization response with status and fixed definition
|
|
628
628
|
*/
|
|
629
629
|
async standardizeApi(params) {
|
|
630
|
+
const searchParams = new URLSearchParams();
|
|
631
|
+
if (params.newVersion) searchParams.set("newVersion", params.newVersion);
|
|
632
|
+
const queryString = searchParams.toString();
|
|
630
633
|
const url = `${this.config.registryBasePath}/apis/${encodeURIComponent(
|
|
631
634
|
params.owner
|
|
632
635
|
)}/${encodeURIComponent(params.api)}/${encodeURIComponent(
|
|
633
636
|
params.version
|
|
634
|
-
)}/standardize`;
|
|
637
|
+
)}/standardize${queryString ? `?${queryString}` : ""}`;
|
|
635
638
|
const response = await fetch(url, {
|
|
636
639
|
method: "POST",
|
|
637
640
|
headers: this.headers
|
|
@@ -57,7 +57,10 @@ const CreateApiFromPromptParamsSchema = z.object({
|
|
|
57
57
|
const StandardizeApiParamsSchema = z.object({
|
|
58
58
|
owner: z.string().describe("API owner (organization or user, case-sensitive)"),
|
|
59
59
|
api: z.string().describe("API name (case-sensitive)"),
|
|
60
|
-
version: z.string().describe("Version identifier")
|
|
60
|
+
version: z.string().describe("Version identifier"),
|
|
61
|
+
newVersion: z.string().optional().describe(
|
|
62
|
+
"The version to save the fixed definition as (e.g. '1.0.1'). Omitting this will overwrite the current version — prefer providing a patch bump (e.g. '1.0.0' → '1.0.1') unless the user specifies otherwise."
|
|
63
|
+
)
|
|
61
64
|
});
|
|
62
65
|
export {
|
|
63
66
|
ApiDefinitionParamsSchema,
|
|
@@ -140,7 +140,7 @@ const TOOLS = [
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
title: "Standardize API",
|
|
143
|
-
summary: "Standardize and fix an API definition using AI to ensure compliance with governance policies. Scans the API definition for standardization errors and automatically fixes them using SmartBear AI.
|
|
143
|
+
summary: "Standardize and fix an API definition using AI to ensure compliance with governance policies. Scans the API definition for standardization errors and automatically fixes them using SmartBear AI. Optionally provide 'newVersion' (e.g. patch bump '1.0.0' → '1.0.1') to save the fixed definition as a new version — omitting it will overwrite the current version. Returns the number of errors found and the fixed definition if successful. Use this tool when users ask to standardize, fix, govern, or ensure governance compliance of APIs.",
|
|
144
144
|
inputSchema: StandardizeApiParamsSchema,
|
|
145
145
|
handler: "standardizeApi"
|
|
146
146
|
}
|
|
@@ -411,10 +411,7 @@ const UpdateTestCaseBody = zod.object({
|
|
|
411
411
|
precondition: zod.string().nullish().describe("Any conditions that need to be met."),
|
|
412
412
|
estimatedTime: zod.number().min(updateTestCaseBodyEstimatedTimeMin).nullish().describe("Estimated duration in milliseconds."),
|
|
413
413
|
labels: zod.array(zod.string()).max(updateTestCaseBodyLabelsMax).optional().describe("Array of labels associated to this entity."),
|
|
414
|
-
component: zod.
|
|
415
|
-
id: zod.number().min(1).describe("The ID of the entity"),
|
|
416
|
-
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
417
|
-
}).strict().nullish().describe("ID and link to the Jira component resource."),
|
|
414
|
+
component: zod.number().min(1).nullable().optional().describe("ID and link to the Jira component resource."),
|
|
418
415
|
priority: zod.object({
|
|
419
416
|
id: zod.number().min(1).describe("The ID of the entity"),
|
|
420
417
|
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
@@ -423,16 +420,10 @@ const UpdateTestCaseBody = zod.object({
|
|
|
423
420
|
id: zod.number().min(1).describe("The ID of the entity"),
|
|
424
421
|
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
425
422
|
}).strict().describe("ID and link to the status resource."),
|
|
426
|
-
folder: zod.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
owner: zod.object({
|
|
431
|
-
accountId: zod.string().regex(updateTestCaseBodyOwnerAccountIdRegExp).nullable().describe("Atlassian Account ID of the Jira user."),
|
|
432
|
-
self: zod.string().url().optional().describe(
|
|
433
|
-
"The Jira REST API endpoint to get the full representation of the Jira user."
|
|
434
|
-
)
|
|
435
|
-
}).strict().nullish(),
|
|
423
|
+
folder: zod.number().min(1).nullable().optional().describe(
|
|
424
|
+
"The ID of the folder, to remove folder set it's value to null"
|
|
425
|
+
),
|
|
426
|
+
owner: zod.string().regex(updateTestCaseBodyOwnerAccountIdRegExp).nullable().describe("Atlassian Account ID of the Jira user."),
|
|
436
427
|
customFields: zod.record(zod.string(), zod.unknown()).optional().describe(
|
|
437
428
|
"Multi-line text fields support HTML and should denote new lines with the \\<br\\> tag.\nDates should be in the format 'yyyy-MM-dd'.\nUsers should have values of Jira User Account IDs.\n"
|
|
438
429
|
)
|
|
@@ -1110,20 +1101,14 @@ const UpdateTestCycleBody = zod.object({
|
|
|
1110
1101
|
id: zod.number().min(1).describe("The ID of the entity"),
|
|
1111
1102
|
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
1112
1103
|
}).strict().describe("ID and link relative to Zephyr project."),
|
|
1113
|
-
jiraProjectVersion: zod.
|
|
1114
|
-
id: zod.number().min(1).describe("The ID of the entity"),
|
|
1115
|
-
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
1116
|
-
}).strict().nullish().describe(
|
|
1104
|
+
jiraProjectVersion: zod.number().min(1).nullable().optional().describe(
|
|
1117
1105
|
"ID and Link to fetch information about Jira Project version. Relates to 'Version' or 'Releases' in Jira projects."
|
|
1118
1106
|
),
|
|
1119
1107
|
status: zod.object({
|
|
1120
1108
|
id: zod.number().min(1).describe("The ID of the entity"),
|
|
1121
1109
|
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
1122
1110
|
}).strict().describe("ID and link to the status resource."),
|
|
1123
|
-
folder: zod.
|
|
1124
|
-
id: zod.number().min(1).describe("The ID of the entity"),
|
|
1125
|
-
self: zod.string().url().optional().describe("The REST API endpoint to get more resource details.")
|
|
1126
|
-
}).strict().nullish().describe("ID and link to the folder resource."),
|
|
1111
|
+
folder: zod.number().min(1).nullable().optional().describe("ID and link to the folder resource."),
|
|
1127
1112
|
description: zod.string().nullish().describe("Description outlining the scope."),
|
|
1128
1113
|
plannedStartDate: zod.string().datetime({}).nullish().describe(
|
|
1129
1114
|
"Planned start date of the test cycle. This field cannot be blank. Setting it as null or excluding it from the request will leave the field values unchanged. ISO 8601 Format (i.e., yyyy-MM-dd'T'HH:mm:ss'Z')"
|
|
@@ -1131,12 +1116,7 @@ const UpdateTestCycleBody = zod.object({
|
|
|
1131
1116
|
plannedEndDate: zod.string().datetime({}).nullish().describe(
|
|
1132
1117
|
"The planned end date of the test cycle. This field cannot be blank. Setting it as null or excluding it from the request will leave the field values unchanged. ISO 8601 Format (i.e., yyyy-MM-dd'T'HH:mm:ss'Z')"
|
|
1133
1118
|
),
|
|
1134
|
-
owner: zod.
|
|
1135
|
-
accountId: zod.string().regex(updateTestCycleBodyOwnerAccountIdRegExp).nullable().describe("Atlassian Account ID of the Jira user."),
|
|
1136
|
-
self: zod.string().url().optional().describe(
|
|
1137
|
-
"The Jira REST API endpoint to get the full representation of the Jira user."
|
|
1138
|
-
)
|
|
1139
|
-
}).strict().nullish(),
|
|
1119
|
+
owner: zod.string().regex(updateTestCycleBodyOwnerAccountIdRegExp).nullable().describe("Atlassian Account ID of the Jira user."),
|
|
1140
1120
|
customFields: zod.record(zod.string(), zod.unknown()).optional().describe(
|
|
1141
1121
|
"Multi-line text fields support HTML and should denote new lines with the \\<br\\> tag.\nDates should be in the format 'yyyy-MM-dd'.\nUsers should have values of Jira User Account IDs.\n"
|
|
1142
1122
|
)
|
|
@@ -57,6 +57,14 @@ class UpdateTestCase extends Tool {
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
expectedOutput: "The test case should be updated, but no output is expected."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: "Remove test case from folder",
|
|
63
|
+
parameters: {
|
|
64
|
+
testCaseKey: "SA-T15",
|
|
65
|
+
folder: null
|
|
66
|
+
},
|
|
67
|
+
expectedOutput: "The test case should be updated, but no output is expected."
|
|
60
68
|
}
|
|
61
69
|
]
|
|
62
70
|
};
|
|
@@ -64,7 +72,21 @@ class UpdateTestCase extends Tool {
|
|
|
64
72
|
const parsed = UpdateTestCaseParams.and(UpdateTestCaseBody.partial()).parse(
|
|
65
73
|
args
|
|
66
74
|
);
|
|
67
|
-
const { testCaseKey, ...
|
|
75
|
+
const { testCaseKey, ...rawUpdates } = parsed;
|
|
76
|
+
const nullValuesObject = {};
|
|
77
|
+
if (rawUpdates.folder) {
|
|
78
|
+
nullValuesObject.folder = { id: rawUpdates.folder };
|
|
79
|
+
}
|
|
80
|
+
if (rawUpdates.owner) {
|
|
81
|
+
nullValuesObject.owner = { accountId: rawUpdates.owner };
|
|
82
|
+
}
|
|
83
|
+
if (rawUpdates.component) {
|
|
84
|
+
nullValuesObject.component = { id: rawUpdates.component };
|
|
85
|
+
}
|
|
86
|
+
const updates = {
|
|
87
|
+
...rawUpdates,
|
|
88
|
+
...nullValuesObject
|
|
89
|
+
};
|
|
68
90
|
const existingTestCase = await this.client.getApiClient().get(`/testcases/${testCaseKey}`);
|
|
69
91
|
const mergedBody = deepMerge(existingTestCase, updates);
|
|
70
92
|
delete mergedBody.createdOn;
|
|
@@ -72,8 +72,23 @@ class UpdateTestCycle extends Tool {
|
|
|
72
72
|
const parsed = UpdateTestCycleParams.and(
|
|
73
73
|
UpdateTestCycleBody.partial()
|
|
74
74
|
).parse(args);
|
|
75
|
-
const { testCycleIdOrKey, ...
|
|
76
|
-
const
|
|
75
|
+
const { testCycleIdOrKey, ...rawUpdates } = parsed;
|
|
76
|
+
const nullValuesObject = {};
|
|
77
|
+
if (rawUpdates.folder) {
|
|
78
|
+
nullValuesObject.folder = { id: rawUpdates.folder };
|
|
79
|
+
}
|
|
80
|
+
if (rawUpdates.owner) {
|
|
81
|
+
nullValuesObject.owner = { accountId: rawUpdates.owner };
|
|
82
|
+
}
|
|
83
|
+
if (rawUpdates.jiraProjectVersion) {
|
|
84
|
+
nullValuesObject.jiraProjectVersion = {
|
|
85
|
+
id: rawUpdates.jiraProjectVersion
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const updates = {
|
|
89
|
+
...rawUpdates,
|
|
90
|
+
...nullValuesObject
|
|
91
|
+
};
|
|
77
92
|
if (updates.plannedStartDate === null) delete updates.plannedStartDate;
|
|
78
93
|
if (updates.plannedEndDate === null) delete updates.plannedEndDate;
|
|
79
94
|
const existingTestCycle = await this.client.getApiClient().get(`/testcycles/${testCycleIdOrKey}`);
|