Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2286343
feat(rca): add TFA status-marker parser util + tests
ruturaj-browserstack Jun 15, 2026
5bf4548
feat(rca): add TFA RCA turn util (submit + poll to completion)
ruturaj-browserstack Jun 15, 2026
04891c4
feat(rca): register tfaRcaTurn tool + server-factory wiring + tests
ruturaj-browserstack Jun 15, 2026
b70bee1
feat(rca): consume structured TFA turn, retire status-marker parser
ruturaj-browserstack Jun 15, 2026
b200cbc
feat(rca): make tfaRcaTurn o11y base URL configurable via startup config
ruturaj-browserstack Jun 20, 2026
34d6d7a
feat(rca): add opt-in failure-signature payload to listTestIds
ruturaj-browserstack Jun 23, 2026
05277de
test(rca): cover {text}-shaped TEST_FAILURE entry in listTestIds sign…
ruturaj-browserstack Jun 23, 2026
d9c53d6
feat(rca): make listTestIds Automate base URL configurable (rengg/sta…
ruturaj-browserstack Jun 24, 2026
1329d56
fix(rca): listTestIds — don't require run_count>0, so uploaded builds…
ruturaj-browserstack Jun 24, 2026
983f164
feat(rca): add triggerRcaReport glimpse tool; trim tfaRcaTurn termina…
ruturaj-browserstack Jul 12, 2026
910fafb
fix(rca): confirmed TRA UI deep-link for report/RCA views
ruturaj-browserstack Jul 14, 2026
b5ad013
Merge remote-tracking branch 'upstream/main' into feat/tfa-rca-mcp-br…
ruturaj-browserstack Jul 14, 2026
48b465e
feat(rca): default O11Y_TFA_RCA_BASE_URL to production
ruturaj-browserstack Jul 14, 2026
ac8a02c
Merge pull request #339 from ruturaj-browserstack/feat/tfa-rca-mcp-br…
ruturaj-browserstack Jul 14, 2026
7d41462
feat(rca): listTestIds returns ALL tests of a build (each with status)
ruturaj-browserstack Jul 16, 2026
a5b485a
Merge pull request #344 from ruturaj-browserstack/feat/tfa-rca-mcp-br…
ruturaj-browserstack Jul 16, 2026
257f9c1
fix(rca): raise listTestIds pagination cap (was silently truncating a…
ruturaj-browserstack Jul 16, 2026
80e9c41
Merge pull request #352 from ruturaj-browserstack/feat/tfa-rca-mcp-br…
Dave3130 Jul 22, 2026
77827dc
feat(rca): add getTfaTurnResult one-shot turn read
ruturaj-browserstack Jul 26, 2026
c50bd4e
Merge pull request #354 from ruturaj-browserstack/feat/tfa-rca-mcp-br…
ruturaj-browserstack Jul 26, 2026
c8dde36
feat(rca): add getBuildFailureThemes/listTestsInFailureTheme MCP tools
Dave3130 Aug 4, 2026
d360cb7
added temp fix to pass all rca context
Dave3130 Aug 4, 2026
5587c4a
docs(rca): improve MCP tool descriptions with negative guidance and s…
Dave3130 Aug 4, 2026
3f6352e
fix(rca): remove unused truncate()/RCA_GLIMPSE_ROOT_CAUSE_MAX in turn…
Dave3130 Aug 4, 2026
3b0b01b
fix(rca): restore trimmed RESOLVED glimpse, revert raw-rca temp fix
Dave3130 Aug 4, 2026
cc69b7d
docs(rca): reframe build-failure-themes ready:false as a server-outag…
Dave3130 Aug 13, 2026
3591c91
feat(build-insights): surface branch, commit_sha, vcs_name from build…
Dave3130 Aug 13, 2026
88b7bc4
docs(rca): document getBuildFailureThemes poll cadence (GET, one POST…
Dave3130 Aug 13, 2026
59b4125
AIR-607: mandate PR details as context to TFA agent with contract val…
Dave3130 Aug 20, 2026
a124fb6
fixed++
ruturaj-browserstack Aug 24, 2026
dae0a68
update++
ruturaj-browserstack Aug 24, 2026
4148d0c
update++
ruturaj-browserstack Aug 26, 2026
3e6c6de
update ++
ruturaj-browserstack Aug 31, 2026
8eb63b5
update ++
ruturaj-browserstack Aug 31, 2026
1b03af7
Trim verbose AI-generated comments
ruturaj-browserstack Aug 31, 2026
0e2d5f7
Remove tfa-rca-collaboration tool registration
ruturaj-browserstack Aug 31, 2026
0e4be5a
feat: export addTfaRcaCollaborationTools from package root
ruturaj-browserstack Aug 31, 2026
828dd70
Merge remote-tracking branch 'origin/main' into TM_TRA
ruturaj-browserstack Aug 31, 2026
b0e0684
chore: bump version to 1.3.1
ruturaj-browserstack Aug 31, 2026
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
26 changes: 26 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
}
}

// Overridable via O11Y_TFA_RCA_BASE_URL to target a staging tenant.
const DEFAULT_O11Y_TFA_RCA_BASE_URL = "https://api-automation.browserstack.com";

// Overridable via BROWSERSTACK_AUTOMATION_BASE_URL to target a non-prod env.
const DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL =
"https://api-automation.browserstack.com";

// Overridable via BROWSERSTACK_O11Y_UI_BASE_URL to point at a staging UI.
const DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL =
"https://automation.browserstack.com";

/**
* USE_OWN_LOCAL_BINARY_PROCESS:
* If true, the system will not start a new local binary process, but will use the user's own process.
Expand All @@ -41,6 +52,9 @@ export class Config {
public readonly USE_OWN_LOCAL_BINARY_PROCESS: boolean,
public readonly REMOTE_MCP: boolean,
public readonly UPLOAD_BASE_DIR: string | undefined,
public readonly O11Y_TFA_RCA_BASE_URL: string,
public readonly BROWSERSTACK_AUTOMATION_BASE_URL: string,
public readonly BROWSERSTACK_O11Y_UI_BASE_URL: string,
) {}
}

Expand All @@ -52,6 +66,18 @@ const config = new Config(
process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0
? process.env.MCP_UPLOAD_BASE_DIR
: undefined,
process.env.O11Y_TFA_RCA_BASE_URL &&
process.env.O11Y_TFA_RCA_BASE_URL.length > 0
? process.env.O11Y_TFA_RCA_BASE_URL
: DEFAULT_O11Y_TFA_RCA_BASE_URL,
process.env.BROWSERSTACK_AUTOMATION_BASE_URL &&
process.env.BROWSERSTACK_AUTOMATION_BASE_URL.length > 0
? process.env.BROWSERSTACK_AUTOMATION_BASE_URL
: DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL,
process.env.BROWSERSTACK_O11Y_UI_BASE_URL &&
process.env.BROWSERSTACK_O11Y_UI_BASE_URL.length > 0
? process.env.BROWSERSTACK_O11Y_UI_BASE_URL
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL,
);

export default config;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ process.on("exit", () => {
export { setLogger } from "./logger.js";
export { BrowserStackMcpServer } from "./server-factory.js";
export { trackMCP } from "./lib/instrumentation.js";
export { default as addTfaRcaCollaborationTools } from "./tools/tfa-rca-collaboration.js";
export const PackageJsonVersion = packageJson.version;
13 changes: 10 additions & 3 deletions src/tools/build-insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ export async function fetchBuildInsightsTool(
const buildUrl = `https://api-automation.browserstack.com/ext/v1/builds/${args.buildId}`;
const qualityGateUrl = `https://api-automation.browserstack.com/ext/v1/quality-gates/${args.buildId}`;

// Quality gate data is optional — a failure there should not block build insights
const [buildData, qualityData] = await Promise.all([
fetchFromBrowserStackAPI(buildUrl, config),
fetchFromBrowserStackAPI(qualityGateUrl, config),
fetchFromBrowserStackAPI(qualityGateUrl, config).catch((error) => {
logger.warn("Failed to fetch quality gate data", error);
return null;
}),
]);

// Select useful fields for users
Expand All @@ -34,10 +38,13 @@ export async function fetchBuildInsightsTool(
unique_errors: buildData.unique_errors?.overview,
observability_url: buildData?.observability_url,
ci_build_url: buildData.ci_info?.build_url,
quality_gate_result: qualityData.quality_gate_result,
branch: buildData.vcs_info?.branch,
commit_sha: buildData.vcs_info?.sha,
vcs_name: buildData.vcs_info?.name,
quality_gate_result: qualityData?.quality_gate_result,
};

const qualityProfiles = qualityData.quality_profiles?.map(
const qualityProfiles = qualityData?.quality_profiles?.map(
(profile: any) => ({
name: profile.name,
result: profile.result,
Expand Down
9 changes: 9 additions & 0 deletions src/tools/rca-agent-utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { z } from "zod";
import appConfig from "../../config.js";
import { TestStatus } from "./types.js";

export function getAutomationBaseUrl(): string {
return appConfig.BROWSERSTACK_AUTOMATION_BASE_URL;
}

export const FETCH_RCA_PARAMS = {
testId: z
.array(z.number().int())
Expand Down Expand Up @@ -34,4 +39,8 @@ export const LIST_TEST_IDS_PARAMS = {
.describe(
"Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status",
),
includeFailureDetail: z
.boolean()
.optional()
.describe("Add per-test failure signature for clustering. Default false."),
};
142 changes: 114 additions & 28 deletions src/tools/rca-agent-utils/get-failed-test-id.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import logger from "../../logger.js";
import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js";
import { getAutomationBaseUrl } from "./constants.js";
import {
TestStatus,
FailedTestInfo,
TestRun,
TestDetails,
TestFailureSignature,
} from "./types.js";

// Keeps the response payload lean — never full stack traces.
const ERROR_SUMMARY_MAX = 200;

// Safety bound on pagination; hitting it is logged, never silent.
const MAX_PAGES = 100;

export async function getTestIds(
buildId: string,
authString: string,
status?: TestStatus,
includeFailureDetail = false,
): Promise<FailedTestInfo[]> {
const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`;
// No `status` → returns ALL tests; a `status` narrows the query.
const baseUrl = `${getAutomationBaseUrl()}/ext/v1/builds/${buildId}/testRuns`;
let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
let allFailedTests: FailedTestInfo[] = [];
let allTests: FailedTestInfo[] = [];
let requestNumber = 0;

// Construct Basic auth header
Expand All @@ -34,18 +49,23 @@ export async function getTestIds(

const data = (await response.json()) as TestRun;

// Extract failed IDs from current page
// Extract test IDs from the current page (all tests unless narrowed).
if (data.hierarchy && data.hierarchy.length > 0) {
const currentFailedTests = extractFailedTestIds(data.hierarchy, status);
allFailedTests = allFailedTests.concat(currentFailedTests);
const currentTests = extractTestIds(
data.hierarchy,
status,
includeFailureDetail,
);
allTests = allTests.concat(currentTests);
}

// Check for pagination termination conditions
if (
!data.pagination?.has_next ||
!data.pagination.next_page ||
requestNumber >= 5
) {
if (!data.pagination?.has_next || !data.pagination.next_page) {
break;
}
if (requestNumber >= MAX_PAGES) {
logger.warn(
`listTestIds: hit MAX_PAGES (${MAX_PAGES}) for build ${buildId}; result may be partial`,
);
break;
}

Expand All @@ -57,39 +77,105 @@ export async function getTestIds(
url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
}

// Return unique failed test IDs
return allFailedTests;
return allTests;
} catch (error) {
logger.error("Error fetching failed tests:", error);
logger.error("Error fetching test runs:", error);
throw error;
}
}

export function extractFailedTestIds(
export function extractTestIds(
hierarchy: TestDetails[],
status?: TestStatus,
includeFailureDetail = false,
): FailedTestInfo[] {
let failedTests: FailedTestInfo[] = [];
let tests: FailedTestInfo[] = [];

for (const node of hierarchy) {
if (node.details?.status === status) {
if (node.details?.observability_url) {
const idMatch = node.details.observability_url.match(/details=(\d+)/);
if (idMatch) {
failedTests.push({
test_id: idMatch[1],
test_name: node.display_name || `Test ${idMatch[1]}`,
});
// observability_url `details=<id>` filters out suite/hook nodes. run_count
// is not required — JUnit-uploaded builds report run_count=0 for real tests too.
const nodeStatus = node.details?.status;
const statusMatches = status === undefined || nodeStatus === status;
if (statusMatches && node.details?.observability_url) {
const idMatch = node.details.observability_url.match(/details=(\d+)/);
if (idMatch) {
const entry: FailedTestInfo = {
test_id: idMatch[1],
test_name: node.display_name || `Test ${idMatch[1]}`,
status: nodeStatus,
};
// Failure signatures only exist for failed tests; include when asked.
if (includeFailureDetail && nodeStatus === TestStatus.FAILED) {
const signature = buildFailureSignature(node.details);
if (signature) entry.failure = signature;
}
tests.push(entry);
}
}

if (node.children && node.children.length > 0) {
failedTests = failedTests.concat(
extractFailedTestIds(node.children, status),
tests = tests.concat(
extractTestIds(node.children, status, includeFailureDetail),
);
}
}

return failedTests;
return tests;
}

// Back-compat alias — prefer extractTestIds. Kept so existing imports/tests
// referencing the old name continue to resolve.
export const extractFailedTestIds = extractTestIds;

// Build a trimmed failure signature from a test node's `details`. Returns
// undefined when no signal is available so the field is simply omitted.
function buildFailureSignature(details: any): TestFailureSignature | undefined {
if (!details) return undefined;

const signature: TestFailureSignature = {};

if (details.failure_categories != null) {
signature.category = Array.isArray(details.failure_categories)
? details.failure_categories.filter(Boolean).join(", ")
: String(details.failure_categories);
}

const errorSummary = extractFirstFailureLine(details);
if (errorSummary) signature.error_summary = errorSummary;

if (details.file_path) signature.file_path = String(details.file_path);
if (typeof details.is_flaky === "boolean")
signature.is_flaky = details.is_flaky;
if (typeof details.is_always_failing === "boolean")
signature.is_always_failing = details.is_always_failing;
if (typeof details.is_new_failure === "boolean")
signature.is_new_failure = details.is_new_failure;

return Object.keys(signature).length > 0 ? signature : undefined;
}

// First non-empty line of the first retry's TEST_FAILURE log, capped. Handles
// both string entries and object entries ({ message } / { text }).
function extractFirstFailureLine(details: any): string | undefined {
const retries = details?.retries;
if (!Array.isArray(retries)) return undefined;

for (const retry of retries) {
const failures = retry?.logs?.TEST_FAILURE;
if (!failures) continue;
const entries = Array.isArray(failures) ? failures : [failures];
for (const failure of entries) {
const text =
typeof failure === "string"
? failure
: (failure?.message ?? failure?.text ?? "");
const firstLine = String(text)
.split("\n")
.map((line) => line.trim())
.find((line) => line.length > 0);
if (firstLine) return firstLine.slice(0, ERROR_SUMMARY_MAX);
}
}

return undefined;
}
17 changes: 17 additions & 0 deletions src/tools/rca-agent-utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,26 @@ export interface TestRun {
};
}

// Trimmed per-test failure signature for downstream clustering.
// Never carries full stack traces — error_summary is a single capped line.
export interface TestFailureSignature {
category?: string;
error_summary?: string;
file_path?: string;
is_flaky?: boolean;
is_always_failing?: boolean;
is_new_failure?: boolean;
}

export interface FailedTestInfo {
test_id: number;
test_name: string;
// The test's own status (passed/failed/pending/skipped). listTestIds returns
// ALL tests by default, so consumers rely on this to filter/group.
status?: TestStatus;
// Present only when listTestIds is called with includeFailureDetail=true
// (only failed tests carry a signature).
failure?: TestFailureSignature;
}

export enum RCAState {
Expand Down
12 changes: 9 additions & 3 deletions src/tools/rca-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,21 @@ export async function listTestIdsTool(
args: {
buildId: string;
status?: TestStatus;
includeFailureDetail?: boolean;
},
config: BrowserStackConfig,
): Promise<CallToolResult> {
try {
const { buildId, status } = args;
const { buildId, status, includeFailureDetail } = args;
const authString = getBrowserStackAuth(config);

// Get test IDs
const testIds = await getTestIds(buildId, authString, status);
const testIds = await getTestIds(
buildId,
authString,
status,
includeFailureDetail,
);

return {
content: [
Expand Down Expand Up @@ -244,7 +250,7 @@ export default function addRCATools(

tools.listTestIds = server.tool(
"listTestIds",
"List test IDs from a BrowserStack Automate build, optionally filtered by status",
"List all tests of a BrowserStack build (each with its status); optional status filter.",
LIST_TEST_IDS_PARAMS,
async (args) => {
try {
Expand Down
Loading
Loading