Skip to content

Commit 52474f8

Browse files
Update test results (#30)
* Bump version to 1.0.12 * Feat: Implement addTestResult functionality and update test management tools
1 parent 22794a5 commit 52474f8

File tree

5 files changed

+213
-35
lines changed

5 files changed

+213
-35
lines changed

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class Config {
1818
const config = new Config(
1919
process.env.BROWSERSTACK_USERNAME!,
2020
process.env.BROWSERSTACK_ACCESS_KEY!,
21-
process.env.DEV_MODE === "true"
21+
process.env.DEV_MODE === "true",
2222
);
2323

2424
export default config;

src/lib/instrumentation.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export function trackMCP(
2020
clientInfo: { name?: string; version?: string },
2121
error?: unknown,
2222
): void {
23-
2423
if (config.DEV_MODE) {
2524
logger.info("Tracking MCP is disabled in dev mode");
2625
return;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 adding a test result to a test run.
9+
*/
10+
export const AddTestResultSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe("Identifier of the project (e.g., PR-12345)"),
14+
test_run_id: z.string().describe("Identifier of the test run (e.g., TR-678)"),
15+
test_result: z.object({
16+
status: z
17+
.string()
18+
.describe("Status of the test result, e.g., 'passed', 'failed'."),
19+
description: z
20+
.string()
21+
.optional()
22+
.describe("Optional description of the test result."),
23+
}),
24+
test_case_id: z
25+
.string()
26+
.describe("Identifier of the test case, e.g., 'TC-13'."),
27+
});
28+
29+
export type AddTestResultArgs = z.infer<typeof AddTestResultSchema>;
30+
31+
/**
32+
* Adds a test result to a specific test run via BrowserStack Test Management API.
33+
*/
34+
export async function addTestResult(
35+
rawArgs: AddTestResultArgs,
36+
): Promise<CallToolResult> {
37+
try {
38+
const args = AddTestResultSchema.parse(rawArgs);
39+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
40+
args.project_identifier,
41+
)}/test-runs/${encodeURIComponent(args.test_run_id)}/results`;
42+
43+
const body = {
44+
test_result: args.test_result,
45+
test_case_id: args.test_case_id,
46+
} as any;
47+
48+
const response = await axios.post(url, body, {
49+
auth: {
50+
username: config.browserstackUsername,
51+
password: config.browserstackAccessKey,
52+
},
53+
headers: { "Content-Type": "application/json" },
54+
});
55+
56+
const data = response.data;
57+
if (!data.success) {
58+
throw new Error(
59+
`API returned unsuccessful response: ${JSON.stringify(data)}`,
60+
);
61+
}
62+
63+
return {
64+
content: [
65+
{
66+
type: "text",
67+
text: `Successfully added test result with ID=${data["test-result"].id}`,
68+
},
69+
{ type: "text", text: JSON.stringify(data["test-result"], null, 2) },
70+
],
71+
};
72+
} catch (err: any) {
73+
return formatAxiosError(err, "Failed to add test result to test run");
74+
}
75+
}

src/tools/testmanagement.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
CreateTestCaseSchema,
1515
} from "./testmanagement-utils/create-testcase";
1616

17-
1817
let serverInstance: McpServer;
1918

2019
import {
@@ -37,8 +36,10 @@ import {
3736
updateTestRun,
3837
} from "./testmanagement-utils/update-testrun";
3938

40-
41-
//TODO: Moving the traceMCP and catch block to the parent(server) function
39+
import {
40+
addTestResult,
41+
AddTestResultSchema,
42+
} from "./testmanagement-utils/add-test-result";
4243

4344
/**
4445
* Wrapper to call createProjectOrFolder util.
@@ -111,8 +112,10 @@ export async function listTestCasesTool(
111112
args: z.infer<typeof ListTestCasesSchema>,
112113
): Promise<CallToolResult> {
113114
try {
115+
trackMCP("listTestCases", serverInstance.server.getClientVersion()!);
114116
return await listTestCases(args);
115117
} catch (err) {
118+
trackMCP("listTestCases", serverInstance.server.getClientVersion()!, err);
116119
return {
117120
content: [
118121
{
@@ -135,8 +138,10 @@ export async function createTestRunTool(
135138
args: z.infer<typeof CreateTestRunSchema>,
136139
): Promise<CallToolResult> {
137140
try {
141+
trackMCP("createTestRun", serverInstance.server.getClientVersion()!);
138142
return await createTestRun(args);
139143
} catch (err) {
144+
trackMCP("createTestRun", serverInstance.server.getClientVersion()!, err);
140145
return {
141146
content: [
142147
{
@@ -159,8 +164,10 @@ export async function listTestRunsTool(
159164
args: z.infer<typeof ListTestRunsSchema>,
160165
): Promise<CallToolResult> {
161166
try {
167+
trackMCP("listTestRuns", serverInstance.server.getClientVersion()!);
162168
return await listTestRuns(args);
163169
} catch (err) {
170+
trackMCP("listTestRuns", serverInstance.server.getClientVersion()!, err);
164171
return {
165172
content: [
166173
{
@@ -185,8 +192,10 @@ export async function updateTestRunTool(
185192
args: z.infer<typeof UpdateTestRunSchema>,
186193
): Promise<CallToolResult> {
187194
try {
195+
trackMCP("updateTestRun", serverInstance.server.getClientVersion()!);
188196
return await updateTestRun(args);
189197
} catch (err) {
198+
trackMCP("updateTestRun", serverInstance.server.getClientVersion()!, err);
190199
return {
191200
content: [
192201
{
@@ -202,6 +211,32 @@ export async function updateTestRunTool(
202211
}
203212
}
204213

214+
/**
215+
* Adds a test result to a specific test run via BrowserStack Test Management API.
216+
*/
217+
export async function addTestResultTool(
218+
args: z.infer<typeof AddTestResultSchema>,
219+
): Promise<CallToolResult> {
220+
try {
221+
trackMCP("addTestResult", serverInstance.server.getClientVersion()!);
222+
return await addTestResult(args);
223+
} catch (err) {
224+
trackMCP("addTestResult", serverInstance.server.getClientVersion()!, err);
225+
return {
226+
content: [
227+
{
228+
type: "text",
229+
text: `Failed to add test result: ${
230+
err instanceof Error ? err.message : "Unknown error"
231+
}. Please open an issue on GitHub if the problem persists`,
232+
isError: true,
233+
},
234+
],
235+
isError: true,
236+
};
237+
}
238+
}
239+
205240
/**
206241
* Registers both project/folder and test-case tools.
207242
*/
@@ -247,4 +282,10 @@ export default function addTestManagementTools(server: McpServer) {
247282
UpdateTestRunSchema.shape,
248283
updateTestRunTool,
249284
);
285+
server.tool(
286+
"addTestResult",
287+
"Add a test result to a specific test run via BrowserStack Test Management API.",
288+
AddTestResultSchema.shape,
289+
addTestResultTool,
290+
);
250291
}

tests/tools/testmanagement.test.ts

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { createProjectOrFolderTool } from '../../src/tools/testmanagement';
1+
import {
2+
createProjectOrFolderTool,
3+
createTestCaseTool,
4+
createTestRunTool,
5+
addTestResultTool,
6+
listTestRunsTool,
7+
updateTestRunTool
8+
} from '../../src/tools/testmanagement';
9+
import addTestManagementTools from '../../src/tools/testmanagement';
210
import { createProjectOrFolder } from '../../src/tools/testmanagement-utils/create-project-folder';
3-
import { createTestCaseTool , createTestRunTool } from '../../src/tools/testmanagement';
411
import { createTestCase, sanitizeArgs, TestCaseCreateRequest } from '../../src/tools/testmanagement-utils/create-testcase';
5-
import addTestManagementTools from '../../src/tools/testmanagement';
6-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7-
import axios from 'axios';
812
import { listTestCases } from '../../src/tools/testmanagement-utils/list-testcases';
913
import { createTestRun } from '../../src/tools/testmanagement-utils/create-testrun';
10-
14+
import { addTestResult } from '../../src/tools/testmanagement-utils/add-test-result';
15+
import { listTestRuns } from '../../src/tools/testmanagement-utils/list-testruns';
16+
import { updateTestRun } from '../../src/tools/testmanagement-utils/update-testrun';
17+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18+
import axios from 'axios';
1119

1220
// Mock dependencies
1321
jest.mock('../../src/tools/testmanagement-utils/create-project-folder', () => ({
@@ -21,7 +29,7 @@ jest.mock('../../src/tools/testmanagement-utils/create-testcase', () => ({
2129
createTestCase: jest.fn(),
2230
sanitizeArgs: jest.fn((args) => args),
2331
CreateTestCaseSchema: {
24-
shape: {},
32+
shape: {},
2533
},
2634
}));
2735
jest.mock('../../src/config', () => ({
@@ -31,11 +39,18 @@ jest.mock('../../src/config', () => ({
3139
browserstackAccessKey: 'fake-key',
3240
},
3341
}));
34-
3542
jest.mock('../../src/lib/instrumentation', () => ({
3643
trackMCP: jest.fn()
3744
}));
3845

46+
jest.mock('../../src/tools/testmanagement-utils/add-test-result', () => ({
47+
addTestResult: jest.fn(),
48+
AddTestResultSchema: {
49+
parse: (args: any) => args,
50+
shape: {},
51+
},
52+
}));
53+
3954
const mockServer = {
4055
tool: jest.fn(),
4156
server: {
@@ -57,6 +72,23 @@ jest.mock('../../src/tools/testmanagement-utils/create-testrun', () => ({
5772
}));
5873
jest.mock('axios');
5974

75+
76+
jest.mock('../../src/tools/testmanagement-utils/list-testruns', () => ({
77+
listTestRuns: jest.fn(),
78+
ListTestRunsSchema: {
79+
parse: (args: any) => args,
80+
shape: {},
81+
},
82+
}));
83+
jest.mock('../../src/tools/testmanagement-utils/update-testrun', () => ({
84+
updateTestRun: jest.fn(),
85+
UpdateTestRunSchema: {
86+
parse: (args: any) => args,
87+
shape: {},
88+
},
89+
}));
90+
91+
6092
const mockedAxios = axios as jest.Mocked<typeof axios>;
6193

6294
describe('createTestCaseTool', () => {
@@ -264,28 +296,6 @@ describe('createTestRunTool', () => {
264296
});
265297
});
266298

267-
//
268-
// New tests for listTestRunsTool & updateTestRunTool
269-
//
270-
271-
import { listTestRunsTool, updateTestRunTool } from '../../src/tools/testmanagement';
272-
import { listTestRuns } from '../../src/tools/testmanagement-utils/list-testruns';
273-
import { updateTestRun } from '../../src/tools/testmanagement-utils/update-testrun';
274-
275-
jest.mock('../../src/tools/testmanagement-utils/list-testruns', () => ({
276-
listTestRuns: jest.fn(),
277-
ListTestRunsSchema: {
278-
parse: (args: any) => args,
279-
shape: {},
280-
},
281-
}));
282-
jest.mock('../../src/tools/testmanagement-utils/update-testrun', () => ({
283-
updateTestRun: jest.fn(),
284-
UpdateTestRunSchema: {
285-
parse: (args: any) => args,
286-
shape: {},
287-
},
288-
}));
289299

290300
describe('listTestRunsTool', () => {
291301
beforeEach(() => {
@@ -357,3 +367,56 @@ describe('updateTestRunTool', () => {
357367
expect(result.content[0].text).toContain('Failed to update test run: API Error');
358368
});
359369
});
370+
371+
372+
373+
describe('addTestResultTool', () => {
374+
beforeEach(() => {
375+
jest.clearAllMocks();
376+
});
377+
378+
const validArgs = {
379+
project_identifier: 'proj-123',
380+
test_run_id: 'run-456',
381+
test_result: {
382+
status: 'passed',
383+
description: 'All good',
384+
issues: ['ISSUE-1'],
385+
issue_tracker: { name: 'jira', host: 'https://jira.example.com' },
386+
custom_fields: { priority: 'high' },
387+
},
388+
test_case_id: 'TC-1',
389+
};
390+
391+
const successAddResult = {
392+
content: [{ type: 'text', text: 'Successfully added test result to test run run-456' }],
393+
isError: false,
394+
};
395+
396+
it('should successfully add a test result', async () => {
397+
(addTestResult as jest.Mock).mockResolvedValue(successAddResult);
398+
399+
const result = await addTestResultTool(validArgs as any);
400+
401+
expect(addTestResult).toHaveBeenCalledWith(validArgs);
402+
expect(result).toBe(successAddResult);
403+
});
404+
405+
it('should handle API errors gracefully', async () => {
406+
(addTestResult as jest.Mock).mockRejectedValue(new Error('Network Error'));
407+
408+
const result = await addTestResultTool(validArgs as any);
409+
410+
expect(result.isError).toBe(true);
411+
expect(result.content[0].text).toContain('Failed to add test result: Network Error');
412+
});
413+
414+
it('should handle unknown errors gracefully', async () => {
415+
(addTestResult as jest.Mock).mockRejectedValue('unexpected');
416+
417+
const result = await addTestResultTool(validArgs as any);
418+
419+
expect(result.isError).toBe(true);
420+
expect(result.content[0].text).toContain('Unknown error');
421+
});
422+
});

0 commit comments

Comments
 (0)