From f8e62d85fee6d1268536acf8f82b4565ba712916 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 10 May 2026 18:21:50 +0300 Subject: [PATCH 1/2] fix(mcp): kill zombie Mocha state when timeout fires before test reaches pause() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- bin/mcp-server.js | 9 +++++++-- lib/codecept.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bin/mcp-server.js b/bin/mcp-server.js index 8f526ca3c..3a955416e 100755 --- a/bin/mcp-server.js +++ b/bin/mcp-server.js @@ -401,14 +401,17 @@ async function cancelRun() { abortRun = true if (typeof pendingRunCleanup === 'function') { try { pendingRunCleanup() } catch {} } if (pausedController) { try { pausedController.resolveContinue() } catch {} ; pausedController = null } + + const mocha = typeof container.mocha === 'function' ? container.mocha() : container.mocha + try { mocha?.runner?.abort?.() } catch {} + if (pendingRunPromise) { - try { await Promise.race([pendingRunPromise.catch(() => {}), new Promise(r => setTimeout(r, 5000))]) } catch {} + try { await pendingRunPromise.catch(() => {}) } catch {} } pendingRunPromise = null pendingRunResults = null pendingTestFile = null pendingStepInfo = null - abortRun = false return true } @@ -1032,6 +1035,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { pendingRunCleanup = null } + abortRun = false let runError = null const runPromise = (async () => { try { @@ -1126,6 +1130,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { pendingRunCleanup = null } + abortRun = false let runError = null const runPromise = (async () => { try { diff --git a/lib/codecept.js b/lib/codecept.js index 81ddce456..0574e04d7 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -316,7 +316,7 @@ class Codecept { try { event.emit(event.all.before, this) - mocha.run(async (failures) => await done(failures)) + mocha.runner = mocha.run(async (failures) => await done(failures)) } catch (e) { output.error(e.stack) reject(e) From fb091ec6933865eb3e65aa1271f5ea3d97e59830 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 10 May 2026 20:36:07 +0300 Subject: [PATCH 2/2] refactor(mcp): drop paranoid container.mocha type guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit container.mocha is the static method on the Container class — always callable. The typeof-check ternary was dead defensive code. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/mcp-server.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/mcp-server.js b/bin/mcp-server.js index 3a955416e..a95cbbf27 100755 --- a/bin/mcp-server.js +++ b/bin/mcp-server.js @@ -60,9 +60,7 @@ function aiTraceHint() { } function applyMochaGrep(grep) { - if (!grep) return - const mocha = typeof container.mocha === 'function' ? container.mocha() : container.mocha - if (mocha && typeof mocha.grep === 'function') mocha.grep(grep) + if (grep) container.mocha().grep(grep) } function pauseAtMatcher(pauseAt) { @@ -402,8 +400,7 @@ async function cancelRun() { if (typeof pendingRunCleanup === 'function') { try { pendingRunCleanup() } catch {} } if (pausedController) { try { pausedController.resolveContinue() } catch {} ; pausedController = null } - const mocha = typeof container.mocha === 'function' ? container.mocha() : container.mocha - try { mocha?.runner?.abort?.() } catch {} + try { container.mocha().runner?.abort() } catch {} if (pendingRunPromise) { try { await pendingRunPromise.catch(() => {}) } catch {}