@@ -60,7 +60,20 @@ function aiTraceHint() {
6060}
6161
6262function applyMochaGrep ( grep ) {
63- if ( grep && typeof container . mocha ?. grep === 'function' ) container . mocha . grep ( grep )
63+ if ( ! grep ) return
64+ const mocha = typeof container . mocha === 'function' ? container . mocha ( ) : container . mocha
65+ if ( mocha && typeof mocha . grep === 'function' ) mocha . grep ( grep )
66+ }
67+
68+ function pauseAtMatcher ( pauseAt ) {
69+ if ( pauseAt == null ) return ( ) => false
70+ if ( typeof pauseAt === 'number' ) return ( idx ) => idx === pauseAt
71+ if ( typeof pauseAt === 'string' ) {
72+ const m = pauseAt . match ( / ^ \/ ( .+ ) \/ ( [ g i m s u y ] * ) $ / )
73+ const re = m ? new RegExp ( m [ 1 ] , m [ 2 ] ) : new RegExp ( pauseAt . replace ( / [ . + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) , 'i' )
74+ return ( _idx , name ) => re . test ( name )
75+ }
76+ return ( ) => false
6477}
6578
6679async function ensureBootstrap ( ) {
@@ -124,12 +137,7 @@ function pluginsSignature(plugins) {
124137async function teardownContainer ( ) {
125138 if ( ! containerInitialized ) return
126139 try {
127- await endShellSession ( )
128- const helpers = container . helpers ( )
129- for ( const helperName in helpers ) {
130- const helper = helpers [ helperName ]
131- try { if ( helper . _finish ) await helper . _finish ( ) } catch { }
132- }
140+ await closeBrowser ( )
133141 try { if ( codecept ?. teardown ) await codecept . teardown ( ) } catch { }
134142 } finally {
135143 containerInitialized = false
@@ -365,15 +373,17 @@ function outputBaseDir() {
365373// pause(), the handler registered via setPauseHandler resolves a "paused"
366374// promise that run_test is racing against test completion. The "pause" tool
367375// then drives the REPL by mutating next/abort and resolving the controller.
368- let pausedController = null // { resolveContinue, registeredVariables }
369- let pendingRunPromise = null // run_test's run() promise while paused
370- let pendingRunResults = null // results array being collected while paused
371- let pendingRunCleanup = null // cleanup callback to detach test.after / step.after listeners
372- let pendingTestFile = null // file path of the test currently running
373- let pendingStepInfo = null // { index, name, status } of the last step that fired step.after
376+ let pausedController = null
377+ let pendingRunPromise = null
378+ let pendingRunResults = null
379+ let pendingRunCleanup = null
380+ let pendingTestFile = null
381+ let pendingStepInfo = null
382+ let abortRun = false
374383const pauseEvents = new EventEmitter ( )
375384
376385setPauseHandler ( ( { registeredVariables } ) => {
386+ if ( abortRun ) return Promise . reject ( new Error ( 'MCP session aborted' ) )
377387 return new Promise ( resolve => {
378388 pausedController = {
379389 registeredVariables,
@@ -386,6 +396,33 @@ setPauseHandler(({ registeredVariables }) => {
386396 } )
387397} )
388398
399+ async function cancelRun ( ) {
400+ if ( ! pendingRunPromise && ! pausedController ) return false
401+ abortRun = true
402+ if ( typeof pendingRunCleanup === 'function' ) { try { pendingRunCleanup ( ) } catch { } }
403+ if ( pausedController ) { try { pausedController . resolveContinue ( ) } catch { } ; pausedController = null }
404+ if ( pendingRunPromise ) {
405+ try { await Promise . race ( [ pendingRunPromise . catch ( ( ) => { } ) , new Promise ( r => setTimeout ( r , 5000 ) ) ] ) } catch { }
406+ }
407+ pendingRunPromise = null
408+ pendingRunResults = null
409+ pendingTestFile = null
410+ pendingStepInfo = null
411+ abortRun = false
412+ return true
413+ }
414+
415+ async function closeBrowser ( ) {
416+ if ( ! containerInitialized ) return
417+ await cancelRun ( )
418+ await endShellSession ( )
419+ for ( const helper of Object . values ( container . helpers ( ) || { } ) ) {
420+ try { if ( helper . _cleanup ) await helper . _cleanup ( ) } catch { }
421+ try { if ( helper . _finishTest ) await helper . _finishTest ( ) } catch { }
422+ }
423+ browserStarted = false
424+ }
425+
389426async function captureLiveArtifacts ( prefix = 'pause' ) {
390427 const helper = pickActingHelper ( container . helpers ( ) )
391428 if ( ! helper ) return { }
@@ -558,7 +595,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
558595 test : { type : 'string' } ,
559596 timeout : { type : 'number' } ,
560597 grep : { type : 'string' , description : 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' } ,
561- pauseAt : { type : 'number' , description : '1-based step index. Test will pause after the Nth step completes. Useful as a programmatic breakpoint without editing the test.' } ,
598+ pauseAt : {
599+ description : 'Programmatic breakpoint. Either a 1-based step index (number) or a step-name match (string — substring case-insensitive, or `/regex/i` literal). Examples: 5 / "fill field" / "/grab.*url/i".' ,
600+ oneOf : [ { type : 'number' } , { type : 'string' } ] ,
601+ } ,
562602 plugins : PLUGINS_PROP ,
563603 } ,
564604 required : [ 'test' ] ,
@@ -619,6 +659,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
619659 } ,
620660 } ,
621661 } ,
662+ {
663+ name : 'cancel' ,
664+ description : 'Abort the currently paused or in-progress test run without closing the browser. Use when you want to bail out of a paused test and start something else without going through stop_browser/start_browser. The browser session and Mocha state stay alive.' ,
665+ inputSchema : { type : 'object' , properties : { } } ,
666+ } ,
622667 ] ,
623668} ) )
624669
@@ -676,6 +721,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
676721 return { content : [ { type : 'text' , text : JSON . stringify ( { status : 'Session already active' , plugins : plugins ?? null } , null , 2 ) } ] }
677722 }
678723 await initCodecept ( configPath , plugins )
724+ if ( containerInitialized && ! browserStarted ) {
725+ for ( const helper of Object . values ( container . helpers ( ) || { } ) ) {
726+ try { if ( helper . _beforeSuite ) await helper . _beforeSuite ( ) } catch { }
727+ }
728+ browserStarted = true
729+ }
679730 await startShellSession ( )
680731 return { content : [ { type : 'text' , text : JSON . stringify ( { status : 'Session started — run_code and snapshot are now available' , plugins : plugins ?? null } , null , 2 ) } ] }
681732 }
@@ -684,8 +735,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
684735 if ( ! containerInitialized ) {
685736 return { content : [ { type : 'text' , text : JSON . stringify ( { status : 'Browser not initialized' } , null , 2 ) } ] }
686737 }
687- await teardownContainer ( )
688- return { content : [ { type : 'text' , text : JSON . stringify ( { status : 'Browser stopped successfully ' } , null , 2 ) } ] }
738+ await closeBrowser ( )
739+ return { content : [ { type : 'text' , text : JSON . stringify ( { status : 'Browser stopped — Mocha and config preserved; call start_browser to reopen ' } , null , 2 ) } ] }
689740 }
690741
691742 case 'snapshot' : {
@@ -755,6 +806,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
755806 } )
756807 }
757808
809+ case 'cancel' : {
810+ const cancelled = await cancelRun ( )
811+ await ensureSession ( )
812+ return { content : [ { type : 'text' , text : JSON . stringify ( { status : cancelled ? 'Run cancelled — browser kept open' : 'No run in progress' } , null , 2 ) } ] }
813+ }
814+
758815 case 'run_code' : {
759816 const { code, timeout = 60000 , saveArtifacts = true , settleMs = 300 } = args
760817 await initCodecept ( )
@@ -814,6 +871,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
814871 const paramNames = [ 'I' , ...Object . keys ( scope ) . filter ( k => k !== 'I' ) . sort ( ) ]
815872 const paramValues = paramNames . map ( k => scope [ k ] )
816873
874+ const wasPaused = ! ! pausedController
875+ if ( wasPaused ) recorder . session . start ( 'mcp_run_code' )
876+
817877 let returnValue
818878 try {
819879 const asyncFn = new Function ( ...paramNames , `return (async () => { ${ code } })()` )
@@ -833,7 +893,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
833893 for ( const m of consoleMethods ) console [ m ] = origConsoleMethods [ m ]
834894 try { event . dispatcher . removeListener ( event . step . after , onStepAfter ) } catch { }
835895 try { event . dispatcher . removeListener ( event . step . passed , onStepPassed ) } catch { }
836- try { recorder . reset ( ) } catch { }
896+ if ( wasPaused ) {
897+ try { recorder . session . restore ( 'mcp_run_code' ) } catch { }
898+ } else {
899+ try { recorder . reset ( ) } catch { }
900+ }
837901 }
838902
839903 result . commands = commands
@@ -918,6 +982,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
918982 pendingTestFile = testFile
919983 pendingStepInfo = null
920984 let stepIndex = 0
985+ const matchPauseAt = pauseAtMatcher ( pauseAt )
921986
922987 const onAfter = t => {
923988 const aiTrace = t . artifacts ?. aiTrace
@@ -932,14 +997,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
932997 }
933998 const onStepAfter = step => {
934999 stepIndex += 1
935- try {
936- pendingStepInfo = { index : stepIndex , name : step . toString ( ) , status : step . status }
937- } catch {
938- pendingStepInfo = { index : stepIndex }
939- }
940- if ( typeof pauseAt === 'number' && stepIndex === pauseAt ) {
941- pauseNow ( )
942- }
1000+ const idx = stepIndex
1001+ const name = ( ( ) => { try { return step . toString ( ) } catch { return '' } } ) ( )
1002+ recorder . add ( 'mcp pause info' , ( ) => {
1003+ pendingStepInfo = { index : idx , name, status : step . status }
1004+ } )
1005+ if ( matchPauseAt ( idx , name ) ) pauseNow ( )
9431006 }
9441007 event . dispatcher . on ( event . test . after , onAfter )
9451008 event . dispatcher . on ( event . step . after , onStepAfter )
@@ -1030,11 +1093,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
10301093 }
10311094 const onStepAfter = step => {
10321095 stepIndex += 1
1033- try {
1034- pendingStepInfo = { index : stepIndex , name : step . toString ( ) , status : step . status }
1035- } catch {
1036- pendingStepInfo = { index : stepIndex }
1037- }
1096+ const idx = stepIndex
1097+ const name = ( ( ) => { try { return step . toString ( ) } catch { return '' } } ) ( )
1098+ recorder . add ( 'mcp pause info' , ( ) => {
1099+ pendingStepInfo = { index : idx , name , status : step . status }
1100+ } )
10381101 pauseNow ( )
10391102 }
10401103 event . dispatcher . on ( event . test . after , onAfter )
0 commit comments