Skip to content

feat: implement create LCA steps functionality and polling mechanism … #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions src/tools/testmanagement-utils/TCG-utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export async function pollScenariosTestDetails(
progressToken: context._meta?.progressToken ?? traceId,
progress: count,
total: count,
message: `Fetched ${count} scenarios`,
message: `Generated ${count} scenarios`,
},
});
}
Expand Down Expand Up @@ -230,7 +230,7 @@ export async function pollScenariosTestDetails(
progressToken: context._meta?.progressToken ?? traceId,
progress: iteratorCount,
total,
message: `Fetched ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
message: `Generated ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
},
});
}
Expand Down Expand Up @@ -315,7 +315,7 @@ export async function bulkCreateTestCases(
method: "notifications/progress",
params: {
progressToken: context._meta?.progressToken ?? "bulk-create",
message: `Bulk create done for scenario ${doneCount} of ${total}`,
message: `Saving and creating test cases...`,
total,
progress: doneCount,
},
Expand All @@ -326,7 +326,7 @@ export async function bulkCreateTestCases(
method: "notifications/progress",
params: {
progressToken: context._meta?.progressToken ?? traceId,
message: `Bulk create failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
message: `Creation failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
total,
progress: doneCount,
},
Expand Down Expand Up @@ -361,3 +361,61 @@ export async function projectIdentifierToId(
}
throw new Error(`Project with identifier ${projectId} not found.`);
}

export async function testCaseIdentifierToDetails(
projectId: string,
testCaseIdentifier: string,
): Promise<{ testCaseId: string; folderId: string }> {
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;

const response = await axios.get(url, {
headers: {
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
accept: "application/json, text/plain, */*",
},
});

if (response.data.success !== true) {
throw new Error(
`Failed to fetch test case details: ${response.statusText}`,
);
}

// Check if test_cases array exists and has items
if (
!response.data.test_cases ||
!Array.isArray(response.data.test_cases) ||
response.data.test_cases.length === 0
) {
throw new Error(
`No test cases found in response for identifier ${testCaseIdentifier}`,
);
}

for (const testCase of response.data.test_cases) {
if (testCase.identifier === testCaseIdentifier) {
// Extract folder ID from the links.folder URL
// URL format: "/api/v1/projects/1930314/folder/10193436/test-cases"
let folderId = "";
if (testCase.links && testCase.links.folder) {
const folderMatch = testCase.links.folder.match(/\/folder\/(\d+)\//);
if (folderMatch && folderMatch[1]) {
folderId = folderMatch[1];
}
}

if (!folderId) {
throw new Error(
`Could not extract folder ID for test case ${testCaseIdentifier}`,
);
}

return {
testCaseId: testCase.id.toString(),
folderId: folderId,
};
}
}

throw new Error(`Test case with identifier ${testCaseIdentifier} not found.`);
}
193 changes: 193 additions & 0 deletions src/tools/testmanagement-utils/create-lca-steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import config from "../../config.js";
import { formatAxiosError } from "../../lib/error.js";
import {
projectIdentifierToId,
testCaseIdentifierToDetails,
} from "./TCG-utils/api.js";
import { pollLCAStatus } from "./poll-lca-status.js";

/**
* Schema for creating LCA steps for a test case
*/
export const CreateLCAStepsSchema = z.object({
project_identifier: z
.string()
.describe("ID of the project (Starts with 'PR-')"),
test_case_identifier: z
.string()
.describe("Identifier of the test case (e.g., 'TC-12345')"),
base_url: z.string().describe("Base URL for the test (e.g., 'google.com')"),
credentials: z
.object({
username: z.string().describe("Username for authentication"),
password: z.string().describe("Password for authentication"),
})
.optional()
.describe(
"Optional credentials for authentication. Extract from the test case details if provided in it. This is required for the test cases which require authentication.",
),
local_enabled: z
.boolean()
.optional()
.default(false)
.describe("Whether local testing is enabled"),
test_name: z.string().describe("Name of the test"),
test_case_details: z
.object({
name: z.string().describe("Name of the test case"),
description: z.string().describe("Description of the test case"),
preconditions: z.string().describe("Preconditions for the test case"),
test_case_steps: z
.array(
z.object({
step: z.string().describe("Test step description"),
result: z.string().describe("Expected result"),
}),
)
.describe("Array of test case steps with expected results"),
})
.describe("Test case details including steps"),
wait_for_completion: z
.boolean()
.optional()
.default(true)
.describe("Whether to wait for LCA build completion (default: true)"),
});

export type CreateLCAStepsArgs = z.infer<typeof CreateLCAStepsSchema>;

/**
* Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management
*/
export async function createLCASteps(
args: CreateLCAStepsArgs,
context: any,
): Promise<CallToolResult> {
try {
// Get the project ID from identifier
const projectId = await projectIdentifierToId(args.project_identifier);

// Get the test case ID and folder ID from identifier
const { testCaseId, folderId } = await testCaseIdentifierToDetails(
projectId,
args.test_case_identifier,
);

const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;

const payload = {
base_url: args.base_url,
credentials: args.credentials,
local_enabled: args.local_enabled,
test_name: args.test_name,
test_case_details: args.test_case_details,
version: "v2",
webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
};

const response = await axios.post(url, payload, {
headers: {
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
});

if (response.status >= 200 && response.status < 300) {
// Check if user wants to wait for completion
if (!args.wait_for_completion) {
return {
content: [
{
type: "text",
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
},
{
type: "text",
text: "LCA build started. Check the BrowserStack Lowcode Automation UI for completion status.",
},
],
};
}

// Start polling for LCA build completion
try {
const max_wait_minutes = 10; // Maximum wait time in minutes
const maxWaitMs = max_wait_minutes * 60 * 1000;
const lcaResult = await pollLCAStatus(
projectId,
folderId,
testCaseId,
context,
maxWaitMs, // max wait time
2 * 60 * 1000, // 2 minutes initial wait
10 * 1000, // 10 seconds interval
);

if (lcaResult && lcaResult.status === "done") {
return {
content: [
{
type: "text",
text: `Successfully created LCA steps for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
},
{
type: "text",
text: `LCA build completed! Resource URL: ${lcaResult.resource_path}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
},
{
type: "text",
text: `Warning: LCA build did not complete within ${max_wait_minutes} minutes. You can check the status later in the BrowserStack Test Management UI.`,
},
],
};
}
} catch (pollError) {
console.error("Error during LCA polling:", pollError);
return {
content: [
{
type: "text",
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
},
{
type: "text",
text: "Warning: Error occurred while polling for LCA build completion. Check the BrowserStack Test Management UI for status.",
},
],
};
}
} else {
throw new Error(`Unexpected response: ${JSON.stringify(response.data)}`);
}
} catch (error) {
// Add more specific error handling
if (error instanceof Error) {
if (error.message.includes("not found")) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}. Please verify that the project identifier "${args.project_identifier}" and test case identifier "${args.test_case_identifier}" are correct.`,
isError: true,
},
],
isError: true,
};
}
}
return formatAxiosError(error, "Failed to create LCA steps");
}
}
Loading