Skip to content

Commit 185c011

Browse files
Feat: Add test management utilities for creating test runs and listin… (#23)
* Feat: Add test management utilities for creating test runs and listing test cases * Feat: Enhance listTestCases utility to handle error responses and update tests * Feat: Update ListTestCasesSchema to improve descriptions and remove unused fields * Feat: Refactor CreateTestRunSchema by removing unused fields and simplifying structure * Feat: Add listTestRuns and updateTestRun utilities with corresponding schemas and tests
1 parent fc13c91 commit 185c011

File tree

6 files changed

+702
-3
lines changed

6 files changed

+702
-3
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 creating a test run.
9+
*/
10+
export const CreateTestRunSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe("Identifier of the project in which to create the test run."),
14+
test_run: z.object({
15+
name: z.string().describe("Name of the test run"),
16+
description: z
17+
.string()
18+
.optional()
19+
.describe("Brief information about the test run"),
20+
run_state: z
21+
.enum([
22+
"new_run",
23+
"in_progress",
24+
"under_review",
25+
"rejected",
26+
"done",
27+
"closed",
28+
])
29+
.optional()
30+
.describe(
31+
"State of the test run. One of new_run, in_progress, under_review, rejected, done, closed",
32+
),
33+
issues: z.array(z.string()).optional().describe("Linked issue IDs"),
34+
issue_tracker: z
35+
.object({ name: z.string(), host: z.string().url() })
36+
.optional()
37+
.describe("Issue tracker configuration"),
38+
test_cases: z
39+
.array(z.string())
40+
.optional()
41+
.describe("List of test case IDs"),
42+
folder_ids: z
43+
.array(z.number())
44+
.optional()
45+
.describe("Folder IDs to include"),
46+
}),
47+
});
48+
49+
export type CreateTestRunArgs = z.infer<typeof CreateTestRunSchema>;
50+
51+
/**
52+
* Creates a test run via BrowserStack Test Management API.
53+
*/
54+
export async function createTestRun(
55+
rawArgs: CreateTestRunArgs,
56+
): Promise<CallToolResult> {
57+
try {
58+
const inputArgs = {
59+
...rawArgs,
60+
test_run: {
61+
...rawArgs.test_run,
62+
include_all: false,
63+
},
64+
};
65+
const args = CreateTestRunSchema.parse(inputArgs);
66+
67+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
68+
args.project_identifier,
69+
)}/test-runs`;
70+
71+
const response = await axios.post(
72+
url,
73+
{ test_run: args.test_run },
74+
{
75+
auth: {
76+
username: config.browserstackUsername,
77+
password: config.browserstackAccessKey,
78+
},
79+
headers: { "Content-Type": "application/json" },
80+
},
81+
);
82+
83+
const data = response.data;
84+
if (!data.success) {
85+
throw new Error(
86+
`API returned unsuccessful response: ${JSON.stringify(data)}`,
87+
);
88+
}
89+
90+
// Assume data.test_run contains created run info
91+
const created = data.test_run || data;
92+
const runId = created.identifier || created.id || created.name;
93+
94+
const summary = `Successfully created test run ${runId}`;
95+
return {
96+
content: [
97+
{ type: "text", text: summary },
98+
{ type: "text", text: JSON.stringify(created, null, 2) },
99+
],
100+
};
101+
} catch (err) {
102+
return formatAxiosError(err, "Failed to create test run");
103+
}
104+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 cases with optional filters.
9+
*/
10+
export const ListTestCasesSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe(
14+
"Identifier of the project to fetch test cases from. Example: PR-12345",
15+
),
16+
folder_id: z
17+
.string()
18+
.optional()
19+
.describe("If provided, only return cases in this folder."),
20+
case_type: z
21+
.string()
22+
.optional()
23+
.describe(
24+
"Comma-separated list of case types (e.g. functional,regression).",
25+
),
26+
priority: z
27+
.string()
28+
.optional()
29+
.describe("Comma-separated list of priorities (e.g. critical,medium,low)."),
30+
31+
p: z.number().optional().describe("Page number."),
32+
});
33+
34+
export type ListTestCasesArgs = z.infer<typeof ListTestCasesSchema>;
35+
36+
/**
37+
* Calls BrowserStack Test Management to list test cases with filters.
38+
*/
39+
export async function listTestCases(
40+
args: ListTestCasesArgs,
41+
): Promise<CallToolResult> {
42+
try {
43+
// Build query string
44+
const params = new URLSearchParams();
45+
if (args.folder_id) params.append("folder_id", args.folder_id);
46+
if (args.case_type) params.append("case_type", args.case_type);
47+
if (args.priority) params.append("priority", args.priority);
48+
if (args.p !== undefined) params.append("p", args.p.toString());
49+
50+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
51+
args.project_identifier,
52+
)}/test-cases?${params.toString()}`;
53+
54+
const resp = await axios.get(url, {
55+
auth: {
56+
username: config.browserstackUsername,
57+
password: config.browserstackAccessKey,
58+
},
59+
});
60+
61+
const resp_data = resp.data;
62+
if (!resp_data.success) {
63+
return {
64+
content: [
65+
{
66+
type: "text",
67+
text: `Failed to list test cases: ${JSON.stringify(resp_data)}`,
68+
isError: true,
69+
},
70+
],
71+
isError: true,
72+
};
73+
}
74+
75+
const { test_cases, info } = resp_data;
76+
const count = info?.count ?? test_cases.length;
77+
78+
// Summary for more focused output
79+
const summary = test_cases
80+
.map(
81+
(tc: any) =>
82+
`• ${tc.identifier}: ${tc.title} [${tc.case_type} | ${tc.priority}]`,
83+
)
84+
.join("\n");
85+
86+
return {
87+
content: [
88+
{
89+
type: "text",
90+
text: `Found ${count} test case(s):\n\n${summary}`,
91+
},
92+
{
93+
type: "text",
94+
text: JSON.stringify(test_cases, null, 2),
95+
},
96+
],
97+
};
98+
} catch (err) {
99+
return formatAxiosError(err, "Failed to list test cases");
100+
}
101+
}
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+
}

0 commit comments

Comments
 (0)