Skip to content

Commit 23b59f5

Browse files
DavertMikDavertMikclaude
authored
fix(mcp): timeout aborts Mocha runner so next run_test isn't blocked (#5551)
* fix(mcp): timeout aborts Mocha runner so next run_test isn't blocked Previously the run_test / run_step_by_step timeout was just a setTimeout that rejected the race promise — the Mocha runner kept going, the recorder chain stayed queued, listeners stayed attached, and pause sessions kept trapping. Subsequent run_test calls hit "Mocha instance is currently running". cancel didn't help because pendingRunPromise was only assigned in the paused branch, so it saw nothing to cancel. - Assign pendingRunPromise immediately after runPromise creation in both run_test and run_step_by_step (was set only on pause). - Wrap the Promise.race in try/catch + finally; clear the setTimeout and route timeout rejections through cancelRun(); return a clean status: "failed" payload to the client. - Make cancelRun() actually abort: look up the Mocha runner via mocha._runner / _previousRunner / runner and call runner.abort(); recorder.reset() to drop any queued tasks. Existing abortRun + pause release stay in place for stuck-on-pause cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): drop unused runner.abort + recorder.reset hacks The pause-and-abortRun chain alone is enough: resolveContinue releases the current pauseSession, abortRun causes the next pauseNow-queued pauseSession to reject inside setPauseHandler, the rejection propagates through the recorder to codecept.run, and Mocha's runningNow clears naturally. Reaching into mocha._runner / _previousRunner / .runner and calling recorder.reset() were speculative — the timeout repro still clears Mocha state without them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(mcp): extract raceRunOutcome helper to dedupe run_test/run_step_by_step Both tools had the same Promise.race + try/catch + finally + cancelRun + failure-response block. Extracted into raceRunOutcome(runPromise, timeout) which returns a tagged outcome ({outcome:'paused'|'completed'} or {outcome:'aborted', error}) so the caller branches once. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(mcp): rename raceRunOutcome → waitForTestResult, outcome → status Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4f0fa49 commit 23b59f5

1 file changed

Lines changed: 33 additions & 20 deletions

File tree

bin/mcp-server.js

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,23 @@ async function cancelRun() {
412412
return true
413413
}
414414

415+
async function waitForTestResult(runPromise, timeout) {
416+
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
417+
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
418+
let timeoutId
419+
const timeoutPromise = new Promise((_, reject) => {
420+
timeoutId = setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
421+
})
422+
try {
423+
return { status: await Promise.race([completedPromise, pausedPromise, timeoutPromise]) }
424+
} catch (err) {
425+
await cancelRun()
426+
return { status: 'aborted', error: err.message }
427+
} finally {
428+
clearTimeout(timeoutId)
429+
}
430+
}
431+
415432
async function closeBrowser() {
416433
if (!containerInitialized) return
417434
await cancelRun()
@@ -1025,18 +1042,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
10251042
throw err
10261043
}
10271044
})()
1045+
pendingRunPromise = runPromise
10281046

1029-
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
1030-
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
1031-
1032-
const which = await Promise.race([
1033-
completedPromise,
1034-
pausedPromise,
1035-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
1036-
])
1047+
const result = await waitForTestResult(runPromise, timeout)
1048+
if (result.status === 'aborted') {
1049+
await startShellSession()
1050+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'failed', file: testFile, error: result.error }, null, 2) }] }
1051+
}
10371052

1038-
if (which === 'paused') {
1039-
pendingRunPromise = runPromise
1053+
if (result.status === 'paused') {
10401054
const page = await gatherPageBrief()
10411055
return {
10421056
content: [{
@@ -1046,6 +1060,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
10461060
}
10471061
}
10481062

1063+
pendingRunPromise = null
10491064
const final = collectRunCompletion(runError?.message)
10501065
await startShellSession()
10511066
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
@@ -1121,18 +1136,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
11211136
throw err
11221137
}
11231138
})()
1139+
pendingRunPromise = runPromise
11241140

1125-
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
1126-
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
1127-
1128-
const which = await Promise.race([
1129-
completedPromise,
1130-
pausedPromise,
1131-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
1132-
])
1141+
const result = await waitForTestResult(runPromise, timeout)
1142+
if (result.status === 'aborted') {
1143+
await startShellSession()
1144+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'failed', file: testFile, error: result.error }, null, 2) }] }
1145+
}
11331146

1134-
if (which === 'paused') {
1135-
pendingRunPromise = runPromise
1147+
if (result.status === 'paused') {
11361148
const page = await gatherPageBrief()
11371149
return {
11381150
content: [{
@@ -1142,6 +1154,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
11421154
}
11431155
}
11441156

1157+
pendingRunPromise = null
11451158
const final = collectRunCompletion(runError?.message)
11461159
await startShellSession()
11471160
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }

0 commit comments

Comments
 (0)