Skip to content

Commit 40f3b75

Browse files
feat: implement create LCA steps functionality and polling mechanism … (#57)
* feat: implement create LCA steps functionality and polling mechanism for BrowserStack Test Management * fix: update polling notification message for LCA build status * fix: enhance description for optional authentication credentials in LCA steps schema * fix: update progress messages for LCA build and test case generation * fix: enhance descriptions for file upload and test case creation tools in BrowserStack Test Management * fix: update progress messages for LCA status polling and scenario generation * fix: refine progress notification formatting in LCA status polling * fix: add dashboard URL to test case creation response * fix: correct folder path in dashboard URL for test case creation response
1 parent eee8f17 commit 40f3b75

File tree

6 files changed

+596
-7
lines changed

6 files changed

+596
-7
lines changed

src/tools/testmanagement-utils/TCG-utils/api.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export async function pollScenariosTestDetails(
191191
progressToken: context._meta?.progressToken ?? traceId,
192192
progress: count,
193193
total: count,
194-
message: `Fetched ${count} scenarios`,
194+
message: `Generated ${count} scenarios`,
195195
},
196196
});
197197
}
@@ -230,7 +230,7 @@ export async function pollScenariosTestDetails(
230230
progressToken: context._meta?.progressToken ?? traceId,
231231
progress: iteratorCount,
232232
total,
233-
message: `Fetched ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
233+
message: `Generated ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
234234
},
235235
});
236236
}
@@ -315,7 +315,7 @@ export async function bulkCreateTestCases(
315315
method: "notifications/progress",
316316
params: {
317317
progressToken: context._meta?.progressToken ?? "bulk-create",
318-
message: `Bulk create done for scenario ${doneCount} of ${total}`,
318+
message: `Saving and creating test cases...`,
319319
total,
320320
progress: doneCount,
321321
},
@@ -326,7 +326,7 @@ export async function bulkCreateTestCases(
326326
method: "notifications/progress",
327327
params: {
328328
progressToken: context._meta?.progressToken ?? traceId,
329-
message: `Bulk create failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
329+
message: `Creation failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
330330
total,
331331
progress: doneCount,
332332
},
@@ -361,3 +361,61 @@ export async function projectIdentifierToId(
361361
}
362362
throw new Error(`Project with identifier ${projectId} not found.`);
363363
}
364+
365+
export async function testCaseIdentifierToDetails(
366+
projectId: string,
367+
testCaseIdentifier: string,
368+
): Promise<{ testCaseId: string; folderId: string }> {
369+
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
370+
371+
const response = await axios.get(url, {
372+
headers: {
373+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
374+
accept: "application/json, text/plain, */*",
375+
},
376+
});
377+
378+
if (response.data.success !== true) {
379+
throw new Error(
380+
`Failed to fetch test case details: ${response.statusText}`,
381+
);
382+
}
383+
384+
// Check if test_cases array exists and has items
385+
if (
386+
!response.data.test_cases ||
387+
!Array.isArray(response.data.test_cases) ||
388+
response.data.test_cases.length === 0
389+
) {
390+
throw new Error(
391+
`No test cases found in response for identifier ${testCaseIdentifier}`,
392+
);
393+
}
394+
395+
for (const testCase of response.data.test_cases) {
396+
if (testCase.identifier === testCaseIdentifier) {
397+
// Extract folder ID from the links.folder URL
398+
// URL format: "/api/v1/projects/1930314/folder/10193436/test-cases"
399+
let folderId = "";
400+
if (testCase.links && testCase.links.folder) {
401+
const folderMatch = testCase.links.folder.match(/\/folder\/(\d+)\//);
402+
if (folderMatch && folderMatch[1]) {
403+
folderId = folderMatch[1];
404+
}
405+
}
406+
407+
if (!folderId) {
408+
throw new Error(
409+
`Could not extract folder ID for test case ${testCaseIdentifier}`,
410+
);
411+
}
412+
413+
return {
414+
testCaseId: testCase.id.toString(),
415+
folderId: folderId,
416+
};
417+
}
418+
}
419+
420+
throw new Error(`Test case with identifier ${testCaseIdentifier} not found.`);
421+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import axios from "axios";
4+
import config from "../../config.js";
5+
import { formatAxiosError } from "../../lib/error.js";
6+
import {
7+
projectIdentifierToId,
8+
testCaseIdentifierToDetails,
9+
} from "./TCG-utils/api.js";
10+
import { pollLCAStatus } from "./poll-lca-status.js";
11+
12+
/**
13+
* Schema for creating LCA steps for a test case
14+
*/
15+
export const CreateLCAStepsSchema = z.object({
16+
project_identifier: z
17+
.string()
18+
.describe("ID of the project (Starts with 'PR-')"),
19+
test_case_identifier: z
20+
.string()
21+
.describe("Identifier of the test case (e.g., 'TC-12345')"),
22+
base_url: z.string().describe("Base URL for the test (e.g., 'google.com')"),
23+
credentials: z
24+
.object({
25+
username: z.string().describe("Username for authentication"),
26+
password: z.string().describe("Password for authentication"),
27+
})
28+
.optional()
29+
.describe(
30+
"Optional credentials for authentication. Extract from the test case details if provided in it. This is required for the test cases which require authentication.",
31+
),
32+
local_enabled: z
33+
.boolean()
34+
.optional()
35+
.default(false)
36+
.describe("Whether local testing is enabled"),
37+
test_name: z.string().describe("Name of the test"),
38+
test_case_details: z
39+
.object({
40+
name: z.string().describe("Name of the test case"),
41+
description: z.string().describe("Description of the test case"),
42+
preconditions: z.string().describe("Preconditions for the test case"),
43+
test_case_steps: z
44+
.array(
45+
z.object({
46+
step: z.string().describe("Test step description"),
47+
result: z.string().describe("Expected result"),
48+
}),
49+
)
50+
.describe("Array of test case steps with expected results"),
51+
})
52+
.describe("Test case details including steps"),
53+
wait_for_completion: z
54+
.boolean()
55+
.optional()
56+
.default(true)
57+
.describe("Whether to wait for LCA build completion (default: true)"),
58+
});
59+
60+
export type CreateLCAStepsArgs = z.infer<typeof CreateLCAStepsSchema>;
61+
62+
/**
63+
* Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management
64+
*/
65+
export async function createLCASteps(
66+
args: CreateLCAStepsArgs,
67+
context: any,
68+
): Promise<CallToolResult> {
69+
try {
70+
// Get the project ID from identifier
71+
const projectId = await projectIdentifierToId(args.project_identifier);
72+
73+
// Get the test case ID and folder ID from identifier
74+
const { testCaseId, folderId } = await testCaseIdentifierToDetails(
75+
projectId,
76+
args.test_case_identifier,
77+
);
78+
79+
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
80+
81+
const payload = {
82+
base_url: args.base_url,
83+
credentials: args.credentials,
84+
local_enabled: args.local_enabled,
85+
test_name: args.test_name,
86+
test_case_details: args.test_case_details,
87+
version: "v2",
88+
webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
89+
};
90+
91+
const response = await axios.post(url, payload, {
92+
headers: {
93+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
94+
accept: "application/json, text/plain, */*",
95+
"Content-Type": "application/json",
96+
},
97+
});
98+
99+
if (response.status >= 200 && response.status < 300) {
100+
// Check if user wants to wait for completion
101+
if (!args.wait_for_completion) {
102+
return {
103+
content: [
104+
{
105+
type: "text",
106+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
107+
},
108+
{
109+
type: "text",
110+
text: "LCA build started. Check the BrowserStack Lowcode Automation UI for completion status.",
111+
},
112+
],
113+
};
114+
}
115+
116+
// Start polling for LCA build completion
117+
try {
118+
const max_wait_minutes = 10; // Maximum wait time in minutes
119+
const maxWaitMs = max_wait_minutes * 60 * 1000;
120+
const lcaResult = await pollLCAStatus(
121+
projectId,
122+
folderId,
123+
testCaseId,
124+
context,
125+
maxWaitMs, // max wait time
126+
2 * 60 * 1000, // 2 minutes initial wait
127+
10 * 1000, // 10 seconds interval
128+
);
129+
130+
if (lcaResult && lcaResult.status === "done") {
131+
return {
132+
content: [
133+
{
134+
type: "text",
135+
text: `Successfully created LCA steps for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
136+
},
137+
{
138+
type: "text",
139+
text: `LCA build completed! Resource URL: ${lcaResult.resource_path}`,
140+
},
141+
],
142+
};
143+
} else {
144+
return {
145+
content: [
146+
{
147+
type: "text",
148+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
149+
},
150+
{
151+
type: "text",
152+
text: `Warning: LCA build did not complete within ${max_wait_minutes} minutes. You can check the status later in the BrowserStack Test Management UI.`,
153+
},
154+
],
155+
};
156+
}
157+
} catch (pollError) {
158+
console.error("Error during LCA polling:", pollError);
159+
return {
160+
content: [
161+
{
162+
type: "text",
163+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
164+
},
165+
{
166+
type: "text",
167+
text: "Warning: Error occurred while polling for LCA build completion. Check the BrowserStack Test Management UI for status.",
168+
},
169+
],
170+
};
171+
}
172+
} else {
173+
throw new Error(`Unexpected response: ${JSON.stringify(response.data)}`);
174+
}
175+
} catch (error) {
176+
// Add more specific error handling
177+
if (error instanceof Error) {
178+
if (error.message.includes("not found")) {
179+
return {
180+
content: [
181+
{
182+
type: "text",
183+
text: `Error: ${error.message}. Please verify that the project identifier "${args.project_identifier}" and test case identifier "${args.test_case_identifier}" are correct.`,
184+
isError: true,
185+
},
186+
],
187+
isError: true,
188+
};
189+
}
190+
}
191+
return formatAxiosError(error, "Failed to create LCA steps");
192+
}
193+
}

0 commit comments

Comments
 (0)