@@ -22,14 +22,24 @@ const FLOW_ANIMATION_MS = 120;
2222// Single-column glyph so it does not disturb the row-width math used by the
2323// in-place table redraw; marks flow rows that are retry attempts.
2424const RETRY_ICON = '↻' ;
25+ const TERMINAL_RUN_STATUSES : ReadonlySet < MaestroRunInfo [ 'status' ] > = new Set ( [
26+ 'DONE' ,
27+ 'FAILED' ,
28+ 'CANCELLED' ,
29+ ] ) ;
2530
2631export interface MaestroRunAssets {
2732 logs ?: Record < string , string > ;
2833 video ?: string | false ;
2934 screenshots ?: string [ ] ;
3035}
3136
32- export type MaestroFlowStatus = 'WAITING' | 'READY' | 'DONE' | 'FAILED' ;
37+ export type MaestroFlowStatus =
38+ | 'WAITING'
39+ | 'READY'
40+ | 'DONE'
41+ | 'FAILED'
42+ | 'CANCELLED' ;
3343
3444export interface MaestroFlowInfo {
3545 id : number ;
@@ -53,7 +63,7 @@ export interface MaestroRunEnvironment {
5363
5464export interface MaestroRunInfo {
5565 id : number ;
56- status : 'WAITING' | 'READY' | 'DONE' | 'FAILED' ;
66+ status : 'WAITING' | 'READY' | 'DONE' | 'FAILED' | 'CANCELLED' ;
5767 capabilities : {
5868 deviceName : string ;
5969 platformName : string ;
@@ -2062,8 +2072,8 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
20622072 const flows = run . flows ?? [ ] ;
20632073 if ( flows . length === 0 ) {
20642074 // No flows yet: only settled once the run itself reaches a terminal
2065- // state (e.g. it failed before producing any flow).
2066- if ( run . status !== 'DONE' && run . status !== 'FAILED' ) return false ;
2075+ // state (e.g. it failed before producing any flow, or it was cancelled ).
2076+ if ( ! TERMINAL_RUN_STATUSES . has ( run . status ) ) return false ;
20672077 continue ;
20682078 }
20692079
@@ -2166,6 +2176,45 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
21662176 } ) ;
21672177 }
21682178
2179+ /**
2180+ * Stops a run, overriding the base provider's `/stop` with Maestro's `/cancel`.
2181+ *
2182+ * A 409 means the run reached a terminal state first, which is the normal
2183+ * outcome when Ctrl-C lands as the last flow finishes. That is a success for
2184+ * our purposes, so it is not reported as a failure to stop.
2185+ */
2186+ protected override async stopRun ( runId : number ) : Promise < void > {
2187+ if ( ! this . appId ) {
2188+ return ;
2189+ }
2190+
2191+ try {
2192+ await axios . post (
2193+ `${ this . URL } /${ this . appId } /${ runId } /cancel` ,
2194+ { } ,
2195+ {
2196+ headers : {
2197+ 'Content-Type' : 'application/json' ,
2198+ 'User-Agent' : utils . getUserAgent ( ) ,
2199+ } ,
2200+ auth : {
2201+ username : this . credentials . userName ,
2202+ password : this . credentials . accessKey ,
2203+ } ,
2204+ timeout : HTTP . TIMEOUT_MS ,
2205+ } ,
2206+ ) ;
2207+
2208+ if ( ! this . options . quiet ) {
2209+ logger . info ( `Cancelled run ${ runId } ` ) ;
2210+ }
2211+ } catch ( error ) {
2212+ if ( axios . isAxiosError ( error ) && error . response ?. status === 409 ) {
2213+ return ;
2214+ }
2215+ }
2216+ }
2217+
21692218 /** Stable key identifying the logical flow/shard a flow attempt belongs to. */
21702219 private flowGroupKey ( flow : MaestroFlowInfo ) : string {
21712220 return flow . shard_index != null
@@ -2291,9 +2340,11 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
22912340
22922341 const status = await this . getStatus ( ) ;
22932342
2294- // Track active run IDs for graceful shutdown
2343+ // Track active run IDs for graceful shutdown. CANCELLED belongs in the terminal
2344+ // set: a run cancelled from the dashboard while we poll must not be listed as
2345+ // active, or Ctrl-C would fire another stop at a run that is already settled.
22952346 this . activeRunIds = status . runs
2296- . filter ( ( run ) => run . status !== 'DONE' && run . status !== 'FAILED' )
2347+ . filter ( ( run ) => ! TERMINAL_RUN_STATUSES . has ( run . status ) )
22972348 . map ( ( run ) => run . id ) ;
22982349
22992350 const running = status . runs . find ( ( r ) => r . status === 'READY' ) ;
@@ -2556,6 +2607,8 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
25562607 return { symbol : pc . green ( '✔' ) , text : 'Test has finished running' } ;
25572608 case 'FAILED' :
25582609 return { symbol : pc . red ( '✘' ) , text : 'Test failed' } ;
2610+ case 'CANCELLED' :
2611+ return { symbol : pc . yellow ( '⊘' ) , text : 'Test was cancelled' } ;
25592612 default :
25602613 return { symbol : pc . dim ( '?' ) , text : status } ;
25612614 }
@@ -2585,6 +2638,8 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
25852638 }
25862639 case 'FAILED' :
25872640 return { text : '✘ FAILED' , colored : pc . red ( '✘ FAILED' ) } ;
2641+ case 'CANCELLED' :
2642+ return { text : '⊘ CANCELLED' , colored : pc . yellow ( '⊘ CANCELLED' ) } ;
25882643 default :
25892644 return { text : flow . status , colored : flow . status } ;
25902645 }
@@ -2669,6 +2724,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
26692724 let running = 0 ;
26702725 let passed = 0 ;
26712726 let failed = 0 ;
2727+ let cancelled = 0 ;
26722728
26732729 for ( const flow of remaining ) {
26742730 switch ( flow . status ) {
@@ -2688,6 +2744,9 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
26882744 case 'FAILED' :
26892745 failed ++ ;
26902746 break ;
2747+ case 'CANCELLED' :
2748+ cancelled ++ ;
2749+ break ;
26912750 }
26922751 }
26932752
@@ -2696,6 +2755,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
26962755 if ( running > 0 ) parts . push ( pc . blue ( `${ running } running` ) ) ;
26972756 if ( passed > 0 ) parts . push ( pc . green ( `${ passed } passed` ) ) ;
26982757 if ( failed > 0 ) parts . push ( pc . red ( `${ failed } failed` ) ) ;
2758+ if ( cancelled > 0 ) parts . push ( pc . yellow ( `${ cancelled } cancelled` ) ) ;
26992759
27002760 return ` ... and ${ remaining . length } more: ${ parts . join ( ', ' ) } ` ;
27012761 }
0 commit comments