codeceptjs 4.0.0-rc.17 → 4.0.0-rc.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +733 -196
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/aria.js +260 -0
- package/lib/command/dryRun.js +23 -3
- package/lib/command/init.js +247 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/config.js +77 -4
- package/lib/container.js +34 -2
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +5 -6
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/locator.js +2 -2
- package/lib/mocha/factory.js +5 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +72 -84
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +51 -48
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -170
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +25 -0
- package/lib/workers.js +1 -15
- package/package.json +12 -10
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/plugin/pauseOn.js +0 -167
- package/lib/plugin/stepByStepReport.js +0 -432
- package/lib/plugin/subtitles.js +0 -89
package/bin/mcp-server.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
4
5
|
import Codecept from '../lib/codecept.js'
|
|
5
6
|
import container from '../lib/container.js'
|
|
6
7
|
import { getParamsToString } from '../lib/parser.js'
|
|
7
|
-
import { methodsOfObject } from '../lib/utils.js'
|
|
8
|
+
import { methodsOfObject, safeStringify, truncateString } from '../lib/utils.js'
|
|
9
|
+
import {
|
|
10
|
+
captureSnapshot,
|
|
11
|
+
pickActingHelper,
|
|
12
|
+
traceDirFor,
|
|
13
|
+
snapshotDirFor,
|
|
14
|
+
artifactsToFileUrls,
|
|
15
|
+
writeTraceMarkdown,
|
|
16
|
+
TraceReader,
|
|
17
|
+
ariaDiff,
|
|
18
|
+
} from '../lib/utils/trace.js'
|
|
8
19
|
import event from '../lib/event.js'
|
|
9
|
-
import
|
|
20
|
+
import recorder from '../lib/recorder.js'
|
|
21
|
+
import WebElement from '../lib/element/WebElement.js'
|
|
22
|
+
import { locate, within, session, secret, inject, pause } from '../lib/index.js'
|
|
23
|
+
import { tryTo, retryTo, hopeThat } from '../lib/effects.js'
|
|
24
|
+
import step from '../lib/steps.js'
|
|
25
|
+
import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from '../lib/els.js'
|
|
26
|
+
import { setPauseHandler, pauseNow } from '../lib/pause.js'
|
|
27
|
+
import { EventEmitter } from 'events'
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
10
29
|
import { dirname, resolve as resolvePath } from 'path'
|
|
11
30
|
import path from 'path'
|
|
12
|
-
import crypto from 'crypto'
|
|
13
31
|
import { spawn } from 'child_process'
|
|
14
32
|
import { createRequire } from 'module'
|
|
15
|
-
import { existsSync, readdirSync
|
|
33
|
+
import { existsSync, readdirSync } from 'fs'
|
|
16
34
|
import { mkdirp } from 'mkdirp'
|
|
17
35
|
|
|
18
36
|
const require = createRequire(import.meta.url)
|
|
@@ -23,6 +41,129 @@ const __dirname = dirname(__filename)
|
|
|
23
41
|
let codecept = null
|
|
24
42
|
let containerInitialized = false
|
|
25
43
|
let browserStarted = false
|
|
44
|
+
let shellSessionActive = false
|
|
45
|
+
let bootstrapDone = false
|
|
46
|
+
let currentPluginsSig = ''
|
|
47
|
+
let currentAiTraceDir = null // mirrors the dir aiTrace plugin computes per test/session
|
|
48
|
+
let aiTraceEnabled = false // tracked across the session so tool responses can surface a hint when off
|
|
49
|
+
|
|
50
|
+
event.dispatcher.on(event.test.before, test => {
|
|
51
|
+
try {
|
|
52
|
+
const title = (test && (test.fullTitle ? test.fullTitle() : test.title)) || 'MCP Session'
|
|
53
|
+
currentAiTraceDir = traceDirFor(test?.file, title, outputBaseDir())
|
|
54
|
+
} catch {}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function aiTraceHint() {
|
|
58
|
+
if (aiTraceEnabled) return undefined
|
|
59
|
+
return 'aiTrace plugin is disabled — re-run start_browser with plugins={ aiTrace: { enabled: true } } to capture per-step DOM/ARIA/console traces for debugging.'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyMochaGrep(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(/^\/(.+)\/([gimsuy]*)$/)
|
|
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
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function ensureBootstrap() {
|
|
80
|
+
if (bootstrapDone) return
|
|
81
|
+
await codecept.bootstrap()
|
|
82
|
+
bootstrapDone = true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function startShellSession() {
|
|
86
|
+
if (shellSessionActive) return
|
|
87
|
+
await ensureBootstrap()
|
|
88
|
+
recorder.start()
|
|
89
|
+
event.emit(event.suite.before, {
|
|
90
|
+
fullTitle: () => 'MCP Session',
|
|
91
|
+
tests: [],
|
|
92
|
+
retries: () => {},
|
|
93
|
+
})
|
|
94
|
+
event.emit(event.test.before, {
|
|
95
|
+
title: 'MCP Session',
|
|
96
|
+
artifacts: {},
|
|
97
|
+
retries: () => {},
|
|
98
|
+
})
|
|
99
|
+
shellSessionActive = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function endShellSession() {
|
|
103
|
+
if (!shellSessionActive) return
|
|
104
|
+
try { event.emit(event.test.after, {}) } catch {}
|
|
105
|
+
try { event.emit(event.suite.after, {}) } catch {}
|
|
106
|
+
try { event.emit(event.all.result, {}) } catch {}
|
|
107
|
+
shellSessionActive = false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function ensureSession() {
|
|
111
|
+
if (shellSessionActive || pausedController) return
|
|
112
|
+
await startShellSession()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizePluginOverrides(plugins) {
|
|
116
|
+
if (!plugins || typeof plugins !== 'object') return {}
|
|
117
|
+
const out = {}
|
|
118
|
+
for (const [name, opts] of Object.entries(plugins)) {
|
|
119
|
+
if (opts === false) continue
|
|
120
|
+
out[name] = (opts === true || opts == null) ? {} : opts
|
|
121
|
+
}
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyPluginOverrides(config, plugins) {
|
|
126
|
+
config.plugins = config.plugins || {}
|
|
127
|
+
for (const [name, opts] of Object.entries(plugins)) {
|
|
128
|
+
config.plugins[name] = { ...(config.plugins[name] || {}), ...opts, enabled: true }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pluginsSignature(plugins) {
|
|
133
|
+
const keys = Object.keys(plugins).sort()
|
|
134
|
+
return JSON.stringify(keys.map(k => [k, plugins[k]]))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function teardownContainer() {
|
|
138
|
+
if (!containerInitialized) return
|
|
139
|
+
try {
|
|
140
|
+
await closeBrowser()
|
|
141
|
+
try { if (codecept?.teardown) await codecept.teardown() } catch {}
|
|
142
|
+
} finally {
|
|
143
|
+
containerInitialized = false
|
|
144
|
+
browserStarted = false
|
|
145
|
+
bootstrapDone = false
|
|
146
|
+
aiTraceEnabled = false
|
|
147
|
+
codecept = null
|
|
148
|
+
currentPluginsSig = ''
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let shutdownStarted = false
|
|
153
|
+
function installShutdownHooks() {
|
|
154
|
+
const onSignal = (signal) => {
|
|
155
|
+
if (shutdownStarted) return
|
|
156
|
+
shutdownStarted = true
|
|
157
|
+
teardownContainer().finally(() => process.exit(signal === 'SIGINT' ? 130 : 0))
|
|
158
|
+
}
|
|
159
|
+
process.on('SIGTERM', () => onSignal('SIGTERM'))
|
|
160
|
+
process.on('SIGINT', () => onSignal('SIGINT'))
|
|
161
|
+
process.on('beforeExit', () => {
|
|
162
|
+
if (shutdownStarted) return
|
|
163
|
+
shutdownStarted = true
|
|
164
|
+
teardownContainer().catch(() => {})
|
|
165
|
+
})
|
|
166
|
+
}
|
|
26
167
|
|
|
27
168
|
let runLock = Promise.resolve()
|
|
28
169
|
async function withLock(fn) {
|
|
@@ -224,19 +365,136 @@ async function resolveTestToFile({ cli, root, configPath, test }) {
|
|
|
224
365
|
return fsFound ? normalizePath(fsFound) : null
|
|
225
366
|
}
|
|
226
367
|
|
|
227
|
-
function
|
|
228
|
-
return
|
|
368
|
+
function outputBaseDir() {
|
|
369
|
+
return global.output_dir || resolvePath(process.cwd(), 'output')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// In-process pause coordination. When a test running through run_test calls
|
|
373
|
+
// pause(), the handler registered via setPauseHandler resolves a "paused"
|
|
374
|
+
// promise that run_test is racing against test completion. The "pause" tool
|
|
375
|
+
// then drives the REPL by mutating next/abort and resolving the controller.
|
|
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
|
|
383
|
+
const pauseEvents = new EventEmitter()
|
|
384
|
+
|
|
385
|
+
setPauseHandler(({ registeredVariables }) => {
|
|
386
|
+
if (abortRun) return Promise.reject(new Error('MCP session aborted'))
|
|
387
|
+
return new Promise(resolve => {
|
|
388
|
+
pausedController = {
|
|
389
|
+
registeredVariables,
|
|
390
|
+
resolveContinue: () => {
|
|
391
|
+
pausedController = null
|
|
392
|
+
resolve()
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
pauseEvents.emit('paused')
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
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
|
+
|
|
426
|
+
async function captureLiveArtifacts(prefix = 'pause') {
|
|
427
|
+
const helper = pickActingHelper(container.helpers())
|
|
428
|
+
if (!helper) return {}
|
|
429
|
+
const dir = snapshotDirFor(outputBaseDir())
|
|
430
|
+
mkdirp.sync(dir)
|
|
431
|
+
const captured = await captureSnapshot(helper, { dir, prefix })
|
|
432
|
+
return artifactsToFileUrls(captured, dir)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function gatherPageBrief() {
|
|
436
|
+
const helper = pickActingHelper(container.helpers())
|
|
437
|
+
if (!helper) return {}
|
|
438
|
+
const out = {}
|
|
439
|
+
try { if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl() } catch {}
|
|
440
|
+
try { if (helper.grabTitle) out.title = await helper.grabTitle() } catch {}
|
|
441
|
+
try {
|
|
442
|
+
if (helper.grabSource) {
|
|
443
|
+
const html = await helper.grabSource()
|
|
444
|
+
out.contentSize = typeof html === 'string' ? html.length : null
|
|
445
|
+
}
|
|
446
|
+
} catch {}
|
|
447
|
+
return out
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function collectRunCompletion(errorMessage) {
|
|
451
|
+
const results = pendingRunResults || []
|
|
452
|
+
const stats = {
|
|
453
|
+
tests: results.length,
|
|
454
|
+
passes: results.filter(r => r.status === 'passed').length,
|
|
455
|
+
failures: results.filter(r => r.status === 'failed').length,
|
|
456
|
+
}
|
|
457
|
+
if (typeof pendingRunCleanup === 'function') pendingRunCleanup()
|
|
458
|
+
pendingRunPromise = null
|
|
459
|
+
pendingRunResults = null
|
|
460
|
+
pendingTestFile = null
|
|
461
|
+
pendingStepInfo = null
|
|
462
|
+
let error = errorMessage || null
|
|
463
|
+
if (!error && results.length === 0) {
|
|
464
|
+
error = 'No tests ran and no error was reported. The Mocha instance may have been disposed (set mocha.cleanReferencesAfterRun=false in config) or the test file matched no scenarios.'
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
status: error ? 'failed' : 'completed',
|
|
468
|
+
aiTraceDir: currentAiTraceDir,
|
|
469
|
+
reporterJson: { stats, tests: results },
|
|
470
|
+
error,
|
|
471
|
+
aiTraceHint: aiTraceHint(),
|
|
472
|
+
}
|
|
229
473
|
}
|
|
230
474
|
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
475
|
+
function pausedPayload() {
|
|
476
|
+
return {
|
|
477
|
+
status: 'paused',
|
|
478
|
+
file: pendingTestFile,
|
|
479
|
+
aiTraceDir: currentAiTraceDir,
|
|
480
|
+
pausedAfter: pendingStepInfo,
|
|
481
|
+
suggestions: [
|
|
482
|
+
'Call snapshot to capture URL/HTML/ARIA/screenshot/console/storage at this point',
|
|
483
|
+
'Call run_code to inspect or manipulate state (e.g. return await I.grabText("h1"))',
|
|
484
|
+
'Call continue to release the pause and let the test run the next step (or finish)',
|
|
485
|
+
'Query a saved step snapshot offline: codeceptq <locator> --file <aiTraceDir>/<NNNN>_<step>_page.html',
|
|
486
|
+
],
|
|
487
|
+
}
|
|
236
488
|
}
|
|
237
489
|
|
|
238
|
-
async function initCodecept(configPath) {
|
|
239
|
-
|
|
490
|
+
async function initCodecept(configPath, pluginOverrides) {
|
|
491
|
+
const plugins = normalizePluginOverrides(pluginOverrides)
|
|
492
|
+
const sig = pluginsSignature(plugins)
|
|
493
|
+
|
|
494
|
+
if (containerInitialized) {
|
|
495
|
+
if (!Object.keys(plugins).length || sig === currentPluginsSig) return
|
|
496
|
+
await teardownContainer()
|
|
497
|
+
}
|
|
240
498
|
|
|
241
499
|
const testRoot = process.env.CODECEPTJS_PROJECT_DIR || process.cwd()
|
|
242
500
|
|
|
@@ -261,13 +519,27 @@ async function initCodecept(configPath) {
|
|
|
261
519
|
const { getConfig } = await import('../lib/command/utils.js')
|
|
262
520
|
const config = await getConfig(configPath)
|
|
263
521
|
|
|
522
|
+
// aiTrace is the canonical per-step ARIA/HTML/screenshot capture for MCP.
|
|
523
|
+
// Always on so run_code / continue can read the latest snapshot from disk
|
|
524
|
+
// instead of double-capturing through grabAriaSnapshot etc.
|
|
525
|
+
applyPluginOverrides(config, { aiTrace: { on: 'step' }, browser: { show: false }, ...plugins })
|
|
526
|
+
|
|
264
527
|
codecept = new Codecept(config, {})
|
|
265
528
|
await codecept.init(testRoot)
|
|
266
|
-
await container.create(config, {})
|
|
267
529
|
await container.started()
|
|
268
530
|
|
|
269
531
|
containerInitialized = true
|
|
270
532
|
browserStarted = true
|
|
533
|
+
aiTraceEnabled = config.plugins?.aiTrace?.enabled === true
|
|
534
|
+
currentPluginsSig = sig
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function formatReturnValue(value) {
|
|
538
|
+
if (value instanceof WebElement) return await value.describe()
|
|
539
|
+
if (Array.isArray(value) && value.length && value.every(v => v instanceof WebElement)) {
|
|
540
|
+
return await Promise.all(value.map(v => v.describe()))
|
|
541
|
+
}
|
|
542
|
+
return value
|
|
271
543
|
}
|
|
272
544
|
|
|
273
545
|
const server = new Server(
|
|
@@ -275,66 +547,124 @@ const server = new Server(
|
|
|
275
547
|
{ capabilities: { tools: {} } }
|
|
276
548
|
)
|
|
277
549
|
|
|
550
|
+
const PLUGINS_PROP = {
|
|
551
|
+
type: 'object',
|
|
552
|
+
description: 'Plugin configs to enable for this session, keyed by plugin name. Same shape as `plugins` in codecept.conf.js — each value is the plugin\'s config object (`enabled: true` is added automatically). Common entries:\n' +
|
|
553
|
+
' • { browser: { show: true } } — visible browser (headed)\n' +
|
|
554
|
+
' • { browser: { show: false } } — headless\n' +
|
|
555
|
+
' • { browser: { browser: "firefox", windowSize: "1280x720" } } — switch browser + viewport\n' +
|
|
556
|
+
' • { pause: { on: "fail" } } / { screenshot: { on: "step" } } / { aiTrace: {} }\n' +
|
|
557
|
+
'Override or add to whatever the project config already enables.',
|
|
558
|
+
additionalProperties: { type: 'object' },
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const CONFIG_PROP = {
|
|
562
|
+
type: 'string',
|
|
563
|
+
description: 'Path to codecept.conf.js (or .cjs). Defaults to $CODECEPTJS_CONFIG, then ./codecept.conf.js in $CODECEPTJS_PROJECT_DIR or cwd. Only needed for projects with a non-standard config location.',
|
|
564
|
+
}
|
|
565
|
+
|
|
278
566
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
279
567
|
tools: [
|
|
280
568
|
{
|
|
281
569
|
name: 'list_tests',
|
|
282
|
-
description: 'List all tests in the CodeceptJS project',
|
|
283
|
-
inputSchema: { type: 'object', properties: {
|
|
570
|
+
description: 'List all tests in the CodeceptJS project. Uses the active session if start_browser was called, otherwise auto-inits with project defaults.',
|
|
571
|
+
inputSchema: { type: 'object', properties: {} },
|
|
284
572
|
},
|
|
285
573
|
{
|
|
286
574
|
name: 'list_actions',
|
|
287
|
-
description: 'List all available CodeceptJS actions (I.* methods)',
|
|
288
|
-
inputSchema: { type: 'object', properties: {
|
|
575
|
+
description: 'List all available CodeceptJS actions (I.* methods). Uses the active session if start_browser was called, otherwise auto-inits with project defaults.',
|
|
576
|
+
inputSchema: { type: 'object', properties: {} },
|
|
289
577
|
},
|
|
290
578
|
{
|
|
291
579
|
name: 'run_code',
|
|
292
|
-
description: 'Run arbitrary CodeceptJS code.',
|
|
580
|
+
description: 'Run arbitrary CodeceptJS code. Response includes `availableObjects` listing every symbol in scope (I, helpers, container, step, tryTo, within, etc.).',
|
|
293
581
|
inputSchema: {
|
|
294
582
|
type: 'object',
|
|
295
583
|
properties: {
|
|
296
584
|
code: { type: 'string' },
|
|
297
585
|
timeout: { type: 'number' },
|
|
298
|
-
config: { type: 'string' },
|
|
299
586
|
saveArtifacts: { type: 'boolean' },
|
|
587
|
+
settleMs: { type: 'number', description: 'Wait N ms after the code finishes before capturing artifacts. Default 300. Set higher (1000+) when actions trigger slow re-renders, or 0 to skip.' },
|
|
300
588
|
},
|
|
301
589
|
required: ['code'],
|
|
302
590
|
},
|
|
303
591
|
},
|
|
304
592
|
{
|
|
305
593
|
name: 'run_test',
|
|
306
|
-
description: 'Run a specific test.',
|
|
594
|
+
description: 'Run a specific test. Returns reporter JSON with one entry per scenario; each entry has a `traceFile` (file:// URL) pointing to the aiTrace markdown for that scenario — Read it on failures to see the failing step\'s DOM/ARIA/screenshot. If aiTrace is disabled the response includes an `aiTraceHint`. If the test calls pause() — or if pauseAt is set and reached — returns early with status "paused" so the agent can inspect via run_code and release with continue. To learn step indices for pauseAt, call run_step_by_step first. Auto-inits with project defaults if no session is active — call start_browser first to customize launch (e.g. plugins={ browser: { show: true } } to watch the run).',
|
|
307
595
|
inputSchema: {
|
|
308
596
|
type: 'object',
|
|
309
597
|
properties: {
|
|
310
598
|
test: { type: 'string' },
|
|
311
599
|
timeout: { type: 'number' },
|
|
312
|
-
|
|
600
|
+
grep: { type: 'string', description: 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' },
|
|
601
|
+
pauseAt: {
|
|
602
|
+
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".',
|
|
603
|
+
oneOf: [{ type: 'number' }, { type: 'string' }],
|
|
604
|
+
},
|
|
605
|
+
plugins: PLUGINS_PROP,
|
|
313
606
|
},
|
|
314
607
|
required: ['test'],
|
|
315
608
|
},
|
|
316
609
|
},
|
|
317
610
|
{
|
|
318
611
|
name: 'run_step_by_step',
|
|
319
|
-
description: 'Run a test step
|
|
612
|
+
description: 'Run a test interactively, pausing after every step. Returns paused payload after the first step (URL/title/contentSize, last step info, suggestions). Call continue to advance one step (and re-pause), or run_code/snapshot to inspect state. On completion each scenario in `reporterJson.tests[]` has a `traceFile` (file:// URL) for the per-step aiTrace markdown — Read it for the full execution log. Much more useful when start_browser was called with plugins={ browser: { show: true } } so you can watch what happens between pauses.',
|
|
320
613
|
inputSchema: {
|
|
321
614
|
type: 'object',
|
|
322
615
|
properties: {
|
|
323
616
|
test: { type: 'string' },
|
|
324
617
|
timeout: { type: 'number' },
|
|
325
|
-
|
|
618
|
+
grep: { type: 'string', description: 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' },
|
|
619
|
+
plugins: PLUGINS_PROP,
|
|
326
620
|
},
|
|
327
621
|
required: ['test'],
|
|
328
622
|
},
|
|
329
623
|
},
|
|
330
624
|
{
|
|
331
625
|
name: 'start_browser',
|
|
332
|
-
description: 'Start the
|
|
333
|
-
|
|
626
|
+
description: 'Start the session — initializes the codeceptjs container, loads helpers, and applies any plugin overrides. This is the only tool that customizes initialization; every other tool either uses the active session or auto-inits with project defaults.\n\n' +
|
|
627
|
+
'MCP enforces two plugin defaults so the agent gets useful telemetry:\n' +
|
|
628
|
+
' • aiTrace: { on: "step", enabled: true } — per-step DOM/ARIA/console/screenshot traces for debugging\n' +
|
|
629
|
+
' • browser: { show: false, enabled: true } — headless by default\n' +
|
|
630
|
+
'Both can be overridden via the `plugins` arg. To watch the run live: plugins={ browser: { show: true } }. To skip per-step trace overhead on a re-run: plugins={ aiTrace: { enabled: false } } (or { on: "fail" } to only capture failures). To switch config or plugins mid-session, call stop_browser first.',
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: 'object',
|
|
633
|
+
properties: {
|
|
634
|
+
config: CONFIG_PROP,
|
|
635
|
+
plugins: PLUGINS_PROP,
|
|
636
|
+
},
|
|
637
|
+
},
|
|
334
638
|
},
|
|
335
639
|
{
|
|
336
640
|
name: 'stop_browser',
|
|
337
|
-
description: 'Stop the
|
|
641
|
+
description: 'Stop the session, close browsers, and tear down the container. Required before re-initing with different config or plugins.',
|
|
642
|
+
inputSchema: { type: 'object', properties: {} },
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: 'snapshot',
|
|
646
|
+
description: 'Capture current browser state (HTML, ARIA, screenshot, console, URL) without performing any action. Returns `traceFile` (file:// URL) to a markdown trace bundling the captured artifacts — Read it for full context. Auto-inits with project defaults if no session is active.',
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {
|
|
650
|
+
fullPage: { type: 'boolean' },
|
|
651
|
+
settleMs: { type: 'number', description: 'Wait N ms before capturing. Default 300. Set higher when the previous action is still re-rendering, or 0 to skip.' },
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: 'continue',
|
|
657
|
+
description: 'Release a paused test (one that called pause() during run_test) and let it run to completion. Returns the final reporter result. Use run_code to inspect or manipulate state while the test is paused — both tools share the same container.',
|
|
658
|
+
inputSchema: {
|
|
659
|
+
type: 'object',
|
|
660
|
+
properties: {
|
|
661
|
+
timeout: { type: 'number' },
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'cancel',
|
|
667
|
+
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.',
|
|
338
668
|
inputSchema: { type: 'object', properties: {} },
|
|
339
669
|
},
|
|
340
670
|
],
|
|
@@ -346,8 +676,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
346
676
|
try {
|
|
347
677
|
switch (name) {
|
|
348
678
|
case 'list_tests': {
|
|
349
|
-
|
|
350
|
-
await initCodecept(configPath)
|
|
679
|
+
await initCodecept()
|
|
351
680
|
|
|
352
681
|
codecept.loadTests()
|
|
353
682
|
const tests = codecept.testFiles.map(testFile => {
|
|
@@ -362,8 +691,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
362
691
|
}
|
|
363
692
|
|
|
364
693
|
case 'list_actions': {
|
|
365
|
-
|
|
366
|
-
await initCodecept(configPath)
|
|
694
|
+
await initCodecept()
|
|
367
695
|
|
|
368
696
|
const helpers = container.helpers()
|
|
369
697
|
const supportI = container.support('I')
|
|
@@ -391,225 +719,433 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
391
719
|
}
|
|
392
720
|
|
|
393
721
|
case 'start_browser': {
|
|
394
|
-
const configPath = args
|
|
395
|
-
if (browserStarted) {
|
|
396
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: '
|
|
722
|
+
const { config: configPath, plugins } = args || {}
|
|
723
|
+
if (browserStarted && shellSessionActive) {
|
|
724
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session already active', plugins: plugins ?? null }, null, 2) }] }
|
|
725
|
+
}
|
|
726
|
+
await initCodecept(configPath, plugins)
|
|
727
|
+
if (containerInitialized && !browserStarted) {
|
|
728
|
+
for (const helper of Object.values(container.helpers() || {})) {
|
|
729
|
+
try { if (helper._beforeSuite) await helper._beforeSuite() } catch {}
|
|
730
|
+
}
|
|
731
|
+
browserStarted = true
|
|
397
732
|
}
|
|
398
|
-
await
|
|
399
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: '
|
|
733
|
+
await startShellSession()
|
|
734
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session started — run_code and snapshot are now available', plugins: plugins ?? null }, null, 2) }] }
|
|
400
735
|
}
|
|
401
736
|
|
|
402
737
|
case 'stop_browser': {
|
|
403
738
|
if (!containerInitialized) {
|
|
404
739
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser not initialized' }, null, 2) }] }
|
|
405
740
|
}
|
|
741
|
+
await closeBrowser()
|
|
742
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser stopped — Mocha and config preserved; call start_browser to reopen' }, null, 2) }] }
|
|
743
|
+
}
|
|
406
744
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
745
|
+
case 'snapshot': {
|
|
746
|
+
const { fullPage = false, settleMs = 300 } = args || {}
|
|
747
|
+
await initCodecept()
|
|
748
|
+
await ensureSession()
|
|
749
|
+
|
|
750
|
+
const helper = pickActingHelper(container.helpers())
|
|
751
|
+
if (!helper) throw new Error('No supported acting helper available (Playwright, Puppeteer, WebDriver).')
|
|
752
|
+
|
|
753
|
+
const dir = snapshotDirFor(outputBaseDir())
|
|
754
|
+
mkdirp.sync(dir)
|
|
755
|
+
|
|
756
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
757
|
+
const captured = await captureSnapshot(helper, { dir, prefix: 'snapshot', fullPage })
|
|
758
|
+
const traceFile = writeTraceMarkdown({
|
|
759
|
+
dir,
|
|
760
|
+
title: 'snapshot',
|
|
761
|
+
file: 'mcp',
|
|
762
|
+
durationMs: 0,
|
|
763
|
+
commands: [],
|
|
764
|
+
captured,
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
content: [{
|
|
769
|
+
type: 'text',
|
|
770
|
+
text: JSON.stringify({
|
|
771
|
+
status: 'success',
|
|
772
|
+
dir,
|
|
773
|
+
traceFile: pathToFileURL(traceFile).href,
|
|
774
|
+
artifacts: artifactsToFileUrls(captured, dir),
|
|
775
|
+
aiTraceHint: aiTraceHint(),
|
|
776
|
+
}, null, 2),
|
|
777
|
+
}],
|
|
411
778
|
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
case 'continue': {
|
|
782
|
+
if (!pausedController) throw new Error('No paused test. Run a test first via run_test or run_step_by_step; this tool becomes available if the test pauses.')
|
|
783
|
+
const { timeout = 60000 } = args || {}
|
|
784
|
+
return await withSilencedIO(async () => {
|
|
785
|
+
pausedController.resolveContinue()
|
|
786
|
+
if (!pendingRunPromise) {
|
|
787
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'continued' }, null, 2) }] }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Race: test pauses again (step-by-step or another pause()) vs test finishes.
|
|
791
|
+
const pausedAgain = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
|
|
792
|
+
const completed = pendingRunPromise.then(() => 'completed', () => 'completed')
|
|
793
|
+
const which = await Promise.race([
|
|
794
|
+
pausedAgain,
|
|
795
|
+
completed,
|
|
796
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
797
|
+
])
|
|
412
798
|
|
|
413
|
-
|
|
414
|
-
|
|
799
|
+
if (which === 'paused') {
|
|
800
|
+
const page = await gatherPageBrief()
|
|
801
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...pausedPayload(), page }, null, 2) }] }
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
let runError = null
|
|
805
|
+
try { await pendingRunPromise } catch (err) { runError = err }
|
|
806
|
+
const file = pendingTestFile
|
|
807
|
+
const final = collectRunCompletion(runError?.message)
|
|
808
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file }, null, 2) }] }
|
|
809
|
+
})
|
|
810
|
+
}
|
|
415
811
|
|
|
416
|
-
|
|
812
|
+
case 'cancel': {
|
|
813
|
+
const cancelled = await cancelRun()
|
|
814
|
+
await ensureSession()
|
|
815
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: cancelled ? 'Run cancelled — browser kept open' : 'No run in progress' }, null, 2) }] }
|
|
417
816
|
}
|
|
418
817
|
|
|
419
818
|
case 'run_code': {
|
|
420
|
-
const { code, timeout = 60000,
|
|
421
|
-
await initCodecept(
|
|
819
|
+
const { code, timeout = 60000, saveArtifacts = true, settleMs = 300 } = args
|
|
820
|
+
await initCodecept()
|
|
821
|
+
await ensureSession()
|
|
822
|
+
|
|
823
|
+
const support = container.supportObjects() || {}
|
|
824
|
+
if (!support.I) throw new Error('I object not available. Make sure helpers are configured.')
|
|
825
|
+
|
|
826
|
+
const result = { status: 'unknown', output: '', error: null, commands: [], artifacts: {} }
|
|
827
|
+
|
|
828
|
+
const commands = []
|
|
829
|
+
let lastStepValue
|
|
830
|
+
const onStepAfter = step => {
|
|
831
|
+
try { commands.push(step.toString()) } catch {}
|
|
832
|
+
}
|
|
833
|
+
const onStepPassed = (step, val) => {
|
|
834
|
+
if (val !== undefined) lastStepValue = val
|
|
835
|
+
}
|
|
836
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
837
|
+
event.dispatcher.on(event.step.passed, onStepPassed)
|
|
838
|
+
|
|
839
|
+
const traceDir = traceDirFor(`mcp_${Date.now()}`, 'run_code', outputBaseDir())
|
|
840
|
+
mkdirp.sync(traceDir)
|
|
841
|
+
const startedAt = Date.now()
|
|
842
|
+
|
|
843
|
+
// Pin the latest aiTrace ARIA file before running the code, so we
|
|
844
|
+
// can diff after. aiTrace owns per-step capture; we just read it.
|
|
845
|
+
const reader = new TraceReader(currentAiTraceDir)
|
|
846
|
+
const ariaBefore = reader.last('aria')
|
|
847
|
+
|
|
848
|
+
const MAX_LOG_ENTRIES = 100
|
|
849
|
+
const MAX_LOG_MSG_BYTES = 2000
|
|
850
|
+
const MAX_RETURN_BYTES = 20000
|
|
851
|
+
const consoleLogs = []
|
|
852
|
+
const consoleMethods = ['log', 'info', 'warn', 'error', 'debug']
|
|
853
|
+
const origConsoleMethods = {}
|
|
854
|
+
const captureLog = level => (...args) => {
|
|
855
|
+
if (consoleLogs.length >= MAX_LOG_ENTRIES) return
|
|
856
|
+
const message = args.map(a => {
|
|
857
|
+
if (typeof a === 'string') return a
|
|
858
|
+
return truncateString(safeStringify(a, [], 2), MAX_LOG_MSG_BYTES).value
|
|
859
|
+
}).join(' ')
|
|
860
|
+
consoleLogs.push({ level, message, t: Date.now() - startedAt })
|
|
861
|
+
}
|
|
862
|
+
for (const m of consoleMethods) {
|
|
863
|
+
origConsoleMethods[m] = console[m]
|
|
864
|
+
console[m] = captureLog(m)
|
|
865
|
+
}
|
|
422
866
|
|
|
423
|
-
const
|
|
424
|
-
|
|
867
|
+
const scope = {
|
|
868
|
+
locate, within, session, secret, inject, pause, share: container.share,
|
|
869
|
+
tryTo, retryTo, hopeThat,
|
|
870
|
+
step, element, eachElement, expectElement, expectAnyElement, expectAllElements,
|
|
871
|
+
container, helpers: container.helpers(),
|
|
872
|
+
...support,
|
|
873
|
+
}
|
|
874
|
+
const paramNames = ['I', ...Object.keys(scope).filter(k => k !== 'I').sort()]
|
|
875
|
+
const paramValues = paramNames.map(k => scope[k])
|
|
425
876
|
|
|
426
|
-
const
|
|
877
|
+
const wasPaused = !!pausedController
|
|
878
|
+
if (wasPaused) recorder.session.start('mcp_run_code')
|
|
427
879
|
|
|
880
|
+
let returnValue
|
|
428
881
|
try {
|
|
429
|
-
const asyncFn = new Function(
|
|
430
|
-
await Promise.race([
|
|
431
|
-
asyncFn(
|
|
882
|
+
const asyncFn = new Function(...paramNames, `return (async () => { ${code} })()`)
|
|
883
|
+
returnValue = await Promise.race([
|
|
884
|
+
asyncFn(...paramValues),
|
|
432
885
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
433
886
|
])
|
|
887
|
+
await recorder.promise()
|
|
434
888
|
|
|
435
889
|
result.status = 'success'
|
|
436
890
|
result.output = 'Code executed successfully'
|
|
437
|
-
|
|
438
|
-
if (saveArtifacts) {
|
|
439
|
-
const helpers = container.helpers()
|
|
440
|
-
const helper = Object.values(helpers)[0]
|
|
441
|
-
if (helper) {
|
|
442
|
-
try {
|
|
443
|
-
const traceDir = getTraceDir('mcp', 'run_code')
|
|
444
|
-
mkdirp.sync(traceDir)
|
|
445
|
-
|
|
446
|
-
if (helper.grabAriaSnapshot) {
|
|
447
|
-
const aria = await helper.grabAriaSnapshot()
|
|
448
|
-
const ariaFile = path.join(traceDir, 'aria.txt')
|
|
449
|
-
writeFileSync(ariaFile, aria)
|
|
450
|
-
result.artifacts.aria = `file://${ariaFile}`
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (helper.grabCurrentUrl) {
|
|
454
|
-
result.artifacts.url = await helper.grabCurrentUrl()
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (helper.grabBrowserLogs) {
|
|
458
|
-
const logs = (await helper.grabBrowserLogs()) || []
|
|
459
|
-
const logsFile = path.join(traceDir, 'console.json')
|
|
460
|
-
writeFileSync(logsFile, JSON.stringify(logs, null, 2))
|
|
461
|
-
result.artifacts.consoleLogs = `file://${logsFile}`
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (helper.grabSource) {
|
|
465
|
-
const html = await helper.grabSource()
|
|
466
|
-
const htmlFile = path.join(traceDir, 'page.html')
|
|
467
|
-
writeFileSync(htmlFile, html)
|
|
468
|
-
result.artifacts.html = `file://${htmlFile}`
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (helper.saveScreenshot) {
|
|
472
|
-
const screenshotFile = path.join(traceDir, 'screenshot.png')
|
|
473
|
-
await helper.saveScreenshot(screenshotFile)
|
|
474
|
-
result.artifacts.screenshot = `file://${screenshotFile}`
|
|
475
|
-
}
|
|
476
|
-
} catch (e) {
|
|
477
|
-
result.output += ` (Warning: ${e.message})`
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
891
|
} catch (error) {
|
|
482
892
|
result.status = 'failed'
|
|
483
893
|
result.error = error.message
|
|
484
894
|
result.output = error.stack || error.message
|
|
895
|
+
} finally {
|
|
896
|
+
for (const m of consoleMethods) console[m] = origConsoleMethods[m]
|
|
897
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
898
|
+
try { event.dispatcher.removeListener(event.step.passed, onStepPassed) } catch {}
|
|
899
|
+
if (wasPaused) {
|
|
900
|
+
try { recorder.session.restore('mcp_run_code') } catch {}
|
|
901
|
+
} else {
|
|
902
|
+
try { recorder.reset() } catch {}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
result.commands = commands
|
|
907
|
+
result.logs = consoleLogs
|
|
908
|
+
if (consoleLogs.length === MAX_LOG_ENTRIES) result.logsTruncated = true
|
|
909
|
+
result.availableObjects = paramNames
|
|
910
|
+
|
|
911
|
+
if (returnValue === undefined) returnValue = await Promise.resolve(lastStepValue)
|
|
912
|
+
returnValue = await formatReturnValue(returnValue)
|
|
913
|
+
|
|
914
|
+
if (returnValue !== undefined) {
|
|
915
|
+
const json = typeof returnValue === 'string' ? returnValue : safeStringify(returnValue, [], 2)
|
|
916
|
+
const stringified = truncateString(json, MAX_RETURN_BYTES)
|
|
917
|
+
result.returnValue = stringified.value
|
|
918
|
+
if (stringified.truncated) result.returnValueTruncated = true
|
|
485
919
|
}
|
|
486
920
|
|
|
921
|
+
let captured = {}
|
|
922
|
+
if (saveArtifacts) {
|
|
923
|
+
const helper = pickActingHelper(container.helpers())
|
|
924
|
+
if (helper) {
|
|
925
|
+
try {
|
|
926
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
927
|
+
captured = await captureSnapshot(helper, { dir: traceDir, prefix: 'mcp' })
|
|
928
|
+
result.artifacts = artifactsToFileUrls(captured, traceDir)
|
|
929
|
+
} catch (e) {
|
|
930
|
+
result.output += ` (Warning: ${e.message})`
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Diff against the latest aiTrace ARIA file produced by the steps
|
|
936
|
+
// that just ran inside this run_code call.
|
|
937
|
+
const ariaAfter = reader.last('aria')
|
|
938
|
+
if (ariaBefore && ariaAfter && ariaBefore !== ariaAfter) {
|
|
939
|
+
const diff = ariaDiff(ariaBefore, ariaAfter)
|
|
940
|
+
if (diff) result.ariaDiff = diff
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const traceFile = writeTraceMarkdown({
|
|
944
|
+
dir: traceDir,
|
|
945
|
+
title: 'run_code',
|
|
946
|
+
file: 'mcp',
|
|
947
|
+
durationMs: Date.now() - startedAt,
|
|
948
|
+
commands,
|
|
949
|
+
captured,
|
|
950
|
+
error: result.error,
|
|
951
|
+
})
|
|
952
|
+
result.dir = traceDir
|
|
953
|
+
result.traceFile = pathToFileURL(traceFile).href
|
|
954
|
+
result.aiTraceHint = aiTraceHint()
|
|
955
|
+
|
|
487
956
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
488
957
|
}
|
|
489
958
|
|
|
490
959
|
case 'run_test': {
|
|
491
960
|
return await withLock(async () => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const {
|
|
496
|
-
|
|
961
|
+
if (pausedController) {
|
|
962
|
+
throw new Error('A previous run_test is still paused. Call "continue" first.')
|
|
963
|
+
}
|
|
964
|
+
const { test, timeout = 60000, pauseAt, grep, plugins } = args || {}
|
|
965
|
+
await initCodecept(undefined, plugins)
|
|
966
|
+
await endShellSession()
|
|
967
|
+
applyMochaGrep(grep)
|
|
968
|
+
|
|
969
|
+
return await withSilencedIO(async () => {
|
|
970
|
+
codecept.loadTests()
|
|
971
|
+
|
|
972
|
+
let testFiles = codecept.testFiles
|
|
973
|
+
if (test) {
|
|
974
|
+
const testName = normalizePath(test).toLowerCase()
|
|
975
|
+
testFiles = codecept.testFiles.filter(f => {
|
|
976
|
+
const filePath = normalizePath(f).toLowerCase()
|
|
977
|
+
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
978
|
+
})
|
|
979
|
+
}
|
|
497
980
|
|
|
498
|
-
|
|
499
|
-
|
|
981
|
+
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
982
|
+
const testFile = testFiles[0]
|
|
983
|
+
|
|
984
|
+
pendingRunResults = []
|
|
985
|
+
pendingTestFile = testFile
|
|
986
|
+
pendingStepInfo = null
|
|
987
|
+
let stepIndex = 0
|
|
988
|
+
const matchPauseAt = pauseAtMatcher(pauseAt)
|
|
989
|
+
|
|
990
|
+
const onAfter = t => {
|
|
991
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
992
|
+
pendingRunResults.push({
|
|
993
|
+
title: t.title,
|
|
994
|
+
file: t.file,
|
|
995
|
+
status: t.err ? 'failed' : 'passed',
|
|
996
|
+
error: t.err?.message,
|
|
997
|
+
duration: t.duration,
|
|
998
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
const onStepAfter = step => {
|
|
1002
|
+
stepIndex += 1
|
|
1003
|
+
const idx = stepIndex
|
|
1004
|
+
const name = (() => { try { return step.toString() } catch { return '' } })()
|
|
1005
|
+
recorder.add('mcp pause info', () => {
|
|
1006
|
+
pendingStepInfo = { index: idx, name, status: step.status }
|
|
1007
|
+
})
|
|
1008
|
+
if (matchPauseAt(idx, name)) pauseNow()
|
|
1009
|
+
}
|
|
1010
|
+
event.dispatcher.on(event.test.after, onAfter)
|
|
1011
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
1012
|
+
pendingRunCleanup = () => {
|
|
1013
|
+
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
1014
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
1015
|
+
pendingRunCleanup = null
|
|
1016
|
+
}
|
|
500
1017
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1018
|
+
let runError = null
|
|
1019
|
+
const runPromise = (async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
await ensureBootstrap()
|
|
1022
|
+
await codecept.run(testFile)
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
runError = err
|
|
1025
|
+
throw err
|
|
1026
|
+
}
|
|
1027
|
+
})()
|
|
504
1028
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
: await runCmd(cli, runArgs, { cwd: root, timeout })
|
|
1029
|
+
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
|
|
1030
|
+
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
|
|
508
1031
|
|
|
509
|
-
|
|
1032
|
+
const which = await Promise.race([
|
|
1033
|
+
completedPromise,
|
|
1034
|
+
pausedPromise,
|
|
1035
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
1036
|
+
])
|
|
510
1037
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1038
|
+
if (which === 'paused') {
|
|
1039
|
+
pendingRunPromise = runPromise
|
|
1040
|
+
const page = await gatherPageBrief()
|
|
1041
|
+
return {
|
|
1042
|
+
content: [{
|
|
1043
|
+
type: 'text',
|
|
1044
|
+
text: JSON.stringify({ ...pausedPayload(), page }, null, 2),
|
|
1045
|
+
}],
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
517
1048
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
meta: { exitCode: code, cli, root, configPath, args: runArgs, resolvedFile: resolvedFile || null },
|
|
523
|
-
reporterJson: parsed,
|
|
524
|
-
stderr: err ? err.slice(0, 20000) : '',
|
|
525
|
-
rawStdout: parsed ? '' : out.slice(0, 20000),
|
|
526
|
-
}, null, 2),
|
|
527
|
-
}],
|
|
528
|
-
}
|
|
1049
|
+
const final = collectRunCompletion(runError?.message)
|
|
1050
|
+
await startShellSession()
|
|
1051
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
1052
|
+
})
|
|
529
1053
|
})
|
|
530
1054
|
}
|
|
531
1055
|
|
|
532
1056
|
case 'run_step_by_step': {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return await withSilencedIO(async () => {
|
|
537
|
-
codecept.loadTests()
|
|
538
|
-
|
|
539
|
-
let testFiles = codecept.testFiles
|
|
540
|
-
if (test) {
|
|
541
|
-
const testName = normalizePath(test).toLowerCase()
|
|
542
|
-
testFiles = codecept.testFiles.filter(f => {
|
|
543
|
-
const filePath = normalizePath(f).toLowerCase()
|
|
544
|
-
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
549
|
-
|
|
550
|
-
const results = []
|
|
551
|
-
const currentSteps = {}
|
|
552
|
-
let currentTestTitle = null
|
|
553
|
-
const testFile = testFiles[0]
|
|
554
|
-
|
|
555
|
-
const onBefore = (t) => {
|
|
556
|
-
const traceDir = getTraceDir(t.title, t.file)
|
|
557
|
-
currentTestTitle = t.title
|
|
558
|
-
currentSteps[t.title] = []
|
|
559
|
-
results.push({
|
|
560
|
-
test: t.title,
|
|
561
|
-
file: t.file,
|
|
562
|
-
traceFile: `file://${resolvePath(traceDir, 'trace.md')}`,
|
|
563
|
-
status: 'running',
|
|
564
|
-
steps: [],
|
|
565
|
-
})
|
|
1057
|
+
return await withLock(async () => {
|
|
1058
|
+
if (pausedController) {
|
|
1059
|
+
throw new Error('A previous run is still paused. Call "continue" first.')
|
|
566
1060
|
}
|
|
1061
|
+
const { test, timeout = 60000, grep, plugins } = args || {}
|
|
1062
|
+
await initCodecept(undefined, plugins)
|
|
1063
|
+
await endShellSession()
|
|
1064
|
+
applyMochaGrep(grep)
|
|
1065
|
+
|
|
1066
|
+
return await withSilencedIO(async () => {
|
|
1067
|
+
codecept.loadTests()
|
|
1068
|
+
|
|
1069
|
+
let testFiles = codecept.testFiles
|
|
1070
|
+
if (test) {
|
|
1071
|
+
const testName = normalizePath(test).toLowerCase()
|
|
1072
|
+
testFiles = codecept.testFiles.filter(f => {
|
|
1073
|
+
const filePath = normalizePath(f).toLowerCase()
|
|
1074
|
+
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
1075
|
+
})
|
|
1076
|
+
}
|
|
567
1077
|
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1078
|
+
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
1079
|
+
const testFile = testFiles[0]
|
|
1080
|
+
|
|
1081
|
+
pendingRunResults = []
|
|
1082
|
+
pendingTestFile = testFile
|
|
1083
|
+
pendingStepInfo = null
|
|
1084
|
+
let stepIndex = 0
|
|
1085
|
+
|
|
1086
|
+
const onAfter = t => {
|
|
1087
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
1088
|
+
pendingRunResults.push({
|
|
1089
|
+
title: t.title,
|
|
1090
|
+
file: t.file,
|
|
1091
|
+
status: t.err ? 'failed' : 'passed',
|
|
1092
|
+
error: t.err?.message,
|
|
1093
|
+
duration: t.duration,
|
|
1094
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
1095
|
+
})
|
|
1096
|
+
}
|
|
1097
|
+
const onStepAfter = step => {
|
|
1098
|
+
stepIndex += 1
|
|
1099
|
+
const idx = stepIndex
|
|
1100
|
+
const name = (() => { try { return step.toString() } catch { return '' } })()
|
|
1101
|
+
recorder.add('mcp pause info', () => {
|
|
1102
|
+
pendingStepInfo = { index: idx, name, status: step.status }
|
|
1103
|
+
})
|
|
1104
|
+
pauseNow()
|
|
1105
|
+
}
|
|
1106
|
+
event.dispatcher.on(event.test.after, onAfter)
|
|
1107
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
1108
|
+
pendingRunCleanup = () => {
|
|
1109
|
+
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
1110
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
1111
|
+
pendingRunCleanup = null
|
|
573
1112
|
}
|
|
574
|
-
currentTestTitle = null
|
|
575
|
-
}
|
|
576
1113
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1114
|
+
let runError = null
|
|
1115
|
+
const runPromise = (async () => {
|
|
1116
|
+
try {
|
|
1117
|
+
await ensureBootstrap()
|
|
1118
|
+
await codecept.run(testFile)
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
runError = err
|
|
1121
|
+
throw err
|
|
1122
|
+
}
|
|
1123
|
+
})()
|
|
587
1124
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
event.dispatcher.on(event.step.after, onStepAfter)
|
|
1125
|
+
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
|
|
1126
|
+
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
|
|
591
1127
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
await codecept.bootstrap()
|
|
596
|
-
await codecept.run(testFile)
|
|
597
|
-
})(),
|
|
1128
|
+
const which = await Promise.race([
|
|
1129
|
+
completedPromise,
|
|
1130
|
+
pausedPromise,
|
|
598
1131
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
599
1132
|
])
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1133
|
+
|
|
1134
|
+
if (which === 'paused') {
|
|
1135
|
+
pendingRunPromise = runPromise
|
|
1136
|
+
const page = await gatherPageBrief()
|
|
1137
|
+
return {
|
|
1138
|
+
content: [{
|
|
1139
|
+
type: 'text',
|
|
1140
|
+
text: JSON.stringify({ ...pausedPayload(), page }, null, 2),
|
|
1141
|
+
}],
|
|
1142
|
+
}
|
|
605
1143
|
}
|
|
606
|
-
} finally {
|
|
607
|
-
try { event.dispatcher.removeListener(event.test.before, onBefore) } catch {}
|
|
608
|
-
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
609
|
-
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
610
|
-
}
|
|
611
1144
|
|
|
612
|
-
|
|
1145
|
+
const final = collectRunCompletion(runError?.message)
|
|
1146
|
+
await startShellSession()
|
|
1147
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
1148
|
+
})
|
|
613
1149
|
})
|
|
614
1150
|
}
|
|
615
1151
|
|
|
@@ -625,6 +1161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
625
1161
|
})
|
|
626
1162
|
|
|
627
1163
|
async function main() {
|
|
1164
|
+
installShutdownHooks()
|
|
628
1165
|
const transport = new StdioServerTransport()
|
|
629
1166
|
await server.connect(transport)
|
|
630
1167
|
}
|