Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ As of now we support 20 tools.
List all test runs from the 'Shopping App' project that were executed last week and are currently marked in-progress
```

6. `updateTestRun` — Partially update a test run (status, tags, notes, associated test cases).
6. `updateTestRun` — Update a test run's name/state and/or add test cases to it.
**Prompt example**

```text
Expand Down
161 changes: 153 additions & 8 deletions src/tools/testmanagement-utils/update-testrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ import { formatAxiosError } from "../../lib/error.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

/**
* Selection of test cases (with optional configurations) to add.
*/
const TestCaseSelectionSchema = z.object({
test_case_ids: z
.array(z.string())
.min(1)
.describe("Test case IDs, e.g. TC-123"),
configuration_ids: z
Comment thread
SavioBS629 marked this conversation as resolved.
.array(z.number())
.optional()
.describe("Configuration IDs to apply"),
});

/**
* Schema for updating a test run with partial fields.
*/
Expand All @@ -27,33 +41,103 @@ export const UpdateTestRunSchema = z.object({
])
.optional()
.describe("Updated state of the test run"),
add_test_cases: z
.array(TestCaseSelectionSchema)
.optional()
.describe("Test cases to add to the run"),
preserve_existing_results: z
.boolean()
.optional()
.describe("Keep existing results when adding cases (default true)"),
}),
});

type UpdateTestRunArgs = z.infer<typeof UpdateTestRunSchema>;

/**
* Builds the HTTP Basic auth header from per-request config credentials.
*/
function buildAuthHeader(config: BrowserStackConfig): string {
return "Basic " + Buffer.from(getBrowserStackAuth(config)).toString("base64");
}

/**
* Partially updates an existing test run.
*
* Dispatches to one of two BrowserStack endpoints based on the fields provided,
* mirroring how the platform splits these concerns across two endpoints:
* - metadata (name / run_state) -> PATCH .../test-runs/{id}/update
* - adding test cases -> PATCH .../test-runs/{id}/test-cases
*
* Either or both may be supplied in one call; each provided concern hits its
* own endpoint and both outcomes are reported. At least one must be provided.
* Removing test cases is intentionally not exposed — this tool is
* non-destructive.
*/
export async function updateTestRun(
args: UpdateTestRunArgs,
config: BrowserStackConfig,
): Promise<CallToolResult> {
const { name, run_state, add_test_cases } = args.test_run;

const addIds = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
const hasTestCases = addIds.length > 0;
const hasMetadata = name !== undefined || run_state !== undefined;

if (!hasTestCases && !hasMetadata) {
return {
content: [
{
type: "text",
text: "Nothing to update: provide name/run_state and/or add_test_cases.",
},
],
isError: true,
};
}

const tmBaseUrl = await getTMBaseURL(config);
const authHeader = buildAuthHeader(config);

const tasks: Promise<CallToolResult>[] = [];
if (hasMetadata) {
tasks.push(updateTestRunMetadata(args, tmBaseUrl, authHeader));
}
if (hasTestCases) {
tasks.push(updateTestRunTestCases(args, tmBaseUrl, authHeader));
}

const results = await Promise.all(tasks);
if (results.length === 1) {
return results[0];
}

// Both concerns updated: aggregate outcomes; surface an error if either failed.
return {
content: results.flatMap((r) => r.content),
isError: results.some((r) => r.isError),
};
}

/**
* Updates test run metadata (name / run_state) via the /update endpoint.
*/
async function updateTestRunMetadata(
args: UpdateTestRunArgs,
baseUrl: string,
authHeader: string,
): Promise<CallToolResult> {
try {
const body = { test_run: args.test_run };
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
const { name, run_state } = args.test_run;
const body = { test_run: { name, run_state } };
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;

const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");

const resp = await apiClient.patch({
url,
headers: {
Authorization:
"Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
Authorization: authHeader,
"Content-Type": "application/json",
},
body,
Expand Down Expand Up @@ -85,3 +169,64 @@ export async function updateTestRun(
return formatAxiosError(err, "Failed to update test run");
}
}

/**
* Adds test cases to a run via the /test-cases endpoint.
* This call is applied asynchronously by the backend.
*/
async function updateTestRunTestCases(
args: UpdateTestRunArgs,
baseUrl: string,
authHeader: string,
): Promise<CallToolResult> {
try {
const { add_test_cases, preserve_existing_results } = args.test_run;

const body = {
test_run: {
add_test_cases,
preserve_existing_results: preserve_existing_results ?? true,
},
};

const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-runs/${encodeURIComponent(args.test_run_id)}/test-cases`;

const resp = await apiClient.patch({
url,
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
},
body,
});

const data = resp.data;
if (!data.success) {
return {
content: [
{
type: "text",
text: `Failed to update test run test cases: ${JSON.stringify(data)}`,
},
],
isError: true,
};
}

const added = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];

return {
content: [
{
type: "text",
text: `Queued test-case update for ${args.test_run_id} (added ${added.length}); changes apply asynchronously.`,
},
{ type: "text", text: JSON.stringify(data, null, 2) },
],
};
} catch (err) {
return formatAxiosError(err, "Failed to update test run test cases");
}
}
2 changes: 1 addition & 1 deletion src/tools/testmanagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ export default function addTestManagementTools(

tools.updateTestRun = server.tool(
"updateTestRun",
"Update a test run in BrowserStack Test Management.",
"Update a test run's metadata and/or add test cases to it.",
UpdateTestRunSchema.shape,
(args) => updateTestRunTool(args, config, server),
);
Expand Down
Loading
Loading