@browserstack/mcp-server 1.2.25-beta.1 → 1.2.27-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.js +42 -2
- package/dist/server-factory.js +2 -0
- package/dist/tools/rca-agent-utils/constants.d.ts +9 -0
- package/dist/tools/rca-agent-utils/constants.js +15 -0
- package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +2 -2
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +70 -7
- package/dist/tools/rca-agent-utils/types.d.ts +9 -0
- package/dist/tools/rca-agent.d.ts +1 -0
- package/dist/tools/rca-agent.js +2 -2
- package/dist/tools/testmanagement-utils/list-testcases.js +7 -1
- package/dist/tools/testmanagement-utils/update-testrun.d.ts +15 -0
- package/dist/tools/testmanagement-utils/update-testrun.js +128 -6
- package/dist/tools/testmanagement.js +1 -1
- package/dist/tools/tfa-rca-collaboration.d.ts +8 -0
- package/dist/tools/tfa-rca-collaboration.js +78 -0
- package/dist/tools/tfa-rca-utils/constants.d.ts +65 -0
- package/dist/tools/tfa-rca-utils/constants.js +87 -0
- package/dist/tools/tfa-rca-utils/submit-turn.d.ts +28 -0
- package/dist/tools/tfa-rca-utils/submit-turn.js +222 -0
- package/dist/tools/tfa-rca-utils/trigger-report.d.ts +36 -0
- package/dist/tools/tfa-rca-utils/trigger-report.js +70 -0
- package/dist/tools/tfa-rca-utils/types.d.ts +104 -0
- package/dist/tools/tfa-rca-utils/types.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -334,7 +334,7 @@ As of now we support 20 tools.
|
|
|
334
334
|
List all test runs from the 'Shopping App' project that were executed last week and are currently marked in-progress
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
6. `updateTestRun` —
|
|
337
|
+
6. `updateTestRun` — Update a test run's name/state and/or add test cases to it.
|
|
338
338
|
**Prompt example**
|
|
339
339
|
|
|
340
340
|
```text
|
package/dist/config.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export declare class Config {
|
|
|
8
8
|
readonly USE_OWN_LOCAL_BINARY_PROCESS: boolean;
|
|
9
9
|
readonly REMOTE_MCP: boolean;
|
|
10
10
|
readonly UPLOAD_BASE_DIR: string | undefined;
|
|
11
|
-
|
|
11
|
+
readonly O11Y_TFA_RCA_BASE_URL: string;
|
|
12
|
+
readonly BROWSERSTACK_AUTOMATION_BASE_URL: string;
|
|
13
|
+
readonly BROWSERSTACK_O11Y_UI_BASE_URL: string;
|
|
14
|
+
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean, UPLOAD_BASE_DIR: string | undefined, O11Y_TFA_RCA_BASE_URL: string, BROWSERSTACK_AUTOMATION_BASE_URL: string, BROWSERSTACK_O11Y_UI_BASE_URL: string);
|
|
12
15
|
}
|
|
13
16
|
declare const config: Config;
|
|
14
17
|
export default config;
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,31 @@ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
|
|
|
28
28
|
browserstackLocalOptions[key] = envVar;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Default o11y base URL for the `tfaRcaTurn` collaborative-RCA tool —
|
|
33
|
+
* PRODUCTION (verified: api-automation.browserstack.com routes the
|
|
34
|
+
* /ext/v1 testRca + rcaChat endpoints). Same value for every user served by
|
|
35
|
+
* the process; overridable at startup via `O11Y_TFA_RCA_BASE_URL` to target a
|
|
36
|
+
* staging tenant (e.g. api-observability-<tenant>.bsstag.com) where a build's
|
|
37
|
+
* representatives actually live. Per-process config, never a per-call arg.
|
|
38
|
+
*/
|
|
39
|
+
const DEFAULT_O11Y_TFA_RCA_BASE_URL = "https://api-automation.browserstack.com";
|
|
40
|
+
/**
|
|
41
|
+
* Base URL for the Automate test-runs API (`/ext/v1/builds/{id}/testRuns`) that
|
|
42
|
+
* `listTestIds` calls. Overridable at startup via `BROWSERSTACK_AUTOMATION_BASE_URL`
|
|
43
|
+
* so the tool can target a non-prod env (e.g. rengg-tfa staging) where a build
|
|
44
|
+
* actually lives, instead of the prod default. Per-process config, never a
|
|
45
|
+
* per-call arg.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL = "https://api-automation.browserstack.com";
|
|
48
|
+
/**
|
|
49
|
+
* Base URL of the Test Observability (TRA) web UI used to build the
|
|
50
|
+
* human-facing "view the full report" links returned by the RCA tools.
|
|
51
|
+
* Same value for every user served by the process; overridable at startup via
|
|
52
|
+
* `BROWSERSTACK_O11Y_UI_BASE_URL` (e.g. to point at a staging UI). Per-process
|
|
53
|
+
* config, never a per-call arg.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL = "https://automation.browserstack.com";
|
|
31
56
|
/**
|
|
32
57
|
* USE_OWN_LOCAL_BINARY_PROCESS:
|
|
33
58
|
* If true, the system will not start a new local binary process, but will use the user's own process.
|
|
@@ -38,15 +63,30 @@ export class Config {
|
|
|
38
63
|
USE_OWN_LOCAL_BINARY_PROCESS;
|
|
39
64
|
REMOTE_MCP;
|
|
40
65
|
UPLOAD_BASE_DIR;
|
|
41
|
-
|
|
66
|
+
O11Y_TFA_RCA_BASE_URL;
|
|
67
|
+
BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
68
|
+
BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
69
|
+
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP, UPLOAD_BASE_DIR, O11Y_TFA_RCA_BASE_URL, BROWSERSTACK_AUTOMATION_BASE_URL, BROWSERSTACK_O11Y_UI_BASE_URL) {
|
|
42
70
|
this.DEV_MODE = DEV_MODE;
|
|
43
71
|
this.browserstackLocalOptions = browserstackLocalOptions;
|
|
44
72
|
this.USE_OWN_LOCAL_BINARY_PROCESS = USE_OWN_LOCAL_BINARY_PROCESS;
|
|
45
73
|
this.REMOTE_MCP = REMOTE_MCP;
|
|
46
74
|
this.UPLOAD_BASE_DIR = UPLOAD_BASE_DIR;
|
|
75
|
+
this.O11Y_TFA_RCA_BASE_URL = O11Y_TFA_RCA_BASE_URL;
|
|
76
|
+
this.BROWSERSTACK_AUTOMATION_BASE_URL = BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
77
|
+
this.BROWSERSTACK_O11Y_UI_BASE_URL = BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
47
78
|
}
|
|
48
79
|
}
|
|
49
80
|
const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOptions, process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true", process.env.REMOTE_MCP === "true", process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0
|
|
50
81
|
? process.env.MCP_UPLOAD_BASE_DIR
|
|
51
|
-
: undefined
|
|
82
|
+
: undefined, process.env.O11Y_TFA_RCA_BASE_URL &&
|
|
83
|
+
process.env.O11Y_TFA_RCA_BASE_URL.length > 0
|
|
84
|
+
? process.env.O11Y_TFA_RCA_BASE_URL
|
|
85
|
+
: DEFAULT_O11Y_TFA_RCA_BASE_URL, process.env.BROWSERSTACK_AUTOMATION_BASE_URL &&
|
|
86
|
+
process.env.BROWSERSTACK_AUTOMATION_BASE_URL.length > 0
|
|
87
|
+
? process.env.BROWSERSTACK_AUTOMATION_BASE_URL
|
|
88
|
+
: DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL, process.env.BROWSERSTACK_O11Y_UI_BASE_URL &&
|
|
89
|
+
process.env.BROWSERSTACK_O11Y_UI_BASE_URL.length > 0
|
|
90
|
+
? process.env.BROWSERSTACK_O11Y_UI_BASE_URL
|
|
91
|
+
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL);
|
|
52
92
|
export default config;
|
package/dist/server-factory.js
CHANGED
|
@@ -16,6 +16,7 @@ import addAppLiveTools from "./tools/applive.js";
|
|
|
16
16
|
import addBuildInsightsTools from "./tools/build-insights.js";
|
|
17
17
|
import { setupOnInitialized } from "./oninitialized.js";
|
|
18
18
|
import addRCATools from "./tools/rca-agent.js";
|
|
19
|
+
import addTfaRcaCollaborationTools from "./tools/tfa-rca-collaboration.js";
|
|
19
20
|
/**
|
|
20
21
|
* Wrapper class for BrowserStack MCP Server
|
|
21
22
|
* Stores a map of registered tools by name
|
|
@@ -51,6 +52,7 @@ export class BrowserStackMcpServer {
|
|
|
51
52
|
addSelfHealTools,
|
|
52
53
|
addBuildInsightsTools,
|
|
53
54
|
addRCATools,
|
|
55
|
+
addTfaRcaCollaborationTools,
|
|
54
56
|
];
|
|
55
57
|
toolAdders.forEach((adder) => {
|
|
56
58
|
// Each adder now returns a Record<string, Tool>
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { TestStatus } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Base URL for the Automate test-runs API used by `listTestIds`. Process-startup
|
|
5
|
+
* config resolved in `src/config.ts` from `BROWSERSTACK_AUTOMATION_BASE_URL`
|
|
6
|
+
* (default prod). Set it to a rengg/staging host to list a build that lives
|
|
7
|
+
* there. Read per call so a per-server-instance config is honored; never read
|
|
8
|
+
* `process.env` here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getAutomationBaseUrl(): string;
|
|
3
11
|
export declare const FETCH_RCA_PARAMS: {
|
|
4
12
|
testId: z.ZodArray<z.ZodNumber>;
|
|
5
13
|
};
|
|
@@ -10,4 +18,5 @@ export declare const GET_BUILD_ID_PARAMS: {
|
|
|
10
18
|
export declare const LIST_TEST_IDS_PARAMS: {
|
|
11
19
|
buildId: z.ZodString;
|
|
12
20
|
status: z.ZodEnum<typeof TestStatus>;
|
|
21
|
+
includeFailureDetail: z.ZodOptional<z.ZodBoolean>;
|
|
13
22
|
};
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import appConfig from "../../config.js";
|
|
2
3
|
import { TestStatus } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Base URL for the Automate test-runs API used by `listTestIds`. Process-startup
|
|
6
|
+
* config resolved in `src/config.ts` from `BROWSERSTACK_AUTOMATION_BASE_URL`
|
|
7
|
+
* (default prod). Set it to a rengg/staging host to list a build that lives
|
|
8
|
+
* there. Read per call so a per-server-instance config is honored; never read
|
|
9
|
+
* `process.env` here.
|
|
10
|
+
*/
|
|
11
|
+
export function getAutomationBaseUrl() {
|
|
12
|
+
return appConfig.BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
13
|
+
}
|
|
3
14
|
export const FETCH_RCA_PARAMS = {
|
|
4
15
|
testId: z
|
|
5
16
|
.array(z.number().int())
|
|
@@ -21,4 +32,8 @@ export const LIST_TEST_IDS_PARAMS = {
|
|
|
21
32
|
status: z
|
|
22
33
|
.nativeEnum(TestStatus)
|
|
23
34
|
.describe("Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"),
|
|
35
|
+
includeFailureDetail: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Add per-test failure signature for clustering. Default false."),
|
|
24
39
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { TestStatus, FailedTestInfo, TestDetails } from "./types.js";
|
|
2
|
-
export declare function getTestIds(buildId: string, authString: string, status?: TestStatus): Promise<FailedTestInfo[]>;
|
|
3
|
-
export declare function extractFailedTestIds(hierarchy: TestDetails[], status?: TestStatus): FailedTestInfo[];
|
|
2
|
+
export declare function getTestIds(buildId: string, authString: string, status?: TestStatus, includeFailureDetail?: boolean): Promise<FailedTestInfo[]>;
|
|
3
|
+
export declare function extractFailedTestIds(hierarchy: TestDetails[], status?: TestStatus, includeFailureDetail?: boolean): FailedTestInfo[];
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import logger from "../../logger.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { getAutomationBaseUrl } from "./constants.js";
|
|
3
|
+
// Cap on the failure summary line — keep the response payload lean (we never
|
|
4
|
+
// return full stack traces into the MCP client's context window).
|
|
5
|
+
const ERROR_SUMMARY_MAX = 200;
|
|
6
|
+
export async function getTestIds(buildId, authString, status, includeFailureDetail = false) {
|
|
7
|
+
const baseUrl = `${getAutomationBaseUrl()}/ext/v1/builds/${buildId}/testRuns`;
|
|
4
8
|
let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
|
|
5
9
|
let allFailedTests = [];
|
|
6
10
|
let requestNumber = 0;
|
|
@@ -22,7 +26,7 @@ export async function getTestIds(buildId, authString, status) {
|
|
|
22
26
|
const data = (await response.json());
|
|
23
27
|
// Extract failed IDs from current page
|
|
24
28
|
if (data.hierarchy && data.hierarchy.length > 0) {
|
|
25
|
-
const currentFailedTests = extractFailedTestIds(data.hierarchy, status);
|
|
29
|
+
const currentFailedTests = extractFailedTestIds(data.hierarchy, status, includeFailureDetail);
|
|
26
30
|
allFailedTests = allFailedTests.concat(currentFailedTests);
|
|
27
31
|
}
|
|
28
32
|
// Check for pagination termination conditions
|
|
@@ -46,23 +50,82 @@ export async function getTestIds(buildId, authString, status) {
|
|
|
46
50
|
throw error;
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
|
-
export function extractFailedTestIds(hierarchy, status) {
|
|
53
|
+
export function extractFailedTestIds(hierarchy, status, includeFailureDetail = false) {
|
|
50
54
|
let failedTests = [];
|
|
51
55
|
for (const node of hierarchy) {
|
|
56
|
+
// Match on status alone — the observability_url `details=<id>` check below
|
|
57
|
+
// already filters to real test nodes (suite/hook nodes carry no status and
|
|
58
|
+
// no such URL). Do NOT also require run_count: JUnit-uploaded builds report
|
|
59
|
+
// run_count=0 even for genuinely failed tests, which would drop them all.
|
|
52
60
|
if (node.details?.status === status) {
|
|
53
61
|
if (node.details?.observability_url) {
|
|
54
62
|
const idMatch = node.details.observability_url.match(/details=(\d+)/);
|
|
55
63
|
if (idMatch) {
|
|
56
|
-
|
|
64
|
+
const entry = {
|
|
57
65
|
test_id: idMatch[1],
|
|
58
66
|
test_name: node.display_name || `Test ${idMatch[1]}`,
|
|
59
|
-
}
|
|
67
|
+
};
|
|
68
|
+
if (includeFailureDetail) {
|
|
69
|
+
const signature = buildFailureSignature(node.details);
|
|
70
|
+
if (signature)
|
|
71
|
+
entry.failure = signature;
|
|
72
|
+
}
|
|
73
|
+
failedTests.push(entry);
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
if (node.children && node.children.length > 0) {
|
|
64
|
-
failedTests = failedTests.concat(extractFailedTestIds(node.children, status));
|
|
78
|
+
failedTests = failedTests.concat(extractFailedTestIds(node.children, status, includeFailureDetail));
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
return failedTests;
|
|
68
82
|
}
|
|
83
|
+
// Build a trimmed failure signature from a test node's `details`. Returns
|
|
84
|
+
// undefined when no signal is available so the field is simply omitted.
|
|
85
|
+
function buildFailureSignature(details) {
|
|
86
|
+
if (!details)
|
|
87
|
+
return undefined;
|
|
88
|
+
const signature = {};
|
|
89
|
+
if (details.failure_categories != null) {
|
|
90
|
+
signature.category = Array.isArray(details.failure_categories)
|
|
91
|
+
? details.failure_categories.filter(Boolean).join(", ")
|
|
92
|
+
: String(details.failure_categories);
|
|
93
|
+
}
|
|
94
|
+
const errorSummary = extractFirstFailureLine(details);
|
|
95
|
+
if (errorSummary)
|
|
96
|
+
signature.error_summary = errorSummary;
|
|
97
|
+
if (details.file_path)
|
|
98
|
+
signature.file_path = String(details.file_path);
|
|
99
|
+
if (typeof details.is_flaky === "boolean")
|
|
100
|
+
signature.is_flaky = details.is_flaky;
|
|
101
|
+
if (typeof details.is_always_failing === "boolean")
|
|
102
|
+
signature.is_always_failing = details.is_always_failing;
|
|
103
|
+
if (typeof details.is_new_failure === "boolean")
|
|
104
|
+
signature.is_new_failure = details.is_new_failure;
|
|
105
|
+
return Object.keys(signature).length > 0 ? signature : undefined;
|
|
106
|
+
}
|
|
107
|
+
// First non-empty line of the first retry's TEST_FAILURE log, capped. Handles
|
|
108
|
+
// both string entries and object entries ({ message } / { text }).
|
|
109
|
+
function extractFirstFailureLine(details) {
|
|
110
|
+
const retries = details?.retries;
|
|
111
|
+
if (!Array.isArray(retries))
|
|
112
|
+
return undefined;
|
|
113
|
+
for (const retry of retries) {
|
|
114
|
+
const failures = retry?.logs?.TEST_FAILURE;
|
|
115
|
+
if (!failures)
|
|
116
|
+
continue;
|
|
117
|
+
const entries = Array.isArray(failures) ? failures : [failures];
|
|
118
|
+
for (const failure of entries) {
|
|
119
|
+
const text = typeof failure === "string"
|
|
120
|
+
? failure
|
|
121
|
+
: (failure?.message ?? failure?.text ?? "");
|
|
122
|
+
const firstLine = String(text)
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => line.trim())
|
|
125
|
+
.find((line) => line.length > 0);
|
|
126
|
+
if (firstLine)
|
|
127
|
+
return firstLine.slice(0, ERROR_SUMMARY_MAX);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
@@ -17,9 +17,18 @@ export interface TestRun {
|
|
|
17
17
|
next_page: string | null;
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
export interface TestFailureSignature {
|
|
21
|
+
category?: string;
|
|
22
|
+
error_summary?: string;
|
|
23
|
+
file_path?: string;
|
|
24
|
+
is_flaky?: boolean;
|
|
25
|
+
is_always_failing?: boolean;
|
|
26
|
+
is_new_failure?: boolean;
|
|
27
|
+
}
|
|
20
28
|
export interface FailedTestInfo {
|
|
21
29
|
test_id: number;
|
|
22
30
|
test_name: string;
|
|
31
|
+
failure?: TestFailureSignature;
|
|
23
32
|
}
|
|
24
33
|
export declare enum RCAState {
|
|
25
34
|
PENDING = "pending",
|
|
@@ -11,5 +11,6 @@ export declare function fetchRCADataTool(args: {
|
|
|
11
11
|
export declare function listTestIdsTool(args: {
|
|
12
12
|
buildId: string;
|
|
13
13
|
status?: TestStatus;
|
|
14
|
+
includeFailureDetail?: boolean;
|
|
14
15
|
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
15
16
|
export default function addRCATools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
|
package/dist/tools/rca-agent.js
CHANGED
|
@@ -101,10 +101,10 @@ export async function fetchRCADataTool(args, config) {
|
|
|
101
101
|
}
|
|
102
102
|
export async function listTestIdsTool(args, config) {
|
|
103
103
|
try {
|
|
104
|
-
const { buildId, status } = args;
|
|
104
|
+
const { buildId, status, includeFailureDetail } = args;
|
|
105
105
|
const authString = getBrowserStackAuth(config);
|
|
106
106
|
// Get test IDs
|
|
107
|
-
const testIds = await getTestIds(buildId, authString, status);
|
|
107
|
+
const testIds = await getTestIds(buildId, authString, status, includeFailureDetail);
|
|
108
108
|
return {
|
|
109
109
|
content: [
|
|
110
110
|
{
|
|
@@ -65,7 +65,13 @@ export async function listTestCases(args, config) {
|
|
|
65
65
|
const count = info?.count ?? test_cases.length;
|
|
66
66
|
// Summary for more focused output
|
|
67
67
|
const summary = test_cases
|
|
68
|
-
.map((tc) =>
|
|
68
|
+
.map((tc) => {
|
|
69
|
+
const links = (tc.issues ?? [])
|
|
70
|
+
.filter((i) => i?.issue_type && i?.jira_id)
|
|
71
|
+
.map((i) => `${i.issue_type}:${i.jira_id}`)
|
|
72
|
+
.join(", ");
|
|
73
|
+
return `• ${tc.identifier}: ${tc.title} [${tc.case_type} | ${tc.priority}]${links ? ` {linked: ${links}}` : ""}`;
|
|
74
|
+
})
|
|
69
75
|
.join("\n");
|
|
70
76
|
return {
|
|
71
77
|
content: [
|
|
@@ -17,11 +17,26 @@ export declare const UpdateTestRunSchema: z.ZodObject<{
|
|
|
17
17
|
rejected: "rejected";
|
|
18
18
|
closed: "closed";
|
|
19
19
|
}>>;
|
|
20
|
+
add_test_cases: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
21
|
+
test_case_ids: z.ZodArray<z.ZodString>;
|
|
22
|
+
configuration_ids: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
23
|
+
}, z.core.$strip>>>;
|
|
24
|
+
preserve_existing_results: z.ZodOptional<z.ZodBoolean>;
|
|
20
25
|
}, z.core.$strip>;
|
|
21
26
|
}, z.core.$strip>;
|
|
22
27
|
type UpdateTestRunArgs = z.infer<typeof UpdateTestRunSchema>;
|
|
23
28
|
/**
|
|
24
29
|
* Partially updates an existing test run.
|
|
30
|
+
*
|
|
31
|
+
* Dispatches to one of two BrowserStack endpoints based on the fields provided,
|
|
32
|
+
* mirroring how the platform splits these concerns across two endpoints:
|
|
33
|
+
* - metadata (name / run_state) -> PATCH .../test-runs/{id}/update
|
|
34
|
+
* - adding test cases -> PATCH .../test-runs/{id}/test-cases
|
|
35
|
+
*
|
|
36
|
+
* Either or both may be supplied in one call; each provided concern hits its
|
|
37
|
+
* own endpoint and both outcomes are reported. At least one must be provided.
|
|
38
|
+
* Removing test cases is intentionally not exposed — this tool is
|
|
39
|
+
* non-destructive.
|
|
25
40
|
*/
|
|
26
41
|
export declare function updateTestRun(args: UpdateTestRunArgs, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
27
42
|
export {};
|
|
@@ -3,6 +3,19 @@ import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { formatAxiosError } from "../../lib/error.js";
|
|
5
5
|
import { getTMBaseURL } from "../../lib/tm-base-url.js";
|
|
6
|
+
/**
|
|
7
|
+
* Selection of test cases (with optional configurations) to add.
|
|
8
|
+
*/
|
|
9
|
+
const TestCaseSelectionSchema = z.object({
|
|
10
|
+
test_case_ids: z
|
|
11
|
+
.array(z.string())
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Test case IDs, e.g. TC-123"),
|
|
14
|
+
configuration_ids: z
|
|
15
|
+
.array(z.number())
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Configuration IDs to apply"),
|
|
18
|
+
});
|
|
6
19
|
/**
|
|
7
20
|
* Schema for updating a test run with partial fields.
|
|
8
21
|
*/
|
|
@@ -24,22 +37,82 @@ export const UpdateTestRunSchema = z.object({
|
|
|
24
37
|
])
|
|
25
38
|
.optional()
|
|
26
39
|
.describe("Updated state of the test run"),
|
|
40
|
+
add_test_cases: z
|
|
41
|
+
.array(TestCaseSelectionSchema)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Test cases to add to the run"),
|
|
44
|
+
preserve_existing_results: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Keep existing results when adding cases (default true)"),
|
|
27
48
|
}),
|
|
28
49
|
});
|
|
50
|
+
/**
|
|
51
|
+
* Builds the HTTP Basic auth header from per-request config credentials.
|
|
52
|
+
*/
|
|
53
|
+
function buildAuthHeader(config) {
|
|
54
|
+
return "Basic " + Buffer.from(getBrowserStackAuth(config)).toString("base64");
|
|
55
|
+
}
|
|
29
56
|
/**
|
|
30
57
|
* Partially updates an existing test run.
|
|
58
|
+
*
|
|
59
|
+
* Dispatches to one of two BrowserStack endpoints based on the fields provided,
|
|
60
|
+
* mirroring how the platform splits these concerns across two endpoints:
|
|
61
|
+
* - metadata (name / run_state) -> PATCH .../test-runs/{id}/update
|
|
62
|
+
* - adding test cases -> PATCH .../test-runs/{id}/test-cases
|
|
63
|
+
*
|
|
64
|
+
* Either or both may be supplied in one call; each provided concern hits its
|
|
65
|
+
* own endpoint and both outcomes are reported. At least one must be provided.
|
|
66
|
+
* Removing test cases is intentionally not exposed — this tool is
|
|
67
|
+
* non-destructive.
|
|
31
68
|
*/
|
|
32
69
|
export async function updateTestRun(args, config) {
|
|
70
|
+
const { name, run_state, add_test_cases } = args.test_run;
|
|
71
|
+
const addIds = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
|
|
72
|
+
const hasTestCases = addIds.length > 0;
|
|
73
|
+
const hasMetadata = name !== undefined || run_state !== undefined;
|
|
74
|
+
if (!hasTestCases && !hasMetadata) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: "Nothing to update: provide name/run_state and/or add_test_cases.",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
isError: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const tmBaseUrl = await getTMBaseURL(config);
|
|
86
|
+
const authHeader = buildAuthHeader(config);
|
|
87
|
+
const tasks = [];
|
|
88
|
+
if (hasMetadata) {
|
|
89
|
+
tasks.push(updateTestRunMetadata(args, tmBaseUrl, authHeader));
|
|
90
|
+
}
|
|
91
|
+
if (hasTestCases) {
|
|
92
|
+
tasks.push(updateTestRunTestCases(args, tmBaseUrl, authHeader));
|
|
93
|
+
}
|
|
94
|
+
const results = await Promise.all(tasks);
|
|
95
|
+
if (results.length === 1) {
|
|
96
|
+
return results[0];
|
|
97
|
+
}
|
|
98
|
+
// Both concerns updated: aggregate outcomes; surface an error if either failed.
|
|
99
|
+
return {
|
|
100
|
+
content: results.flatMap((r) => r.content),
|
|
101
|
+
isError: results.some((r) => r.isError),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Updates test run metadata (name / run_state) via the /update endpoint.
|
|
106
|
+
*/
|
|
107
|
+
async function updateTestRunMetadata(args, baseUrl, authHeader) {
|
|
33
108
|
try {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const url = `${
|
|
37
|
-
const authString = getBrowserStackAuth(config);
|
|
38
|
-
const [username, password] = authString.split(":");
|
|
109
|
+
const { name, run_state } = args.test_run;
|
|
110
|
+
const body = { test_run: { name, run_state } };
|
|
111
|
+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;
|
|
39
112
|
const resp = await apiClient.patch({
|
|
40
113
|
url,
|
|
41
114
|
headers: {
|
|
42
|
-
Authorization:
|
|
115
|
+
Authorization: authHeader,
|
|
43
116
|
"Content-Type": "application/json",
|
|
44
117
|
},
|
|
45
118
|
body,
|
|
@@ -70,3 +143,52 @@ export async function updateTestRun(args, config) {
|
|
|
70
143
|
return formatAxiosError(err, "Failed to update test run");
|
|
71
144
|
}
|
|
72
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Adds test cases to a run via the /test-cases endpoint.
|
|
148
|
+
* This call is applied asynchronously by the backend.
|
|
149
|
+
*/
|
|
150
|
+
async function updateTestRunTestCases(args, baseUrl, authHeader) {
|
|
151
|
+
try {
|
|
152
|
+
const { add_test_cases, preserve_existing_results } = args.test_run;
|
|
153
|
+
const body = {
|
|
154
|
+
test_run: {
|
|
155
|
+
add_test_cases,
|
|
156
|
+
preserve_existing_results: preserve_existing_results ?? true,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const url = `${baseUrl}/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-runs/${encodeURIComponent(args.test_run_id)}/test-cases`;
|
|
160
|
+
const resp = await apiClient.patch({
|
|
161
|
+
url,
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: authHeader,
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
},
|
|
166
|
+
body,
|
|
167
|
+
});
|
|
168
|
+
const data = resp.data;
|
|
169
|
+
if (!data.success) {
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `Failed to update test run test cases: ${JSON.stringify(data)}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
isError: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const added = add_test_cases?.flatMap((s) => s.test_case_ids) ?? [];
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: `Queued test-case update for ${args.test_run_id} (added ${added.length}); changes apply asynchronously.`,
|
|
186
|
+
},
|
|
187
|
+
{ type: "text", text: JSON.stringify(data, null, 2) },
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
return formatAxiosError(err, "Failed to update test run test cases");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -400,7 +400,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
400
400
|
tools.listTestCaseTemplates = server.tool("listTestCaseTemplates", "List test-case templates with their numeric template_id. Use the id with createTestCase to apply a custom template (the 'template' slug only selects system templates).", ListTemplatesSchema.shape, (args) => listTemplatesTool(args, config, server));
|
|
401
401
|
tools.createTestRun = server.tool("createTestRun", "Create a test run in BrowserStack Test Management.", CreateTestRunSchema.shape, (args) => createTestRunTool(args, config, server));
|
|
402
402
|
tools.listTestRuns = server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)", ListTestRunsSchema.shape, (args) => listTestRunsTool(args, config, server));
|
|
403
|
-
tools.updateTestRun = server.tool("updateTestRun", "Update a test run
|
|
403
|
+
tools.updateTestRun = server.tool("updateTestRun", "Update a test run's metadata and/or add test cases to it.", UpdateTestRunSchema.shape, (args) => updateTestRunTool(args, config, server));
|
|
404
404
|
tools.addTestResult = server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API.", AddTestResultSchema.shape, (args) => addTestResultTool(args, config, server));
|
|
405
405
|
tools.uploadProductRequirementFile = server.tool("uploadProductRequirementFile", "Upload files (e.g., PDRs, PDFs) to BrowserStack Test Management and retrieve a file mapping ID. This is utilized for generating test cases from files and is part of the Test Case Generator AI Agent in BrowserStack.", UploadFileSchema.shape, (args) => uploadProductRequirementFileTool(args, config, server));
|
|
406
406
|
tools.createTestCasesFromFile = server.tool("createTestCasesFromFile", "Generate test cases from a file in BrowserStack Test Management using the Test Case Generator AI Agent.", CreateTestCasesFromFileSchema.shape, (args, context) => createTestCasesFromFileTool(args, context, config, server));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { BrowserStackConfig } from "../lib/types.js";
|
|
4
|
+
import { TfaRcaTurnArgs } from "./tfa-rca-utils/submit-turn.js";
|
|
5
|
+
import { TriggerRcaReportArgs } from "./tfa-rca-utils/trigger-report.js";
|
|
6
|
+
export declare function tfaRcaTurnTool(args: TfaRcaTurnArgs, config: BrowserStackConfig, context?: any): Promise<CallToolResult>;
|
|
7
|
+
export declare function triggerRcaReportTool(args: TriggerRcaReportArgs, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
8
|
+
export default function addTfaRcaCollaborationTools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
2
|
+
import { handleMCPError } from "../lib/utils.js";
|
|
3
|
+
import { TFA_RCA_TURN_PARAMS, TRIGGER_RCA_REPORT_PARAMS, } from "./tfa-rca-utils/constants.js";
|
|
4
|
+
import { submitTfaRcaTurn, TfaRcaTurnError, } from "./tfa-rca-utils/submit-turn.js";
|
|
5
|
+
import { triggerRcaReport, TriggerRcaReportError, } from "./tfa-rca-utils/trigger-report.js";
|
|
6
|
+
const TOOL_NAME = "tfaRcaTurn";
|
|
7
|
+
const TRIGGER_TOOL_NAME = "triggerRcaReport";
|
|
8
|
+
/** Wrap a domain error into the standard `{ isError: true }` envelope. */
|
|
9
|
+
function domainErrorResult(toolName, error) {
|
|
10
|
+
const readable = toolName.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: `Failed to ${readable}: ${error.message}`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function tfaRcaTurnTool(args, config, context) {
|
|
22
|
+
// The util returns the trimmed, status-discriminated contract; JSON.stringify
|
|
23
|
+
// drops the undefined slots, so the wrapper stays a plain serializer.
|
|
24
|
+
const result = await submitTfaRcaTurn(args, config, context);
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.stringify(result, null, 2),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function triggerRcaReportTool(args, config) {
|
|
35
|
+
const glimpse = await triggerRcaReport(args, config);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify(glimpse, null, 2),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export default function addTfaRcaCollaborationTools(server, config) {
|
|
46
|
+
const tools = {};
|
|
47
|
+
tools.tfaRcaTurn = server.tool(TOOL_NAME, "Submit one collaborative RCA turn for a test run to the TFA agent; returns status, asks, and RCA.", TFA_RCA_TURN_PARAMS, async (args, context) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await tfaRcaTurnTool(args, config, context);
|
|
50
|
+
trackMCP(TOOL_NAME, server.server.getClientVersion(), undefined, config);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Domain failures carry a client-safe, group-scope-safe message.
|
|
55
|
+
if (error instanceof TfaRcaTurnError) {
|
|
56
|
+
trackMCP(TOOL_NAME, server.server.getClientVersion(), error, config);
|
|
57
|
+
return domainErrorResult(TOOL_NAME, error);
|
|
58
|
+
}
|
|
59
|
+
return handleMCPError(TOOL_NAME, server, config, error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
tools.triggerRcaReport = server.tool(TRIGGER_TOOL_NAME, "Trigger or read a build's Release Readiness report; returns a verdict glimpse and a UI link.", TRIGGER_RCA_REPORT_PARAMS, async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
const result = await triggerRcaReportTool(args, config);
|
|
65
|
+
trackMCP(TRIGGER_TOOL_NAME, server.server.getClientVersion(), undefined, config);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// Domain failures carry a client-safe, group-scope-safe message.
|
|
70
|
+
if (error instanceof TriggerRcaReportError) {
|
|
71
|
+
trackMCP(TRIGGER_TOOL_NAME, server.server.getClientVersion(), error, config);
|
|
72
|
+
return domainErrorResult(TRIGGER_TOOL_NAME, error);
|
|
73
|
+
}
|
|
74
|
+
return handleMCPError(TRIGGER_TOOL_NAME, server, config, error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return tools;
|
|
78
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* O11y base for the `rcaChat` proxy. The MCP server talks ONLY to o11y-api
|
|
4
|
+
* (boundary discipline, R9). The value is process-startup config resolved in
|
|
5
|
+
* `src/config.ts` from `O11Y_TFA_RCA_BASE_URL` (default: production) — never read
|
|
6
|
+
* `process.env` here. Resolved per call so a config built per server instance
|
|
7
|
+
* (multi-tenant) is honored.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getO11yBaseUrl(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Test Observability (TRA) web UI base for human-facing "view report" links.
|
|
12
|
+
* Startup config (`BROWSERSTACK_O11Y_UI_BASE_URL` in `src/config.ts`, default
|
|
13
|
+
* observability.browserstack.com) — never read `process.env` here.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getO11yUiBaseUrl(): string;
|
|
16
|
+
/**
|
|
17
|
+
* TRA UI deep-link for a build's AI report (confirmed shape, 2026-07-13):
|
|
18
|
+
* `<UI_BASE>/builds/<buildUuid>?tab=ai_report&subTab=aitfa` — the AI-TFA
|
|
19
|
+
* sub-tab of the build's AI report. `{buildUuid}` is replaced with the
|
|
20
|
+
* caller-supplied build id.
|
|
21
|
+
*/
|
|
22
|
+
export declare const O11Y_UI_BUILD_PATH = "/builds/{buildUuid}?tab=ai_report&subTab=aitfa";
|
|
23
|
+
/** Human-facing TRA UI link for one build's full report. */
|
|
24
|
+
export declare function getO11yUiBuildUrl(buildUuid: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Generic TRA UI pointer used on RESOLVED turns where only a testRunId is
|
|
27
|
+
* known (no buildUuid to deep-link). The full RCA lives on the dashboard
|
|
28
|
+
* (build page → AI report → AI TFA sub-tab).
|
|
29
|
+
*/
|
|
30
|
+
export declare function getRcaViewGuidance(): string;
|
|
31
|
+
/** Trigger (or read, when already complete) a build's Release Readiness report. */
|
|
32
|
+
export declare const RELEASE_READINESS_TRIGGER_PATH = "/ext/v1/ai/builds/{buildUuid}/releaseReadiness/trigger";
|
|
33
|
+
/** Submit one collaborative turn for a test run. */
|
|
34
|
+
export declare const RCA_CHAT_SUBMIT_PATH = "/ext/v1/testRuns/{testRunId}/rcaChat";
|
|
35
|
+
/** Poll a submitted turn to completion. */
|
|
36
|
+
export declare const RCA_CHAT_POLL_PATH = "/ext/v1/testRuns/{testRunId}/rcaChat/{turnId}";
|
|
37
|
+
/** Initial wait before the first poll GET. */
|
|
38
|
+
export declare const POLL_INITIAL_DELAY_MS = 2000;
|
|
39
|
+
/** Interval between poll GETs. */
|
|
40
|
+
export declare const POLL_INTERVAL_MS = 3000;
|
|
41
|
+
/** Wall-clock cap for the in-call poll; exceeding it yields a soft PENDING. */
|
|
42
|
+
export declare const POLL_MAX_WAIT_MS: number;
|
|
43
|
+
/** Max length of the digest message, matching o11y's request `@Size`. */
|
|
44
|
+
export declare const MESSAGE_MAX_LENGTH = 5000;
|
|
45
|
+
/** Max chars of `root_cause` surfaced in the RESOLVED glimpse. */
|
|
46
|
+
export declare const RCA_GLIMPSE_ROOT_CAUSE_MAX = 220;
|
|
47
|
+
/**
|
|
48
|
+
* Zod param shapes for the `tfaRcaTurn` tool, exported as a
|
|
49
|
+
* `Record<string, ZodType>` mirroring `rca-agent-utils/constants.ts`.
|
|
50
|
+
* No credential fields (security rule). Each `.describe()` ≤ 60 chars.
|
|
51
|
+
*/
|
|
52
|
+
export declare const TFA_RCA_TURN_PARAMS: {
|
|
53
|
+
testRunId: z.ZodString;
|
|
54
|
+
message: z.ZodString;
|
|
55
|
+
threadId: z.ZodOptional<z.ZodString>;
|
|
56
|
+
turnId: z.ZodOptional<z.ZodString>;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Zod param shapes for the `triggerRcaReport` tool. No credential fields
|
|
60
|
+
* (security rule). Each `.describe()` ≤ 60 chars.
|
|
61
|
+
*/
|
|
62
|
+
export declare const TRIGGER_RCA_REPORT_PARAMS: {
|
|
63
|
+
buildUuid: z.ZodString;
|
|
64
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import appConfig from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* O11y base for the `rcaChat` proxy. The MCP server talks ONLY to o11y-api
|
|
5
|
+
* (boundary discipline, R9). The value is process-startup config resolved in
|
|
6
|
+
* `src/config.ts` from `O11Y_TFA_RCA_BASE_URL` (default: production) — never read
|
|
7
|
+
* `process.env` here. Resolved per call so a config built per server instance
|
|
8
|
+
* (multi-tenant) is honored.
|
|
9
|
+
*/
|
|
10
|
+
export function getO11yBaseUrl() {
|
|
11
|
+
return appConfig.O11Y_TFA_RCA_BASE_URL;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Test Observability (TRA) web UI base for human-facing "view report" links.
|
|
15
|
+
* Startup config (`BROWSERSTACK_O11Y_UI_BASE_URL` in `src/config.ts`, default
|
|
16
|
+
* observability.browserstack.com) — never read `process.env` here.
|
|
17
|
+
*/
|
|
18
|
+
export function getO11yUiBaseUrl() {
|
|
19
|
+
return appConfig.BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* TRA UI deep-link for a build's AI report (confirmed shape, 2026-07-13):
|
|
23
|
+
* `<UI_BASE>/builds/<buildUuid>?tab=ai_report&subTab=aitfa` — the AI-TFA
|
|
24
|
+
* sub-tab of the build's AI report. `{buildUuid}` is replaced with the
|
|
25
|
+
* caller-supplied build id.
|
|
26
|
+
*/
|
|
27
|
+
export const O11Y_UI_BUILD_PATH = "/builds/{buildUuid}?tab=ai_report&subTab=aitfa";
|
|
28
|
+
/** Human-facing TRA UI link for one build's full report. */
|
|
29
|
+
export function getO11yUiBuildUrl(buildUuid) {
|
|
30
|
+
return (getO11yUiBaseUrl() +
|
|
31
|
+
O11Y_UI_BUILD_PATH.replace("{buildUuid}", encodeURIComponent(buildUuid)));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generic TRA UI pointer used on RESOLVED turns where only a testRunId is
|
|
35
|
+
* known (no buildUuid to deep-link). The full RCA lives on the dashboard
|
|
36
|
+
* (build page → AI report → AI TFA sub-tab).
|
|
37
|
+
*/
|
|
38
|
+
export function getRcaViewGuidance() {
|
|
39
|
+
return `${getO11yUiBaseUrl()} — open the build's AI report (tab=ai_report, subTab=aitfa) to view the full RCA`;
|
|
40
|
+
}
|
|
41
|
+
/** Trigger (or read, when already complete) a build's Release Readiness report. */
|
|
42
|
+
export const RELEASE_READINESS_TRIGGER_PATH = "/ext/v1/ai/builds/{buildUuid}/releaseReadiness/trigger";
|
|
43
|
+
/** Submit one collaborative turn for a test run. */
|
|
44
|
+
export const RCA_CHAT_SUBMIT_PATH = "/ext/v1/testRuns/{testRunId}/rcaChat";
|
|
45
|
+
/** Poll a submitted turn to completion. */
|
|
46
|
+
export const RCA_CHAT_POLL_PATH = "/ext/v1/testRuns/{testRunId}/rcaChat/{turnId}";
|
|
47
|
+
/** Initial wait before the first poll GET. */
|
|
48
|
+
export const POLL_INITIAL_DELAY_MS = 2000;
|
|
49
|
+
/** Interval between poll GETs. */
|
|
50
|
+
export const POLL_INTERVAL_MS = 3000;
|
|
51
|
+
/** Wall-clock cap for the in-call poll; exceeding it yields a soft PENDING. */
|
|
52
|
+
export const POLL_MAX_WAIT_MS = 90 * 1000;
|
|
53
|
+
/** Max length of the digest message, matching o11y's request `@Size`. */
|
|
54
|
+
export const MESSAGE_MAX_LENGTH = 5000;
|
|
55
|
+
/** Max chars of `root_cause` surfaced in the RESOLVED glimpse. */
|
|
56
|
+
export const RCA_GLIMPSE_ROOT_CAUSE_MAX = 220;
|
|
57
|
+
/**
|
|
58
|
+
* Zod param shapes for the `tfaRcaTurn` tool, exported as a
|
|
59
|
+
* `Record<string, ZodType>` mirroring `rca-agent-utils/constants.ts`.
|
|
60
|
+
* No credential fields (security rule). Each `.describe()` ≤ 60 chars.
|
|
61
|
+
*/
|
|
62
|
+
export const TFA_RCA_TURN_PARAMS = {
|
|
63
|
+
testRunId: z.string().describe("Test run id to run RCA collaboration on."),
|
|
64
|
+
message: z
|
|
65
|
+
.string()
|
|
66
|
+
.max(MESSAGE_MAX_LENGTH)
|
|
67
|
+
.describe("Digested analysis to send this turn; no raw logs."),
|
|
68
|
+
threadId: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Thread id from prior turn; omit on first turn."),
|
|
72
|
+
turnId: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Turn id to resume a pending poll; usually omit."),
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Zod param shapes for the `triggerRcaReport` tool. No credential fields
|
|
79
|
+
* (security rule). Each `.describe()` ≤ 60 chars.
|
|
80
|
+
*/
|
|
81
|
+
export const TRIGGER_RCA_REPORT_PARAMS = {
|
|
82
|
+
buildUuid: z.string().describe("Automate build UUID to analyze."),
|
|
83
|
+
force: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Re-run even if a completed report exists."),
|
|
87
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
2
|
+
import { TfaRcaTurnResult } from "./types.js";
|
|
3
|
+
interface TurnContext {
|
|
4
|
+
sendNotification?: (notification: any) => Promise<void>;
|
|
5
|
+
_meta?: {
|
|
6
|
+
progressToken?: string | number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface TfaRcaTurnArgs {
|
|
10
|
+
testRunId: string;
|
|
11
|
+
message: string;
|
|
12
|
+
threadId?: string;
|
|
13
|
+
/** Resume polling an already-submitted turn without re-submitting. */
|
|
14
|
+
turnId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Domain error carrying a client-safe message. The tool maps these to a
|
|
18
|
+
* `{ isError: true }` envelope; the message never contains credentials.
|
|
19
|
+
*/
|
|
20
|
+
export declare class TfaRcaTurnError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Submit one collaborative RCA turn to the o11y `rcaChat` proxy and poll to
|
|
24
|
+
* completion, returning a trimmed structured result. Stateless: all identifiers
|
|
25
|
+
* are function-scoped; nothing persists between calls.
|
|
26
|
+
*/
|
|
27
|
+
export declare function submitTfaRcaTurn(args: TfaRcaTurnArgs, config: BrowserStackConfig, context?: TurnContext): Promise<TfaRcaTurnResult>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
3
|
+
import { getO11yBaseUrl, getRcaViewGuidance, POLL_INITIAL_DELAY_MS, POLL_INTERVAL_MS, POLL_MAX_WAIT_MS, RCA_CHAT_POLL_PATH, RCA_CHAT_SUBMIT_PATH, RCA_GLIMPSE_ROOT_CAUSE_MAX, } from "./constants.js";
|
|
4
|
+
import { PENDING_STATUS, TfaStatus, } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Domain error carrying a client-safe message. The tool maps these to a
|
|
7
|
+
* `{ isError: true }` envelope; the message never contains credentials.
|
|
8
|
+
*/
|
|
9
|
+
export class TfaRcaTurnError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
function buildAuthHeader(config) {
|
|
13
|
+
const authString = getBrowserStackAuth(config);
|
|
14
|
+
return `Basic ${Buffer.from(authString).toString("base64")}`;
|
|
15
|
+
}
|
|
16
|
+
async function notify(context, message, progress) {
|
|
17
|
+
if (!context?.sendNotification)
|
|
18
|
+
return;
|
|
19
|
+
await context.sendNotification({
|
|
20
|
+
method: "notifications/progress",
|
|
21
|
+
params: {
|
|
22
|
+
progressToken: context._meta?.progressToken?.toString() ?? "tfa-rca-turn",
|
|
23
|
+
message,
|
|
24
|
+
progress,
|
|
25
|
+
total: 100,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Map a raw status string from the wire onto the `TfaStatus` enum. */
|
|
30
|
+
function toTfaStatus(raw) {
|
|
31
|
+
switch (raw) {
|
|
32
|
+
case "RESOLVED":
|
|
33
|
+
return TfaStatus.RESOLVED;
|
|
34
|
+
case "BLOCKED":
|
|
35
|
+
return TfaStatus.BLOCKED;
|
|
36
|
+
default:
|
|
37
|
+
return TfaStatus.NEEDS_INFO;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Map one wire ask (snake_case) to the client `TfaAsk` (camelCase). */
|
|
41
|
+
function toAsk(raw) {
|
|
42
|
+
return {
|
|
43
|
+
what: raw?.what ?? "",
|
|
44
|
+
why: raw?.why ?? "",
|
|
45
|
+
evidenceType: raw?.evidence_type ?? "other",
|
|
46
|
+
priority: raw?.priority ?? "medium",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read the LLM-enforced structured turn from the completed `rcaChat` poll body.
|
|
51
|
+
*
|
|
52
|
+
* The poll envelope's own `status` field is the lifecycle state
|
|
53
|
+
* (`working`/`completed`/`failed`); the agent's `TurnResponse` is passed
|
|
54
|
+
* through under `turn` so its `status` (NEEDS_INFO/RESOLVED/BLOCKED) never
|
|
55
|
+
* collides with the lifecycle one. The agent's model validator guarantees the
|
|
56
|
+
* sub-object matching the turn status is present; we still default-fill the
|
|
57
|
+
* lists so the skill never sees `undefined`.
|
|
58
|
+
*/
|
|
59
|
+
function readStructuredTurn(data) {
|
|
60
|
+
const turn = data.turn ?? {};
|
|
61
|
+
const status = toTfaStatus(turn.status);
|
|
62
|
+
const needsInfo = turn.needs_info ?? {};
|
|
63
|
+
const blocked = turn.blocked ?? {};
|
|
64
|
+
return {
|
|
65
|
+
status,
|
|
66
|
+
confidence: turn.confidence ?? "unknown",
|
|
67
|
+
questions: Array.isArray(needsInfo.questions) ? needsInfo.questions : [],
|
|
68
|
+
asks: Array.isArray(needsInfo.asks) ? needsInfo.asks.map(toAsk) : [],
|
|
69
|
+
suggestions: Array.isArray(needsInfo.suggestions)
|
|
70
|
+
? needsInfo.suggestions
|
|
71
|
+
: [],
|
|
72
|
+
hypotheses: Array.isArray(needsInfo.hypotheses) ? needsInfo.hypotheses : [],
|
|
73
|
+
rca: status === TfaStatus.RESOLVED ? (turn.rca ?? undefined) : undefined,
|
|
74
|
+
reason: status === TfaStatus.BLOCKED ? blocked.reason : undefined,
|
|
75
|
+
unmetAsks: status === TfaStatus.BLOCKED && Array.isArray(blocked.unmet_asks)
|
|
76
|
+
? blocked.unmet_asks
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Truncate to `max` chars total (ellipsis included when cut). */
|
|
81
|
+
function truncate(text, max) {
|
|
82
|
+
if (text === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Trim a completed turn to the status-discriminated contract:
|
|
88
|
+
* - NEEDS_INFO: questions/asks/suggestions/hypotheses VERBATIM (the client
|
|
89
|
+
* loop consumes them).
|
|
90
|
+
* - RESOLVED: glimpse only (root_cause truncated, failure_type, related_prs)
|
|
91
|
+
* + a `viewRca` pointer — the full RCA lives on the TRA dashboard.
|
|
92
|
+
* - BLOCKED: reason + unmetAsks.
|
|
93
|
+
*/
|
|
94
|
+
function toTrimmedResult(turn, threadId) {
|
|
95
|
+
switch (turn.status) {
|
|
96
|
+
case TfaStatus.RESOLVED: {
|
|
97
|
+
const rca = turn.rca ?? {};
|
|
98
|
+
return {
|
|
99
|
+
status: turn.status,
|
|
100
|
+
confidence: turn.confidence,
|
|
101
|
+
threadId,
|
|
102
|
+
glimpse: {
|
|
103
|
+
root_cause: truncate(rca.root_cause, RCA_GLIMPSE_ROOT_CAUSE_MAX),
|
|
104
|
+
failure_type: rca.failure_type,
|
|
105
|
+
related_prs: rca.related_prs,
|
|
106
|
+
},
|
|
107
|
+
viewRca: getRcaViewGuidance(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
case TfaStatus.BLOCKED:
|
|
111
|
+
return {
|
|
112
|
+
status: turn.status,
|
|
113
|
+
confidence: turn.confidence,
|
|
114
|
+
threadId,
|
|
115
|
+
reason: turn.reason,
|
|
116
|
+
unmetAsks: turn.unmetAsks,
|
|
117
|
+
};
|
|
118
|
+
default:
|
|
119
|
+
return {
|
|
120
|
+
status: turn.status,
|
|
121
|
+
confidence: turn.confidence,
|
|
122
|
+
threadId,
|
|
123
|
+
questions: turn.questions,
|
|
124
|
+
asks: turn.asks,
|
|
125
|
+
suggestions: turn.suggestions,
|
|
126
|
+
hypotheses: turn.hypotheses,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Map a submit (POST) non-2xx into a clean, group-scope-safe domain error. */
|
|
131
|
+
function mapSubmitError(status) {
|
|
132
|
+
if (status === 403) {
|
|
133
|
+
return new TfaRcaTurnError("AI consent not enabled for this group");
|
|
134
|
+
}
|
|
135
|
+
if (status === 404) {
|
|
136
|
+
return new TfaRcaTurnError("test run not found for your group");
|
|
137
|
+
}
|
|
138
|
+
return new TfaRcaTurnError(`failed to submit RCA turn (status ${status})`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Submit one collaborative RCA turn to the o11y `rcaChat` proxy and poll to
|
|
142
|
+
* completion, returning a trimmed structured result. Stateless: all identifiers
|
|
143
|
+
* are function-scoped; nothing persists between calls.
|
|
144
|
+
*/
|
|
145
|
+
export async function submitTfaRcaTurn(args, config, context) {
|
|
146
|
+
const authHeader = buildAuthHeader(config);
|
|
147
|
+
const headers = {
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
Authorization: authHeader,
|
|
150
|
+
};
|
|
151
|
+
const baseUrl = getO11yBaseUrl();
|
|
152
|
+
let turnId = args.turnId;
|
|
153
|
+
let threadId = args.threadId;
|
|
154
|
+
// Submit only when we are not resuming an existing turn.
|
|
155
|
+
if (!turnId) {
|
|
156
|
+
const submitUrl = baseUrl + RCA_CHAT_SUBMIT_PATH.replace("{testRunId}", args.testRunId);
|
|
157
|
+
await notify(context, "Submitting RCA turn to TFA agent...", 5);
|
|
158
|
+
const body = {
|
|
159
|
+
message: args.message,
|
|
160
|
+
client_context: args.message,
|
|
161
|
+
};
|
|
162
|
+
if (args.threadId) {
|
|
163
|
+
body.thread_id = args.threadId;
|
|
164
|
+
}
|
|
165
|
+
const submitResponse = await apiClient.post({
|
|
166
|
+
url: submitUrl,
|
|
167
|
+
headers,
|
|
168
|
+
body,
|
|
169
|
+
raise_error: false,
|
|
170
|
+
});
|
|
171
|
+
if (!submitResponse.ok) {
|
|
172
|
+
throw mapSubmitError(submitResponse.status);
|
|
173
|
+
}
|
|
174
|
+
const data = submitResponse.data ?? {};
|
|
175
|
+
turnId = data.turnId;
|
|
176
|
+
threadId = data.threadId ?? threadId;
|
|
177
|
+
if (!turnId) {
|
|
178
|
+
throw new TfaRcaTurnError("turn expired or not found");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Poll to completion, soft-PENDING on wall-clock cap.
|
|
182
|
+
const pollUrl = baseUrl +
|
|
183
|
+
RCA_CHAT_POLL_PATH.replace("{testRunId}", args.testRunId).replace("{turnId}", turnId);
|
|
184
|
+
await delay(POLL_INITIAL_DELAY_MS);
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
while (true) {
|
|
187
|
+
const pollResponse = await apiClient.get({
|
|
188
|
+
url: pollUrl,
|
|
189
|
+
headers,
|
|
190
|
+
raise_error: false,
|
|
191
|
+
});
|
|
192
|
+
if (pollResponse.status === 404) {
|
|
193
|
+
throw new TfaRcaTurnError("turn expired or not found");
|
|
194
|
+
}
|
|
195
|
+
if (pollResponse.ok) {
|
|
196
|
+
const data = pollResponse.data ?? {};
|
|
197
|
+
const status = data.status;
|
|
198
|
+
threadId = data.threadId ?? threadId;
|
|
199
|
+
if (status === "failed") {
|
|
200
|
+
throw new TfaRcaTurnError(data.error || "TFA agent run failed");
|
|
201
|
+
}
|
|
202
|
+
if (status === "completed") {
|
|
203
|
+
const turn = readStructuredTurn(data);
|
|
204
|
+
await notify(context, "TFA agent turn complete.", 100);
|
|
205
|
+
return toTrimmedResult(turn, threadId);
|
|
206
|
+
}
|
|
207
|
+
// status === "working" (or any other in-progress value) → keep polling.
|
|
208
|
+
}
|
|
209
|
+
// Transient non-2xx (other than 404) during polling: classify and continue.
|
|
210
|
+
if (Date.now() - startTime >= POLL_MAX_WAIT_MS) {
|
|
211
|
+
await notify(context, "TFA agent still working; will resume later.", 90);
|
|
212
|
+
// PENDING keeps only what the skill needs to resume polling.
|
|
213
|
+
return {
|
|
214
|
+
status: PENDING_STATUS,
|
|
215
|
+
threadId,
|
|
216
|
+
turnId,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
await notify(context, "Waiting for TFA agent reply...", 50);
|
|
220
|
+
await delay(POLL_INTERVAL_MS);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
2
|
+
export interface TriggerRcaReportArgs {
|
|
3
|
+
buildUuid: string;
|
|
4
|
+
/** Re-run even if a completed report already exists. */
|
|
5
|
+
force?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Domain error carrying a client-safe message. The tool maps these to a
|
|
9
|
+
* `{ isError: true }` envelope; the message never contains credentials.
|
|
10
|
+
*/
|
|
11
|
+
export declare class TriggerRcaReportError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Trimmed glimpse of the Release Readiness report. The raw o11y response —
|
|
15
|
+
* including the `prs[]` and `workflows[]` arrays — is NEVER echoed; the full
|
|
16
|
+
* report lives on the Test Observability dashboard (`viewReport`).
|
|
17
|
+
*/
|
|
18
|
+
export interface RcaReportGlimpse {
|
|
19
|
+
state?: string;
|
|
20
|
+
verdict?: string;
|
|
21
|
+
verdictProvisional?: boolean;
|
|
22
|
+
partial?: boolean;
|
|
23
|
+
analyzedCount?: number;
|
|
24
|
+
totalFailedCount?: number;
|
|
25
|
+
totalPrs?: number;
|
|
26
|
+
faultyPrNumbers?: unknown[];
|
|
27
|
+
failureReason?: string;
|
|
28
|
+
/** TRA UI link where the full report lives. */
|
|
29
|
+
viewReport: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Trigger (or read, when one already exists) the Release Readiness report for
|
|
33
|
+
* a build via the o11y external API, returning a trimmed glimpse. Stateless:
|
|
34
|
+
* nothing persists between calls.
|
|
35
|
+
*/
|
|
36
|
+
export declare function triggerRcaReport(args: TriggerRcaReportArgs, config: BrowserStackConfig): Promise<RcaReportGlimpse>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
3
|
+
import { getO11yBaseUrl, getO11yUiBuildUrl, RELEASE_READINESS_TRIGGER_PATH, } from "./constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Domain error carrying a client-safe message. The tool maps these to a
|
|
6
|
+
* `{ isError: true }` envelope; the message never contains credentials.
|
|
7
|
+
*/
|
|
8
|
+
export class TriggerRcaReportError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/** Pull a machine error code out of a non-2xx body, wherever it rides. */
|
|
11
|
+
function extractErrorCode(data) {
|
|
12
|
+
const candidate = data?.code ?? data?.error ?? data?.errorCode ?? data?.message;
|
|
13
|
+
return typeof candidate === "string" ? candidate : "";
|
|
14
|
+
}
|
|
15
|
+
/** Map a trigger (POST) non-2xx into a clean, group-scope-safe domain error. */
|
|
16
|
+
function mapTriggerError(status, data) {
|
|
17
|
+
const code = extractErrorCode(data);
|
|
18
|
+
if (code.includes("REPO_NOT_CONFIGURED")) {
|
|
19
|
+
return new TriggerRcaReportError("repository not configured for Release Readiness; connect the repo in Test Observability settings");
|
|
20
|
+
}
|
|
21
|
+
if (code.includes("RELEASE_READINESS_NOT_FOUND")) {
|
|
22
|
+
return new TriggerRcaReportError("no Release Readiness report found for this build");
|
|
23
|
+
}
|
|
24
|
+
if (status === 403) {
|
|
25
|
+
return new TriggerRcaReportError("Release Readiness AI is not enabled for this group (plan or feature flag)");
|
|
26
|
+
}
|
|
27
|
+
if (status === 404) {
|
|
28
|
+
return new TriggerRcaReportError("build not found for your group");
|
|
29
|
+
}
|
|
30
|
+
return new TriggerRcaReportError(`failed to trigger Release Readiness report (status ${status})`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Trigger (or read, when one already exists) the Release Readiness report for
|
|
34
|
+
* a build via the o11y external API, returning a trimmed glimpse. Stateless:
|
|
35
|
+
* nothing persists between calls.
|
|
36
|
+
*/
|
|
37
|
+
export async function triggerRcaReport(args, config) {
|
|
38
|
+
const authString = getBrowserStackAuth(config);
|
|
39
|
+
const headers = {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
|
|
42
|
+
};
|
|
43
|
+
const url = getO11yBaseUrl() +
|
|
44
|
+
RELEASE_READINESS_TRIGGER_PATH.replace("{buildUuid}", encodeURIComponent(args.buildUuid)) +
|
|
45
|
+
`?force=${args.force === true}`;
|
|
46
|
+
const response = await apiClient.post({
|
|
47
|
+
url,
|
|
48
|
+
headers,
|
|
49
|
+
body: {},
|
|
50
|
+
raise_error: false,
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw mapTriggerError(response.status, response.data);
|
|
54
|
+
}
|
|
55
|
+
const data = response.data ?? {};
|
|
56
|
+
const summary = data.summary ?? {};
|
|
57
|
+
// Trimmed glimpse only — never the raw response, never prs[]/workflows[].
|
|
58
|
+
return {
|
|
59
|
+
state: summary.state ?? data.state,
|
|
60
|
+
verdict: summary.verdict,
|
|
61
|
+
verdictProvisional: summary.verdictProvisional,
|
|
62
|
+
partial: summary.partial,
|
|
63
|
+
analyzedCount: summary.analyzedCount,
|
|
64
|
+
totalFailedCount: summary.totalFailedCount,
|
|
65
|
+
totalPrs: summary.totalPrs,
|
|
66
|
+
faultyPrNumbers: summary.faultyPrNumbers,
|
|
67
|
+
failureReason: summary.failureReason,
|
|
68
|
+
viewReport: getO11yUiBuildUrl(args.buildUuid),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaboration status emitted by the TFA agent for one RCA turn.
|
|
3
|
+
* - NEEDS_INFO: TFA needs more digested input; the skill should execute the
|
|
4
|
+
* returned asks/questions and submit another turn on the same thread.
|
|
5
|
+
* - RESOLVED: an agreed root cause was reached (carries `rca`); the loop stops.
|
|
6
|
+
* - BLOCKED: TFA cannot proceed; the loop stops with a best-effort result.
|
|
7
|
+
*/
|
|
8
|
+
export declare enum TfaStatus {
|
|
9
|
+
NEEDS_INFO = "NEEDS_INFO",
|
|
10
|
+
RESOLVED = "RESOLVED",
|
|
11
|
+
BLOCKED = "BLOCKED"
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Soft, tool-emitted status used when an in-call poll exceeds its wall-clock
|
|
15
|
+
* cap. Not produced by the agent — produced by the turn util so the skill can
|
|
16
|
+
* resume polling on a later turn via the optional `turnId` arg.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PENDING_STATUS: "PENDING";
|
|
19
|
+
/**
|
|
20
|
+
* Confidence the agent attaches to a turn. Mirrors the misc-services
|
|
21
|
+
* `TurnResponse.confidence` Literal; PENDING turns carry `unknown`.
|
|
22
|
+
*/
|
|
23
|
+
export type Confidence = "low" | "medium" | "high" | "unknown";
|
|
24
|
+
/**
|
|
25
|
+
* Evidence category the skill routes an ask to. Mirrors the misc-services
|
|
26
|
+
* `Ask.evidence_type` Literal.
|
|
27
|
+
*/
|
|
28
|
+
export type EvidenceType = "test_logs" | "product_code" | "k8s" | "kibana" | "metrics" | "deploy" | "ci" | "other";
|
|
29
|
+
/** A typed request for evidence; the skill routes it by `evidenceType`. */
|
|
30
|
+
export interface TfaAsk {
|
|
31
|
+
what: string;
|
|
32
|
+
why: string;
|
|
33
|
+
evidenceType: EvidenceType;
|
|
34
|
+
priority: "high" | "medium" | "low";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The agreed root-cause analysis carried on a RESOLVED turn. Mirrors the
|
|
38
|
+
* misc-services `TestFailureAnalysis` schema; passed through verbatim from the
|
|
39
|
+
* o11y `rcaChat` response and never reshaped client-side.
|
|
40
|
+
*/
|
|
41
|
+
export interface TfaRca {
|
|
42
|
+
root_cause?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
possible_fix?: string;
|
|
45
|
+
failure_type?: string;
|
|
46
|
+
alternatives_considered?: string[];
|
|
47
|
+
related_prs?: unknown[];
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Structured turn the o11y `rcaChat` poll returns once `status === "completed"`.
|
|
52
|
+
* Mirrors the misc-services `TurnResponse`; sub-objects are status-discriminated
|
|
53
|
+
* (the agent's model validator guarantees the matching one is present).
|
|
54
|
+
*/
|
|
55
|
+
export interface TurnResponse {
|
|
56
|
+
status: TfaStatus;
|
|
57
|
+
confidence: Confidence;
|
|
58
|
+
questions: string[];
|
|
59
|
+
asks: TfaAsk[];
|
|
60
|
+
suggestions: string[];
|
|
61
|
+
hypotheses: string[];
|
|
62
|
+
rca?: TfaRca;
|
|
63
|
+
/** Present on BLOCKED turns: why TFA cannot proceed. */
|
|
64
|
+
reason?: string;
|
|
65
|
+
/** Present on BLOCKED turns: the asks that went unmet. */
|
|
66
|
+
unmetAsks?: string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Trimmed glimpse of a RESOLVED turn's RCA. The full `TfaRca` payload
|
|
70
|
+
* (analysis, log_evidence, alternatives, ...) is intentionally dropped — the
|
|
71
|
+
* complete report lives on the Test Observability dashboard (`viewRca`).
|
|
72
|
+
*/
|
|
73
|
+
export interface TfaRcaGlimpse {
|
|
74
|
+
/** Truncated to `RCA_GLIMPSE_ROOT_CAUSE_MAX` chars. */
|
|
75
|
+
root_cause?: string;
|
|
76
|
+
failure_type?: string;
|
|
77
|
+
related_prs?: unknown[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Trimmed, status-discriminated result returned by the turn util / tool. The
|
|
81
|
+
* raw o11y envelope, `meta` blob, and full RCA payload are never echoed:
|
|
82
|
+
* - NEEDS_INFO carries questions/asks/suggestions/hypotheses VERBATIM (the
|
|
83
|
+
* client loop executes them).
|
|
84
|
+
* - RESOLVED carries only a `glimpse` + a `viewRca` UI pointer.
|
|
85
|
+
* - BLOCKED carries reason/unmetAsks.
|
|
86
|
+
* - PENDING carries only turnId/threadId to resume polling.
|
|
87
|
+
*/
|
|
88
|
+
export interface TfaRcaTurnResult {
|
|
89
|
+
status: TfaStatus | typeof PENDING_STATUS;
|
|
90
|
+
confidence?: Confidence;
|
|
91
|
+
threadId?: string;
|
|
92
|
+
/** Present on PENDING turns: resume via the tool's `turnId` arg. */
|
|
93
|
+
turnId?: string;
|
|
94
|
+
questions?: string[];
|
|
95
|
+
asks?: TfaAsk[];
|
|
96
|
+
suggestions?: string[];
|
|
97
|
+
hypotheses?: string[];
|
|
98
|
+
/** Present on RESOLVED turns: trimmed root-cause glimpse. */
|
|
99
|
+
glimpse?: TfaRcaGlimpse;
|
|
100
|
+
/** Present on RESOLVED turns: where the full RCA report lives. */
|
|
101
|
+
viewRca?: string;
|
|
102
|
+
reason?: string;
|
|
103
|
+
unmetAsks?: string[];
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaboration status emitted by the TFA agent for one RCA turn.
|
|
3
|
+
* - NEEDS_INFO: TFA needs more digested input; the skill should execute the
|
|
4
|
+
* returned asks/questions and submit another turn on the same thread.
|
|
5
|
+
* - RESOLVED: an agreed root cause was reached (carries `rca`); the loop stops.
|
|
6
|
+
* - BLOCKED: TFA cannot proceed; the loop stops with a best-effort result.
|
|
7
|
+
*/
|
|
8
|
+
export var TfaStatus;
|
|
9
|
+
(function (TfaStatus) {
|
|
10
|
+
TfaStatus["NEEDS_INFO"] = "NEEDS_INFO";
|
|
11
|
+
TfaStatus["RESOLVED"] = "RESOLVED";
|
|
12
|
+
TfaStatus["BLOCKED"] = "BLOCKED";
|
|
13
|
+
})(TfaStatus || (TfaStatus = {}));
|
|
14
|
+
/**
|
|
15
|
+
* Soft, tool-emitted status used when an in-call poll exceeds its wall-clock
|
|
16
|
+
* cap. Not produced by the agent — produced by the turn util so the skill can
|
|
17
|
+
* resume polling on a later turn via the optional `turnId` arg.
|
|
18
|
+
*/
|
|
19
|
+
export const PENDING_STATUS = "PENDING";
|