Skip to content

Commit 148195d

Browse files
Feat: Add listTestRuns and updateTestRun utilities with corresponding schemas and tests
1 parent 02745ee commit 148195d

File tree

4 files changed

+324
-4
lines changed

4 files changed

+324
-4
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import axios from "axios";
2+
import config from "../../config";
3+
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error";
6+
7+
/**
8+
* Schema for listing test runs with optional filters.
9+
*/
10+
export const ListTestRunsSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe(
14+
"Identifier of the project to fetch test runs from (e.g., PR-12345)",
15+
),
16+
run_state: z
17+
.string()
18+
.optional()
19+
.describe("Return all test runs with this state (comma-separated)"),
20+
});
21+
22+
type ListTestRunsArgs = z.infer<typeof ListTestRunsSchema>;
23+
24+
/**
25+
* Fetches and formats the list of test runs for a project.
26+
*/
27+
export async function listTestRuns(
28+
args: ListTestRunsArgs,
29+
): Promise<CallToolResult> {
30+
try {
31+
const params = new URLSearchParams();
32+
if (args.run_state) {
33+
params.set("run_state", args.run_state);
34+
}
35+
36+
const url =
37+
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
38+
args.project_identifier,
39+
)}/test-runs?` + params.toString();
40+
41+
const resp = await axios.get(url, {
42+
auth: {
43+
username: config.browserstackUsername,
44+
password: config.browserstackAccessKey,
45+
},
46+
});
47+
48+
const data = resp.data;
49+
if (!data.success) {
50+
return {
51+
content: [
52+
{
53+
type: "text",
54+
text: `Failed to list test runs: ${JSON.stringify(data)}`,
55+
isError: true,
56+
},
57+
],
58+
isError: true,
59+
};
60+
}
61+
62+
const runs = data.test_runs;
63+
const count = runs.length;
64+
const summary = runs
65+
.map((tr: any) => `• ${tr.identifier}: ${tr.name} [${tr.run_state}]`)
66+
.join("\n");
67+
68+
return {
69+
content: [
70+
{ type: "text", text: `Found ${count} test run(s):\n\n${summary}` },
71+
{ type: "text", text: JSON.stringify(runs, null, 2) },
72+
],
73+
};
74+
} catch (err) {
75+
return formatAxiosError(err, "Failed to list test runs");
76+
}
77+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import axios from "axios";
2+
import config from "../../config";
3+
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error";
6+
7+
/**
8+
* Schema for updating a test run with partial fields.
9+
*/
10+
export const UpdateTestRunSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe("Project identifier (e.g., PR-12345)"),
14+
test_run_id: z.string().describe("Test run identifier (e.g., TR-678)"),
15+
test_run: z.object({
16+
name: z.string().optional().describe("New name of the test run"),
17+
run_state: z
18+
.enum([
19+
"new_run",
20+
"in_progress",
21+
"under_review",
22+
"rejected",
23+
"done",
24+
"closed",
25+
])
26+
.optional()
27+
.describe("Updated state of the test run"),
28+
}),
29+
});
30+
31+
type UpdateTestRunArgs = z.infer<typeof UpdateTestRunSchema>;
32+
33+
/**
34+
* Partially updates an existing test run.
35+
*/
36+
export async function updateTestRun(
37+
args: UpdateTestRunArgs,
38+
): Promise<CallToolResult> {
39+
try {
40+
const body = { test_run: args.test_run };
41+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
42+
args.project_identifier,
43+
)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;
44+
45+
const resp = await axios.patch(url, body, {
46+
auth: {
47+
username: config.browserstackUsername,
48+
password: config.browserstackAccessKey,
49+
},
50+
});
51+
52+
const data = resp.data;
53+
if (!data.success) {
54+
return {
55+
content: [
56+
{
57+
type: "text",
58+
text: `Failed to update test run: ${JSON.stringify(data)}`,
59+
isError: true,
60+
},
61+
],
62+
isError: true,
63+
};
64+
}
65+
66+
return {
67+
content: [
68+
{
69+
type: "text",
70+
text: `Successfully updated test run ${args.test_run_id}`,
71+
},
72+
{ type: "text", text: JSON.stringify(data.testrun || data, null, 2) },
73+
],
74+
};
75+
} catch (err) {
76+
return formatAxiosError(err, "Failed to update test run");
77+
}
78+
}

src/tools/testmanagement.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ import {
2222
createTestRun,
2323
} from "./testmanagement-utils/create-testrun";
2424

25+
import {
26+
ListTestRunsSchema,
27+
listTestRuns,
28+
} from "./testmanagement-utils/list-testruns";
29+
30+
import {
31+
UpdateTestRunSchema,
32+
updateTestRun,
33+
} from "./testmanagement-utils/update-testrun";
34+
2535
/**
2636
* Wrapper to call createProjectOrFolder util.
2737
*/
@@ -98,7 +108,6 @@ export async function listTestCasesTool(
98108
}
99109

100110
/**
101-
* CREATE TEST RUN
102111
* Creates a test run in BrowserStack Test Management.
103112
*/
104113
export async function createTestRunTool(
@@ -122,6 +131,56 @@ export async function createTestRunTool(
122131
}
123132
}
124133

134+
/**
135+
* Lists test runs in a project with optional filters (date ranges, assignee, state, etc.)
136+
*/
137+
export async function listTestRunsTool(
138+
args: z.infer<typeof ListTestRunsSchema>,
139+
): Promise<CallToolResult> {
140+
try {
141+
return await listTestRuns(args);
142+
} catch (err) {
143+
return {
144+
content: [
145+
{
146+
type: "text",
147+
text: `Failed to list test runs: ${
148+
err instanceof Error ? err.message : "Unknown error"
149+
}. Please open an issue on GitHub if the problem persists`,
150+
isError: true,
151+
},
152+
],
153+
isError: true,
154+
};
155+
}
156+
}
157+
158+
/**
159+
* Updates a test run in BrowserStack Test Management.
160+
* This function allows for partial updates to an existing test run.
161+
* It takes the project identifier and test run ID as parameters.
162+
*/
163+
export async function updateTestRunTool(
164+
args: z.infer<typeof UpdateTestRunSchema>,
165+
): Promise<CallToolResult> {
166+
try {
167+
return await updateTestRun(args);
168+
} catch (err) {
169+
return {
170+
content: [
171+
{
172+
type: "text",
173+
text: `Failed to update test run: ${
174+
err instanceof Error ? err.message : "Unknown error"
175+
}. Please open an issue on GitHub if the problem persists`,
176+
isError: true,
177+
},
178+
],
179+
isError: true,
180+
};
181+
}
182+
}
183+
125184
/**
126185
* Registers both project/folder and test-case tools.
127186
*/
@@ -153,4 +212,17 @@ export default function addTestManagementTools(server: McpServer) {
153212
CreateTestRunSchema.shape,
154213
createTestRunTool,
155214
);
215+
216+
server.tool(
217+
"listTestRuns",
218+
"List test runs in a project with optional filters (date ranges, assignee, state, etc.)",
219+
ListTestRunsSchema.shape,
220+
listTestRunsTool,
221+
);
222+
server.tool(
223+
"updateTestRun",
224+
"Update a test run in BrowserStack Test Management.",
225+
UpdateTestRunSchema.shape,
226+
updateTestRunTool,
227+
);
156228
}

tests/tools/testmanagement.test.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createProjectOrFolderTool } from '../../src/tools/testmanagement';
22
import { createProjectOrFolder } from '../../src/tools/testmanagement-utils/create-project-folder';
3-
import { createTestCaseTool , createTestRunTool} from '../../src/tools/testmanagement';
3+
import { createTestCaseTool , createTestRunTool } from '../../src/tools/testmanagement';
44
import { createTestCase, sanitizeArgs, TestCaseCreateRequest } from '../../src/tools/testmanagement-utils/create-testcase';
55
import axios from 'axios';
66
import { listTestCases } from '../../src/tools/testmanagement-utils/list-testcases';
@@ -28,7 +28,6 @@ jest.mock('../../src/config', () => ({
2828
}));
2929
jest.mock('../../src/tools/testmanagement-utils/create-testrun', () => ({
3030
createTestRun: jest.fn(),
31-
// if you’re parsing args the same way as in the others, you can include the schema mock too:
3231
CreateTestRunSchema: {
3332
parse: (args: any) => args,
3433
shape: {},
@@ -197,7 +196,7 @@ describe('listTestCases util', () => {
197196
});
198197
});
199198

200-
describe('createTestRunTool', () => {
199+
describe('createTestRunTool', () => {
201200
beforeEach(() => {
202201
jest.clearAllMocks();
203202
});
@@ -242,3 +241,97 @@ describe('createTestRunTool', () => {
242241
expect(result.content[0].text).toContain('Unknown error');
243242
});
244243
});
244+
245+
//
246+
// New tests for listTestRunsTool & updateTestRunTool
247+
//
248+
249+
import { listTestRunsTool, updateTestRunTool } from '../../src/tools/testmanagement';
250+
import { listTestRuns } from '../../src/tools/testmanagement-utils/list-testruns';
251+
import { updateTestRun } from '../../src/tools/testmanagement-utils/update-testrun';
252+
253+
jest.mock('../../src/tools/testmanagement-utils/list-testruns', () => ({
254+
listTestRuns: jest.fn(),
255+
ListTestRunsSchema: {
256+
parse: (args: any) => args,
257+
shape: {},
258+
},
259+
}));
260+
jest.mock('../../src/tools/testmanagement-utils/update-testrun', () => ({
261+
updateTestRun: jest.fn(),
262+
UpdateTestRunSchema: {
263+
parse: (args: any) => args,
264+
shape: {},
265+
},
266+
}));
267+
268+
describe('listTestRunsTool', () => {
269+
beforeEach(() => {
270+
jest.clearAllMocks();
271+
});
272+
273+
const mockRuns = [
274+
{ identifier: 'TR-1', name: 'Run One', run_state: 'new_run' },
275+
{ identifier: 'TR-2', name: 'Run Two', run_state: 'done' },
276+
];
277+
const projectId = 'PR-123';
278+
279+
it('should return summary and raw JSON on success', async () => {
280+
(listTestRuns as jest.Mock).mockResolvedValue({
281+
content: [
282+
{ type: 'text', text: `Found 2 test run(s):\n\n• TR-1: Run One [new_run]\n• TR-2: Run Two [done]` },
283+
{ type: 'text', text: JSON.stringify(mockRuns, null, 2) },
284+
],
285+
isError: false,
286+
});
287+
288+
const result = await listTestRunsTool({ project_identifier: projectId } as any);
289+
expect(listTestRuns).toHaveBeenCalledWith({ project_identifier: projectId });
290+
expect(result.isError).toBe(false);
291+
expect(result.content[0].text).toContain('Found 2 test run(s):');
292+
expect(result.content[1].text).toBe(JSON.stringify(mockRuns, null, 2));
293+
});
294+
295+
it('should handle errors', async () => {
296+
(listTestRuns as jest.Mock).mockRejectedValue(new Error('Network Error'));
297+
const result = await listTestRunsTool({ project_identifier: projectId } as any);
298+
expect(result.isError).toBe(true);
299+
expect(result.content[0].text).toContain('Failed to list test runs: Network Error');
300+
});
301+
});
302+
303+
describe('updateTestRunTool', () => {
304+
beforeEach(() => {
305+
jest.clearAllMocks();
306+
});
307+
308+
const args = {
309+
project_identifier: 'PR-123',
310+
test_run_id: 'TR-1',
311+
test_run: { name: 'Updated Name', run_state: 'in_progress' },
312+
};
313+
314+
it('should return success message and updated run JSON on success', async () => {
315+
const updated = { name: 'Updated Name', run_state: 'in_progress', tags: [] };
316+
(updateTestRun as jest.Mock).mockResolvedValue({
317+
content: [
318+
{ type: 'text', text: `Successfully updated test run ${args.test_run_id}` },
319+
{ type: 'text', text: JSON.stringify(updated, null, 2) },
320+
],
321+
isError: false,
322+
});
323+
324+
const result = await updateTestRunTool(args as any);
325+
expect(updateTestRun).toHaveBeenCalledWith(args);
326+
expect(result.isError).toBe(false);
327+
expect(result.content[0].text).toContain(`Successfully updated test run ${args.test_run_id}`);
328+
expect(result.content[1].text).toBe(JSON.stringify(updated, null, 2));
329+
});
330+
331+
it('should handle errors', async () => {
332+
(updateTestRun as jest.Mock).mockRejectedValue(new Error('API Error'));
333+
const result = await updateTestRunTool(args as any);
334+
expect(result.isError).toBe(true);
335+
expect(result.content[0].text).toContain('Failed to update test run: API Error');
336+
});
337+
});

0 commit comments

Comments
 (0)