@@ -6,6 +6,20 @@ import { formatAxiosError } from "../../lib/error.js";
66import { BrowserStackConfig } from "../../lib/types.js" ;
77import { getTMBaseURL } from "../../lib/tm-base-url.js" ;
88
9+ /**
10+ * Selection of test cases (with optional configurations) to add.
11+ */
12+ const TestCaseSelectionSchema = z . object ( {
13+ test_case_ids : z
14+ . array ( z . string ( ) )
15+ . min ( 1 )
16+ . describe ( "Test case IDs, e.g. TC-123" ) ,
17+ configuration_ids : z
18+ . array ( z . number ( ) )
19+ . optional ( )
20+ . describe ( "Configuration IDs to apply" ) ,
21+ } ) ;
22+
923/**
1024 * Schema for updating a test run with partial fields.
1125 */
@@ -27,33 +41,103 @@ export const UpdateTestRunSchema = z.object({
2741 ] )
2842 . optional ( )
2943 . describe ( "Updated state of the test run" ) ,
44+ add_test_cases : z
45+ . array ( TestCaseSelectionSchema )
46+ . optional ( )
47+ . describe ( "Test cases to add to the run" ) ,
48+ preserve_existing_results : z
49+ . boolean ( )
50+ . optional ( )
51+ . describe ( "Keep existing results when adding cases (default true)" ) ,
3052 } ) ,
3153} ) ;
3254
3355type UpdateTestRunArgs = z . infer < typeof UpdateTestRunSchema > ;
3456
57+ /**
58+ * Builds the HTTP Basic auth header from per-request config credentials.
59+ */
60+ function buildAuthHeader ( config : BrowserStackConfig ) : string {
61+ return "Basic " + Buffer . from ( getBrowserStackAuth ( config ) ) . toString ( "base64" ) ;
62+ }
63+
3564/**
3665 * Partially updates an existing test run.
66+ *
67+ * Dispatches to one of two BrowserStack endpoints based on the fields provided,
68+ * mirroring how the platform splits these concerns across two endpoints:
69+ * - metadata (name / run_state) -> PATCH .../test-runs/{id}/update
70+ * - adding test cases -> PATCH .../test-runs/{id}/test-cases
71+ *
72+ * Either or both may be supplied in one call; each provided concern hits its
73+ * own endpoint and both outcomes are reported. At least one must be provided.
74+ * Removing test cases is intentionally not exposed — this tool is
75+ * non-destructive.
3776 */
3877export async function updateTestRun (
3978 args : UpdateTestRunArgs ,
4079 config : BrowserStackConfig ,
80+ ) : Promise < CallToolResult > {
81+ const { name, run_state, add_test_cases } = args . test_run ;
82+
83+ const addIds = add_test_cases ?. flatMap ( ( s ) => s . test_case_ids ) ?? [ ] ;
84+ const hasTestCases = addIds . length > 0 ;
85+ const hasMetadata = name !== undefined || run_state !== undefined ;
86+
87+ if ( ! hasTestCases && ! hasMetadata ) {
88+ return {
89+ content : [
90+ {
91+ type : "text" ,
92+ text : "Nothing to update: provide name/run_state and/or add_test_cases." ,
93+ } ,
94+ ] ,
95+ isError : true ,
96+ } ;
97+ }
98+
99+ const tmBaseUrl = await getTMBaseURL ( config ) ;
100+ const authHeader = buildAuthHeader ( config ) ;
101+
102+ const tasks : Promise < CallToolResult > [ ] = [ ] ;
103+ if ( hasMetadata ) {
104+ tasks . push ( updateTestRunMetadata ( args , tmBaseUrl , authHeader ) ) ;
105+ }
106+ if ( hasTestCases ) {
107+ tasks . push ( updateTestRunTestCases ( args , tmBaseUrl , authHeader ) ) ;
108+ }
109+
110+ const results = await Promise . all ( tasks ) ;
111+ if ( results . length === 1 ) {
112+ return results [ 0 ] ;
113+ }
114+
115+ // Both concerns updated: aggregate outcomes; surface an error if either failed.
116+ return {
117+ content : results . flatMap ( ( r ) => r . content ) ,
118+ isError : results . some ( ( r ) => r . isError ) ,
119+ } ;
120+ }
121+
122+ /**
123+ * Updates test run metadata (name / run_state) via the /update endpoint.
124+ */
125+ async function updateTestRunMetadata (
126+ args : UpdateTestRunArgs ,
127+ baseUrl : string ,
128+ authHeader : string ,
41129) : Promise < CallToolResult > {
42130 try {
43- const body = { test_run : args . test_run } ;
44- const tmBaseUrl = await getTMBaseURL ( config ) ;
45- const url = `${ tmBaseUrl } /api/v2/projects/${ encodeURIComponent (
131+ const { name , run_state } = args . test_run ;
132+ const body = { test_run : { name , run_state } } ;
133+ const url = `${ baseUrl } /api/v2/projects/${ encodeURIComponent (
46134 args . project_identifier ,
47135 ) } /test-runs/${ encodeURIComponent ( args . test_run_id ) } /update`;
48136
49- const authString = getBrowserStackAuth ( config ) ;
50- const [ username , password ] = authString . split ( ":" ) ;
51-
52137 const resp = await apiClient . patch ( {
53138 url,
54139 headers : {
55- Authorization :
56- "Basic " + Buffer . from ( `${ username } :${ password } ` ) . toString ( "base64" ) ,
140+ Authorization : authHeader ,
57141 "Content-Type" : "application/json" ,
58142 } ,
59143 body,
@@ -85,3 +169,64 @@ export async function updateTestRun(
85169 return formatAxiosError ( err , "Failed to update test run" ) ;
86170 }
87171}
172+
173+ /**
174+ * Adds test cases to a run via the /test-cases endpoint.
175+ * This call is applied asynchronously by the backend.
176+ */
177+ async function updateTestRunTestCases (
178+ args : UpdateTestRunArgs ,
179+ baseUrl : string ,
180+ authHeader : string ,
181+ ) : Promise < CallToolResult > {
182+ try {
183+ const { add_test_cases, preserve_existing_results } = args . test_run ;
184+
185+ const body = {
186+ test_run : {
187+ add_test_cases,
188+ preserve_existing_results : preserve_existing_results ?? true ,
189+ } ,
190+ } ;
191+
192+ const url = `${ baseUrl } /api/v2/projects/${ encodeURIComponent (
193+ args . project_identifier ,
194+ ) } /test-runs/${ encodeURIComponent ( args . test_run_id ) } /test-cases`;
195+
196+ const resp = await apiClient . patch ( {
197+ url,
198+ headers : {
199+ Authorization : authHeader ,
200+ "Content-Type" : "application/json" ,
201+ } ,
202+ body,
203+ } ) ;
204+
205+ const data = resp . data ;
206+ if ( ! data . success ) {
207+ return {
208+ content : [
209+ {
210+ type : "text" ,
211+ text : `Failed to update test run test cases: ${ JSON . stringify ( data ) } ` ,
212+ } ,
213+ ] ,
214+ isError : true ,
215+ } ;
216+ }
217+
218+ const added = add_test_cases ?. flatMap ( ( s ) => s . test_case_ids ) ?? [ ] ;
219+
220+ return {
221+ content : [
222+ {
223+ type : "text" ,
224+ text : `Queued test-case update for ${ args . test_run_id } (added ${ added . length } ); changes apply asynchronously.` ,
225+ } ,
226+ { type : "text" , text : JSON . stringify ( data , null , 2 ) } ,
227+ ] ,
228+ } ;
229+ } catch ( err ) {
230+ return formatAxiosError ( err , "Failed to update test run test cases" ) ;
231+ }
232+ }
0 commit comments