Skip to content

Commit 6f3d018

Browse files
Merge pull request #333 from SavioBS629/feat/update-testrun-add-testcases
feat: add test cases to existing test run via updateTestRun
2 parents 1a6c9fb + ec43108 commit 6f3d018

4 files changed

Lines changed: 417 additions & 10 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: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ import { formatAxiosError } from "../../lib/error.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
77
import { getTMBaseURL } from "../../lib/tm-base-url.js";
88

9+
/**
10+
* Selection of test cases (with optional configurations) to add.
11+
*/
12+
const TestCaseSelectionSchema = z.object({
13+
test_case_ids: z
14+
.array(z.string())
15+
.min(1)
16+
.describe("Test case IDs, e.g. TC-123"),
17+
configuration_ids: z
18+
.array(z.number())
19+
.optional()
20+
.describe("Configuration IDs to apply"),
21+
});
22+
923
/**
1024
* Schema for updating a test run with partial fields.
1125
*/
@@ -27,33 +41,103 @@ export const UpdateTestRunSchema = z.object({
2741
])
2842
.optional()
2943
.describe("Updated state of the test run"),
44+
add_test_cases: z
45+
.array(TestCaseSelectionSchema)
46+
.optional()
47+
.describe("Test cases to add to the run"),
48+
preserve_existing_results: z
49+
.boolean()
50+
.optional()
51+
.describe("Keep existing results when adding cases (default true)"),
3052
}),
3153
});
3254

3355
type UpdateTestRunArgs = z.infer<typeof UpdateTestRunSchema>;
3456

57+
/**
58+
* Builds the HTTP Basic auth header from per-request config credentials.
59+
*/
60+
function buildAuthHeader(config: BrowserStackConfig): string {
61+
return "Basic " + Buffer.from(getBrowserStackAuth(config)).toString("base64");
62+
}
63+
3564
/**
3665
* Partially updates an existing test run.
66+
*
67+
* Dispatches to one of two BrowserStack endpoints based on the fields provided,
68+
* mirroring how the platform splits these concerns across two endpoints:
69+
* - metadata (name / run_state) -> PATCH .../test-runs/{id}/update
70+
* - adding test cases -> PATCH .../test-runs/{id}/test-cases
71+
*
72+
* Either or both may be supplied in one call; each provided concern hits its
73+
* own endpoint and both outcomes are reported. At least one must be provided.
74+
* Removing test cases is intentionally not exposed — this tool is
75+
* non-destructive.
3776
*/
3877
export async function updateTestRun(
3978
args: UpdateTestRunArgs,
4079
config: BrowserStackConfig,
80+
): Promise<CallToolResult> {
81+
const { name, run_state, add_test_cases } = args.test_run;
82+
83+
const addIds = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
84+
const hasTestCases = addIds.length > 0;
85+
const hasMetadata = name !== undefined || run_state !== undefined;
86+
87+
if (!hasTestCases && !hasMetadata) {
88+
return {
89+
content: [
90+
{
91+
type: "text",
92+
text: "Nothing to update: provide name/run_state and/or add_test_cases.",
93+
},
94+
],
95+
isError: true,
96+
};
97+
}
98+
99+
const tmBaseUrl = await getTMBaseURL(config);
100+
const authHeader = buildAuthHeader(config);
101+
102+
const tasks: Promise<CallToolResult>[] = [];
103+
if (hasMetadata) {
104+
tasks.push(updateTestRunMetadata(args, tmBaseUrl, authHeader));
105+
}
106+
if (hasTestCases) {
107+
tasks.push(updateTestRunTestCases(args, tmBaseUrl, authHeader));
108+
}
109+
110+
const results = await Promise.all(tasks);
111+
if (results.length === 1) {
112+
return results[0];
113+
}
114+
115+
// Both concerns updated: aggregate outcomes; surface an error if either failed.
116+
return {
117+
content: results.flatMap((r) => r.content),
118+
isError: results.some((r) => r.isError),
119+
};
120+
}
121+
122+
/**
123+
* Updates test run metadata (name / run_state) via the /update endpoint.
124+
*/
125+
async function updateTestRunMetadata(
126+
args: UpdateTestRunArgs,
127+
baseUrl: string,
128+
authHeader: string,
41129
): Promise<CallToolResult> {
42130
try {
43-
const body = { test_run: args.test_run };
44-
const tmBaseUrl = await getTMBaseURL(config);
45-
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
131+
const { name, run_state } = args.test_run;
132+
const body = { test_run: { name, run_state } };
133+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
46134
args.project_identifier,
47135
)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;
48136

49-
const authString = getBrowserStackAuth(config);
50-
const [username, password] = authString.split(":");
51-
52137
const resp = await apiClient.patch({
53138
url,
54139
headers: {
55-
Authorization:
56-
"Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
140+
Authorization: authHeader,
57141
"Content-Type": "application/json",
58142
},
59143
body,
@@ -85,3 +169,64 @@ export async function updateTestRun(
85169
return formatAxiosError(err, "Failed to update test run");
86170
}
87171
}
172+
173+
/**
174+
* Adds test cases to a run via the /test-cases endpoint.
175+
* This call is applied asynchronously by the backend.
176+
*/
177+
async function updateTestRunTestCases(
178+
args: UpdateTestRunArgs,
179+
baseUrl: string,
180+
authHeader: string,
181+
): Promise<CallToolResult> {
182+
try {
183+
const { add_test_cases, preserve_existing_results } = args.test_run;
184+
185+
const body = {
186+
test_run: {
187+
add_test_cases,
188+
preserve_existing_results: preserve_existing_results ?? true,
189+
},
190+
};
191+
192+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
193+
args.project_identifier,
194+
)}/test-runs/${encodeURIComponent(args.test_run_id)}/test-cases`;
195+
196+
const resp = await apiClient.patch({
197+
url,
198+
headers: {
199+
Authorization: authHeader,
200+
"Content-Type": "application/json",
201+
},
202+
body,
203+
});
204+
205+
const data = resp.data;
206+
if (!data.success) {
207+
return {
208+
content: [
209+
{
210+
type: "text",
211+
text: `Failed to update test run test cases: ${JSON.stringify(data)}`,
212+
},
213+
],
214+
isError: true,
215+
};
216+
}
217+
218+
const added = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
219+
220+
return {
221+
content: [
222+
{
223+
type: "text",
224+
text: `Queued test-case update for ${args.test_run_id} (added ${added.length}); changes apply asynchronously.`,
225+
},
226+
{ type: "text", text: JSON.stringify(data, null, 2) },
227+
],
228+
};
229+
} catch (err) {
230+
return formatAxiosError(err, "Failed to update test run test cases");
231+
}
232+
}

src/tools/testmanagement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ export default function addTestManagementTools(
736736

737737
tools.updateTestRun = server.tool(
738738
"updateTestRun",
739-
"Update a test run in BrowserStack Test Management.",
739+
"Update a test run's metadata and/or add test cases to it.",
740740
UpdateTestRunSchema.shape,
741741
(args) => updateTestRunTool(args, config, server),
742742
);

0 commit comments

Comments
 (0)