Skip to content

Commit fe3f890

Browse files
DavertMikclaude
andcommitted
fix(mcp): kill zombie Mocha state when timeout fires before test reaches pause()
When run_test's timeout was shorter than the time the test needed to reach pause() (e.g. 50ms timeout vs 30s Playwright helper timeout), Mocha kept running in the background, eventually entered paused state forever (because cancelRun had reset abortRun=false), and every subsequent run_test threw "Mocha instance is currently running tests". The only recovery was killing the MCP process. Three changes: - lib/codecept.js: capture the Runner returned by mocha.run() as mocha.runner, so callers have a clean handle to abort. Previously the return value was discarded. - cancelRun(): call mocha.runner.abort() to actually stop Mocha (sets the runner's _abort flag, makes the run callback fire fast). Drop the 5s race against pendingRunPromise — with runner.abort() the promise settles quickly; relying on a short race meant a 30s Playwright step would outlive the cancel and Mocha state stayed RUNNING. - abortRun lifecycle: stop resetting it inside cancelRun. Reset it at the start of each new run_test / run_step_by_step instead. This way if a late pause() fires after cancelRun returns (test reached pause asynchronously after the timeout), setPauseHandler still rejects it instead of trapping forever. Repro fixed: run_test({timeout: 50}) → Timeout after 50ms run_test({timeout: 60000}) → completed, stats: {tests:1, passes:1} (was: second call permanently failed with "Mocha is already running") Refs: testomatio/e2e-tests#103 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9c8ad1e commit fe3f890

2 files changed

Lines changed: 8 additions & 3 deletions

File tree

bin/mcp-server.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,17 @@ async function cancelRun() {
401401
abortRun = true
402402
if (typeof pendingRunCleanup === 'function') { try { pendingRunCleanup() } catch {} }
403403
if (pausedController) { try { pausedController.resolveContinue() } catch {} ; pausedController = null }
404+
405+
const mocha = typeof container.mocha === 'function' ? container.mocha() : container.mocha
406+
try { mocha?.runner?.abort?.() } catch {}
407+
404408
if (pendingRunPromise) {
405-
try { await Promise.race([pendingRunPromise.catch(() => {}), new Promise(r => setTimeout(r, 5000))]) } catch {}
409+
try { await pendingRunPromise.catch(() => {}) } catch {}
406410
}
407411
pendingRunPromise = null
408412
pendingRunResults = null
409413
pendingTestFile = null
410414
pendingStepInfo = null
411-
abortRun = false
412415
return true
413416
}
414417

@@ -1032,6 +1035,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
10321035
pendingRunCleanup = null
10331036
}
10341037

1038+
abortRun = false
10351039
let runError = null
10361040
const runPromise = (async () => {
10371041
try {
@@ -1126,6 +1130,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
11261130
pendingRunCleanup = null
11271131
}
11281132

1133+
abortRun = false
11291134
let runError = null
11301135
const runPromise = (async () => {
11311136
try {

lib/codecept.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ class Codecept {
316316

317317
try {
318318
event.emit(event.all.before, this)
319-
mocha.run(async (failures) => await done(failures))
319+
mocha.runner = mocha.run(async (failures) => await done(failures))
320320
} catch (e) {
321321
output.error(e.stack)
322322
reject(e)

0 commit comments

Comments
 (0)