Skip to content

Commit f2680df

Browse files
committed
refactor(testmanagement): address updateTestRun PR review
1 parent e7cfff0 commit f2680df

3 files changed

Lines changed: 105 additions & 22 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ As of now we support 20 tools.
334334
List all test runs from the 'Shopping App' project that were executed last week and are currently marked in-progress
335335
```
336336
337-
6. `updateTestRun` — Partially update a test run (status, tags, notes, associated test cases).
337+
6. `updateTestRun` — Update a test run's name/state and/or add test cases to it.
338338
**Prompt example**
339339
340340
```text

src/tools/testmanagement-utils/update-testrun.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { getTMBaseURL } from "../../lib/tm-base-url.js";
1010
* Selection of test cases (with optional configurations) to add.
1111
*/
1212
const TestCaseSelectionSchema = z.object({
13-
test_case_ids: z.array(z.string()).describe("Test case IDs, e.g. TC-123"),
13+
test_case_ids: z
14+
.array(z.string())
15+
.min(1)
16+
.describe("Test case IDs, e.g. TC-123"),
1417
configuration_ids: z
1518
.array(z.number())
1619
.optional()
@@ -51,6 +54,14 @@ export const UpdateTestRunSchema = z.object({
5154

5255
type UpdateTestRunArgs = z.infer<typeof UpdateTestRunSchema>;
5356

57+
/**
58+
* Builds the HTTP Basic auth header from per-request config credentials.
59+
*/
60+
function buildAuthHeader(config: BrowserStackConfig): string {
61+
const [username, password] = getBrowserStackAuth(config).split(":");
62+
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
63+
}
64+
5465
/**
5566
* Partially updates an existing test run.
5667
*
@@ -70,7 +81,8 @@ export async function updateTestRun(
7081
): Promise<CallToolResult> {
7182
const { name, run_state, add_test_cases } = args.test_run;
7283

73-
const hasTestCases = (add_test_cases?.length ?? 0) > 0;
84+
const addIds = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
85+
const hasTestCases = addIds.length > 0;
7486
const hasMetadata = name !== undefined || run_state !== undefined;
7587

7688
if (!hasTestCases && !hasMetadata) {
@@ -85,14 +97,18 @@ export async function updateTestRun(
8597
};
8698
}
8799

88-
const results: CallToolResult[] = [];
100+
const tmBaseUrl = await getTMBaseURL(config);
101+
const authHeader = buildAuthHeader(config);
102+
103+
const tasks: Promise<CallToolResult>[] = [];
89104
if (hasMetadata) {
90-
results.push(await updateTestRunMetadata(args, config));
105+
tasks.push(updateTestRunMetadata(args, tmBaseUrl, authHeader));
91106
}
92107
if (hasTestCases) {
93-
results.push(await updateTestRunTestCases(args, config));
108+
tasks.push(updateTestRunTestCases(args, tmBaseUrl, authHeader));
94109
}
95110

111+
const results = await Promise.all(tasks);
96112
if (results.length === 1) {
97113
return results[0];
98114
}
@@ -109,24 +125,20 @@ export async function updateTestRun(
109125
*/
110126
async function updateTestRunMetadata(
111127
args: UpdateTestRunArgs,
112-
config: BrowserStackConfig,
128+
baseUrl: string,
129+
authHeader: string,
113130
): Promise<CallToolResult> {
114131
try {
115132
const { name, run_state } = args.test_run;
116133
const body = { test_run: { name, run_state } };
117-
const tmBaseUrl = await getTMBaseURL(config);
118-
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
134+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
119135
args.project_identifier,
120136
)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;
121137

122-
const authString = getBrowserStackAuth(config);
123-
const [username, password] = authString.split(":");
124-
125138
const resp = await apiClient.patch({
126139
url,
127140
headers: {
128-
Authorization:
129-
"Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
141+
Authorization: authHeader,
130142
"Content-Type": "application/json",
131143
},
132144
body,
@@ -165,7 +177,8 @@ async function updateTestRunMetadata(
165177
*/
166178
async function updateTestRunTestCases(
167179
args: UpdateTestRunArgs,
168-
config: BrowserStackConfig,
180+
baseUrl: string,
181+
authHeader: string,
169182
): Promise<CallToolResult> {
170183
try {
171184
const { add_test_cases, preserve_existing_results } = args.test_run;
@@ -177,19 +190,14 @@ async function updateTestRunTestCases(
177190
},
178191
};
179192

180-
const tmBaseUrl = await getTMBaseURL(config);
181-
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
193+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
182194
args.project_identifier,
183195
)}/test-runs/${encodeURIComponent(args.test_run_id)}/test-cases`;
184196

185-
const authString = getBrowserStackAuth(config);
186-
const [username, password] = authString.split(":");
187-
188197
const resp = await apiClient.patch({
189198
url,
190199
headers: {
191-
Authorization:
192-
"Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
200+
Authorization: authHeader,
193201
"Content-Type": "application/json",
194202
},
195203
body,

tests/tools/update-testrun.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,79 @@ describe("updateTestRun — endpoint dispatch", () => {
184184
"Failed to update test run test cases",
185185
);
186186
});
187+
188+
it("forwards configuration_ids in the test-cases body", async () => {
189+
(apiClient.patch as any).mockResolvedValue({
190+
data: { success: true, async: true, unique_id: "cfg" },
191+
});
192+
193+
await updateTestRun(
194+
{
195+
project_identifier: "PR-1",
196+
test_run_id: "TR-9",
197+
test_run: {
198+
add_test_cases: [
199+
{ test_case_ids: ["TC-1"], configuration_ids: [3, 4] },
200+
],
201+
},
202+
},
203+
mockConfig,
204+
);
205+
206+
const call = (apiClient.patch as any).mock.calls[0][0];
207+
expect(call.body.test_run.add_test_cases[0].configuration_ids).toEqual([
208+
3, 4,
209+
]);
210+
});
211+
212+
it("returns an error when the PATCH rejects (catch path)", async () => {
213+
(apiClient.patch as any).mockRejectedValue(new Error("network down"));
214+
215+
const result = await updateTestRun(
216+
{
217+
project_identifier: "PR-1",
218+
test_run_id: "TR-9",
219+
test_run: { add_test_cases: [{ test_case_ids: ["TC-1"] }] },
220+
},
221+
mockConfig,
222+
);
223+
224+
expect(result.isError).toBe(true);
225+
});
226+
227+
it("rejects an empty test_case_ids selection without any API call", async () => {
228+
const result = await updateTestRun(
229+
{
230+
project_identifier: "PR-1",
231+
test_run_id: "TR-9",
232+
test_run: { add_test_cases: [{ test_case_ids: [] }] },
233+
},
234+
mockConfig,
235+
);
236+
237+
expect(result.isError).toBe(true);
238+
expect(apiClient.patch).not.toHaveBeenCalled();
239+
expect(result.content?.[0]?.text).toContain("Nothing to update");
240+
});
241+
242+
it("flags an error when both concerns fail", async () => {
243+
(apiClient.patch as any)
244+
.mockResolvedValueOnce({ data: { success: false } })
245+
.mockResolvedValueOnce({ data: { success: false } });
246+
247+
const result = await updateTestRun(
248+
{
249+
project_identifier: "PR-1",
250+
test_run_id: "TR-1",
251+
test_run: {
252+
run_state: "in_progress",
253+
add_test_cases: [{ test_case_ids: ["TC-1"] }],
254+
},
255+
},
256+
mockConfig,
257+
);
258+
259+
expect(result.isError).toBe(true);
260+
expect(apiClient.patch).toHaveBeenCalledTimes(2);
261+
});
187262
});

0 commit comments

Comments
 (0)