codeceptjs 4.0.0-rc.18 → 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 +5 -1
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +250 -82
- 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/command/dryRun.js +9 -3
- package/lib/command/init.js +247 -266
- package/lib/command/query.js +218 -0
- package/lib/config.js +9 -0
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +4 -1
- package/lib/html.js +3 -0
- 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/plugin/browser.js +2 -1
- package/lib/plugin/expose.js +159 -0
- package/lib/workers.js +1 -15
- package/package.json +7 -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/seeFileDownloaded.mustache +0 -23
- 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/bin/mcp-server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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'
|
|
@@ -17,6 +18,11 @@ import {
|
|
|
17
18
|
} from '../lib/utils/trace.js'
|
|
18
19
|
import event from '../lib/event.js'
|
|
19
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'
|
|
20
26
|
import { setPauseHandler, pauseNow } from '../lib/pause.js'
|
|
21
27
|
import { EventEmitter } from 'events'
|
|
22
28
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
@@ -39,6 +45,7 @@ let shellSessionActive = false
|
|
|
39
45
|
let bootstrapDone = false
|
|
40
46
|
let currentPluginsSig = ''
|
|
41
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
|
|
42
49
|
|
|
43
50
|
event.dispatcher.on(event.test.before, test => {
|
|
44
51
|
try {
|
|
@@ -47,7 +54,27 @@ event.dispatcher.on(event.test.before, test => {
|
|
|
47
54
|
} catch {}
|
|
48
55
|
})
|
|
49
56
|
|
|
50
|
-
|
|
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
|
+
}
|
|
51
78
|
|
|
52
79
|
async function ensureBootstrap() {
|
|
53
80
|
if (bootstrapDone) return
|
|
@@ -80,9 +107,9 @@ async function endShellSession() {
|
|
|
80
107
|
shellSessionActive = false
|
|
81
108
|
}
|
|
82
109
|
|
|
83
|
-
function ensureSession() {
|
|
110
|
+
async function ensureSession() {
|
|
84
111
|
if (shellSessionActive || pausedController) return
|
|
85
|
-
|
|
112
|
+
await startShellSession()
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
function normalizePluginOverrides(plugins) {
|
|
@@ -109,18 +136,33 @@ function pluginsSignature(plugins) {
|
|
|
109
136
|
|
|
110
137
|
async function teardownContainer() {
|
|
111
138
|
if (!containerInitialized) return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 = ''
|
|
117
149
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
})
|
|
124
166
|
}
|
|
125
167
|
|
|
126
168
|
let runLock = Promise.resolve()
|
|
@@ -331,15 +373,17 @@ function outputBaseDir() {
|
|
|
331
373
|
// pause(), the handler registered via setPauseHandler resolves a "paused"
|
|
332
374
|
// promise that run_test is racing against test completion. The "pause" tool
|
|
333
375
|
// then drives the REPL by mutating next/abort and resolving the controller.
|
|
334
|
-
let pausedController = null
|
|
335
|
-
let pendingRunPromise = null
|
|
336
|
-
let pendingRunResults = null
|
|
337
|
-
let pendingRunCleanup = null
|
|
338
|
-
let pendingTestFile = null
|
|
339
|
-
let pendingStepInfo = null
|
|
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
|
|
340
383
|
const pauseEvents = new EventEmitter()
|
|
341
384
|
|
|
342
385
|
setPauseHandler(({ registeredVariables }) => {
|
|
386
|
+
if (abortRun) return Promise.reject(new Error('MCP session aborted'))
|
|
343
387
|
return new Promise(resolve => {
|
|
344
388
|
pausedController = {
|
|
345
389
|
registeredVariables,
|
|
@@ -352,6 +396,33 @@ setPauseHandler(({ registeredVariables }) => {
|
|
|
352
396
|
})
|
|
353
397
|
})
|
|
354
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
|
+
|
|
355
426
|
async function captureLiveArtifacts(prefix = 'pause') {
|
|
356
427
|
const helper = pickActingHelper(container.helpers())
|
|
357
428
|
if (!helper) return {}
|
|
@@ -388,10 +459,16 @@ function collectRunCompletion(errorMessage) {
|
|
|
388
459
|
pendingRunResults = null
|
|
389
460
|
pendingTestFile = null
|
|
390
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
|
+
}
|
|
391
466
|
return {
|
|
392
|
-
status: 'completed',
|
|
467
|
+
status: error ? 'failed' : 'completed',
|
|
468
|
+
aiTraceDir: currentAiTraceDir,
|
|
393
469
|
reporterJson: { stats, tests: results },
|
|
394
|
-
error
|
|
470
|
+
error,
|
|
471
|
+
aiTraceHint: aiTraceHint(),
|
|
395
472
|
}
|
|
396
473
|
}
|
|
397
474
|
|
|
@@ -399,11 +476,13 @@ function pausedPayload() {
|
|
|
399
476
|
return {
|
|
400
477
|
status: 'paused',
|
|
401
478
|
file: pendingTestFile,
|
|
479
|
+
aiTraceDir: currentAiTraceDir,
|
|
402
480
|
pausedAfter: pendingStepInfo,
|
|
403
481
|
suggestions: [
|
|
404
482
|
'Call snapshot to capture URL/HTML/ARIA/screenshot/console/storage at this point',
|
|
405
483
|
'Call run_code to inspect or manipulate state (e.g. return await I.grabText("h1"))',
|
|
406
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',
|
|
407
486
|
],
|
|
408
487
|
}
|
|
409
488
|
}
|
|
@@ -443,98 +522,133 @@ async function initCodecept(configPath, pluginOverrides) {
|
|
|
443
522
|
// aiTrace is the canonical per-step ARIA/HTML/screenshot capture for MCP.
|
|
444
523
|
// Always on so run_code / continue can read the latest snapshot from disk
|
|
445
524
|
// instead of double-capturing through grabAriaSnapshot etc.
|
|
446
|
-
applyPluginOverrides(config, { aiTrace: {}, ...plugins })
|
|
525
|
+
applyPluginOverrides(config, { aiTrace: { on: 'step' }, browser: { show: false }, ...plugins })
|
|
447
526
|
|
|
448
527
|
codecept = new Codecept(config, {})
|
|
449
528
|
await codecept.init(testRoot)
|
|
450
|
-
await container.create(config, {})
|
|
451
529
|
await container.started()
|
|
452
530
|
|
|
453
531
|
containerInitialized = true
|
|
454
532
|
browserStarted = true
|
|
533
|
+
aiTraceEnabled = config.plugins?.aiTrace?.enabled === true
|
|
455
534
|
currentPluginsSig = sig
|
|
456
535
|
}
|
|
457
536
|
|
|
458
|
-
|
|
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
|
|
543
|
+
}
|
|
459
544
|
|
|
460
545
|
const server = new Server(
|
|
461
546
|
{ name: 'codeceptjs-mcp-server', version: '1.0.0' },
|
|
462
547
|
{ capabilities: { tools: {} } }
|
|
463
548
|
)
|
|
464
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
|
+
|
|
465
566
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
466
567
|
tools: [
|
|
467
568
|
{
|
|
468
569
|
name: 'list_tests',
|
|
469
|
-
description: 'List all tests in the CodeceptJS project',
|
|
470
|
-
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: {} },
|
|
471
572
|
},
|
|
472
573
|
{
|
|
473
574
|
name: 'list_actions',
|
|
474
|
-
description: 'List all available CodeceptJS actions (I.* methods)',
|
|
475
|
-
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: {} },
|
|
476
577
|
},
|
|
477
578
|
{
|
|
478
579
|
name: 'run_code',
|
|
479
|
-
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.).',
|
|
480
581
|
inputSchema: {
|
|
481
582
|
type: 'object',
|
|
482
583
|
properties: {
|
|
483
584
|
code: { type: 'string' },
|
|
484
585
|
timeout: { type: 'number' },
|
|
485
|
-
config: { type: 'string' },
|
|
486
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.' },
|
|
487
588
|
},
|
|
488
589
|
required: ['code'],
|
|
489
590
|
},
|
|
490
591
|
},
|
|
491
592
|
{
|
|
492
593
|
name: 'run_test',
|
|
493
|
-
description: 'Run a specific test. 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.
|
|
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).',
|
|
494
595
|
inputSchema: {
|
|
495
596
|
type: 'object',
|
|
496
597
|
properties: {
|
|
497
598
|
test: { type: 'string' },
|
|
498
599
|
timeout: { type: 'number' },
|
|
499
|
-
|
|
500
|
-
pauseAt: {
|
|
501
|
-
|
|
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,
|
|
502
606
|
},
|
|
503
607
|
required: ['test'],
|
|
504
608
|
},
|
|
505
609
|
},
|
|
506
610
|
{
|
|
507
611
|
name: 'run_step_by_step',
|
|
508
|
-
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.
|
|
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.',
|
|
509
613
|
inputSchema: {
|
|
510
614
|
type: 'object',
|
|
511
615
|
properties: {
|
|
512
616
|
test: { type: 'string' },
|
|
513
617
|
timeout: { type: 'number' },
|
|
514
|
-
|
|
515
|
-
plugins:
|
|
618
|
+
grep: { type: 'string', description: 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' },
|
|
619
|
+
plugins: PLUGINS_PROP,
|
|
516
620
|
},
|
|
517
621
|
required: ['test'],
|
|
518
622
|
},
|
|
519
623
|
},
|
|
520
624
|
{
|
|
521
625
|
name: 'start_browser',
|
|
522
|
-
description: 'Start the
|
|
523
|
-
|
|
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
|
+
},
|
|
524
638
|
},
|
|
525
639
|
{
|
|
526
640
|
name: 'stop_browser',
|
|
527
|
-
description: 'Stop the
|
|
641
|
+
description: 'Stop the session, close browsers, and tear down the container. Required before re-initing with different config or plugins.',
|
|
528
642
|
inputSchema: { type: 'object', properties: {} },
|
|
529
643
|
},
|
|
530
644
|
{
|
|
531
645
|
name: 'snapshot',
|
|
532
|
-
description: 'Capture current browser state (HTML, ARIA, screenshot, console, URL) without performing any action.',
|
|
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.',
|
|
533
647
|
inputSchema: {
|
|
534
648
|
type: 'object',
|
|
535
649
|
properties: {
|
|
536
|
-
config: { type: 'string' },
|
|
537
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.' },
|
|
538
652
|
},
|
|
539
653
|
},
|
|
540
654
|
},
|
|
@@ -548,6 +662,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
548
662
|
},
|
|
549
663
|
},
|
|
550
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.',
|
|
668
|
+
inputSchema: { type: 'object', properties: {} },
|
|
669
|
+
},
|
|
551
670
|
],
|
|
552
671
|
}))
|
|
553
672
|
|
|
@@ -557,8 +676,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
557
676
|
try {
|
|
558
677
|
switch (name) {
|
|
559
678
|
case 'list_tests': {
|
|
560
|
-
|
|
561
|
-
await initCodecept(configPath)
|
|
679
|
+
await initCodecept()
|
|
562
680
|
|
|
563
681
|
codecept.loadTests()
|
|
564
682
|
const tests = codecept.testFiles.map(testFile => {
|
|
@@ -573,8 +691,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
573
691
|
}
|
|
574
692
|
|
|
575
693
|
case 'list_actions': {
|
|
576
|
-
|
|
577
|
-
await initCodecept(configPath)
|
|
694
|
+
await initCodecept()
|
|
578
695
|
|
|
579
696
|
const helpers = container.helpers()
|
|
580
697
|
const supportI = container.support('I')
|
|
@@ -602,27 +719,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
602
719
|
}
|
|
603
720
|
|
|
604
721
|
case 'start_browser': {
|
|
605
|
-
const configPath = args
|
|
722
|
+
const { config: configPath, plugins } = args || {}
|
|
606
723
|
if (browserStarted && shellSessionActive) {
|
|
607
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session already active' }, null, 2) }] }
|
|
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
|
|
608
732
|
}
|
|
609
|
-
await initCodecept(configPath)
|
|
610
733
|
await startShellSession()
|
|
611
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session started — run_code and snapshot are now available' }, null, 2) }] }
|
|
734
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session started — run_code and snapshot are now available', plugins: plugins ?? null }, null, 2) }] }
|
|
612
735
|
}
|
|
613
736
|
|
|
614
737
|
case 'stop_browser': {
|
|
615
738
|
if (!containerInitialized) {
|
|
616
739
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser not initialized' }, null, 2) }] }
|
|
617
740
|
}
|
|
618
|
-
await
|
|
619
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser stopped
|
|
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) }] }
|
|
620
743
|
}
|
|
621
744
|
|
|
622
745
|
case 'snapshot': {
|
|
623
|
-
const {
|
|
624
|
-
await initCodecept(
|
|
625
|
-
ensureSession()
|
|
746
|
+
const { fullPage = false, settleMs = 300 } = args || {}
|
|
747
|
+
await initCodecept()
|
|
748
|
+
await ensureSession()
|
|
626
749
|
|
|
627
750
|
const helper = pickActingHelper(container.helpers())
|
|
628
751
|
if (!helper) throw new Error('No supported acting helper available (Playwright, Puppeteer, WebDriver).')
|
|
@@ -630,6 +753,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
630
753
|
const dir = snapshotDirFor(outputBaseDir())
|
|
631
754
|
mkdirp.sync(dir)
|
|
632
755
|
|
|
756
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
633
757
|
const captured = await captureSnapshot(helper, { dir, prefix: 'snapshot', fullPage })
|
|
634
758
|
const traceFile = writeTraceMarkdown({
|
|
635
759
|
dir,
|
|
@@ -648,6 +772,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
648
772
|
dir,
|
|
649
773
|
traceFile: pathToFileURL(traceFile).href,
|
|
650
774
|
artifacts: artifactsToFileUrls(captured, dir),
|
|
775
|
+
aiTraceHint: aiTraceHint(),
|
|
651
776
|
}, null, 2),
|
|
652
777
|
}],
|
|
653
778
|
}
|
|
@@ -684,21 +809,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
684
809
|
})
|
|
685
810
|
}
|
|
686
811
|
|
|
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) }] }
|
|
816
|
+
}
|
|
817
|
+
|
|
687
818
|
case 'run_code': {
|
|
688
|
-
const { code, timeout = 60000,
|
|
689
|
-
await initCodecept(
|
|
690
|
-
ensureSession()
|
|
819
|
+
const { code, timeout = 60000, saveArtifacts = true, settleMs = 300 } = args
|
|
820
|
+
await initCodecept()
|
|
821
|
+
await ensureSession()
|
|
691
822
|
|
|
692
|
-
const
|
|
693
|
-
if (!I) throw new Error('I object not available. Make sure helpers are configured.')
|
|
823
|
+
const support = container.supportObjects() || {}
|
|
824
|
+
if (!support.I) throw new Error('I object not available. Make sure helpers are configured.')
|
|
694
825
|
|
|
695
826
|
const result = { status: 'unknown', output: '', error: null, commands: [], artifacts: {} }
|
|
696
827
|
|
|
697
828
|
const commands = []
|
|
829
|
+
let lastStepValue
|
|
698
830
|
const onStepAfter = step => {
|
|
699
831
|
try { commands.push(step.toString()) } catch {}
|
|
700
832
|
}
|
|
833
|
+
const onStepPassed = (step, val) => {
|
|
834
|
+
if (val !== undefined) lastStepValue = val
|
|
835
|
+
}
|
|
701
836
|
event.dispatcher.on(event.step.after, onStepAfter)
|
|
837
|
+
event.dispatcher.on(event.step.passed, onStepPassed)
|
|
702
838
|
|
|
703
839
|
const traceDir = traceDirFor(`mcp_${Date.now()}`, 'run_code', outputBaseDir())
|
|
704
840
|
mkdirp.sync(traceDir)
|
|
@@ -728,13 +864,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
728
864
|
console[m] = captureLog(m)
|
|
729
865
|
}
|
|
730
866
|
|
|
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])
|
|
876
|
+
|
|
877
|
+
const wasPaused = !!pausedController
|
|
878
|
+
if (wasPaused) recorder.session.start('mcp_run_code')
|
|
879
|
+
|
|
731
880
|
let returnValue
|
|
732
881
|
try {
|
|
733
|
-
const asyncFn = new Function(
|
|
882
|
+
const asyncFn = new Function(...paramNames, `return (async () => { ${code} })()`)
|
|
734
883
|
returnValue = await Promise.race([
|
|
735
|
-
asyncFn(
|
|
884
|
+
asyncFn(...paramValues),
|
|
736
885
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
737
886
|
])
|
|
887
|
+
await recorder.promise()
|
|
738
888
|
|
|
739
889
|
result.status = 'success'
|
|
740
890
|
result.output = 'Code executed successfully'
|
|
@@ -745,11 +895,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
745
895
|
} finally {
|
|
746
896
|
for (const m of consoleMethods) console[m] = origConsoleMethods[m]
|
|
747
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
|
+
}
|
|
748
904
|
}
|
|
749
905
|
|
|
750
906
|
result.commands = commands
|
|
751
907
|
result.logs = consoleLogs
|
|
752
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)
|
|
753
913
|
|
|
754
914
|
if (returnValue !== undefined) {
|
|
755
915
|
const json = typeof returnValue === 'string' ? returnValue : safeStringify(returnValue, [], 2)
|
|
@@ -763,6 +923,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
763
923
|
const helper = pickActingHelper(container.helpers())
|
|
764
924
|
if (helper) {
|
|
765
925
|
try {
|
|
926
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
766
927
|
captured = await captureSnapshot(helper, { dir: traceDir, prefix: 'mcp' })
|
|
767
928
|
result.artifacts = artifactsToFileUrls(captured, traceDir)
|
|
768
929
|
} catch (e) {
|
|
@@ -790,6 +951,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
790
951
|
})
|
|
791
952
|
result.dir = traceDir
|
|
792
953
|
result.traceFile = pathToFileURL(traceFile).href
|
|
954
|
+
result.aiTraceHint = aiTraceHint()
|
|
793
955
|
|
|
794
956
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
795
957
|
}
|
|
@@ -799,9 +961,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
799
961
|
if (pausedController) {
|
|
800
962
|
throw new Error('A previous run_test is still paused. Call "continue" first.')
|
|
801
963
|
}
|
|
802
|
-
const { test, timeout = 60000,
|
|
803
|
-
await initCodecept(
|
|
964
|
+
const { test, timeout = 60000, pauseAt, grep, plugins } = args || {}
|
|
965
|
+
await initCodecept(undefined, plugins)
|
|
804
966
|
await endShellSession()
|
|
967
|
+
applyMochaGrep(grep)
|
|
805
968
|
|
|
806
969
|
return await withSilencedIO(async () => {
|
|
807
970
|
codecept.loadTests()
|
|
@@ -822,26 +985,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
822
985
|
pendingTestFile = testFile
|
|
823
986
|
pendingStepInfo = null
|
|
824
987
|
let stepIndex = 0
|
|
988
|
+
const matchPauseAt = pauseAtMatcher(pauseAt)
|
|
825
989
|
|
|
826
990
|
const onAfter = t => {
|
|
991
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
827
992
|
pendingRunResults.push({
|
|
828
993
|
title: t.title,
|
|
829
994
|
file: t.file,
|
|
830
995
|
status: t.err ? 'failed' : 'passed',
|
|
831
996
|
error: t.err?.message,
|
|
832
997
|
duration: t.duration,
|
|
998
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
833
999
|
})
|
|
834
1000
|
}
|
|
835
1001
|
const onStepAfter = step => {
|
|
836
1002
|
stepIndex += 1
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
pendingStepInfo = { index:
|
|
841
|
-
}
|
|
842
|
-
if (
|
|
843
|
-
pauseNow()
|
|
844
|
-
}
|
|
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()
|
|
845
1009
|
}
|
|
846
1010
|
event.dispatcher.on(event.test.after, onAfter)
|
|
847
1011
|
event.dispatcher.on(event.step.after, onStepAfter)
|
|
@@ -883,6 +1047,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
883
1047
|
}
|
|
884
1048
|
|
|
885
1049
|
const final = collectRunCompletion(runError?.message)
|
|
1050
|
+
await startShellSession()
|
|
886
1051
|
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
887
1052
|
})
|
|
888
1053
|
})
|
|
@@ -893,9 +1058,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
893
1058
|
if (pausedController) {
|
|
894
1059
|
throw new Error('A previous run is still paused. Call "continue" first.')
|
|
895
1060
|
}
|
|
896
|
-
const { test, timeout = 60000,
|
|
897
|
-
await initCodecept(
|
|
1061
|
+
const { test, timeout = 60000, grep, plugins } = args || {}
|
|
1062
|
+
await initCodecept(undefined, plugins)
|
|
898
1063
|
await endShellSession()
|
|
1064
|
+
applyMochaGrep(grep)
|
|
899
1065
|
|
|
900
1066
|
return await withSilencedIO(async () => {
|
|
901
1067
|
codecept.loadTests()
|
|
@@ -918,22 +1084,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
918
1084
|
let stepIndex = 0
|
|
919
1085
|
|
|
920
1086
|
const onAfter = t => {
|
|
1087
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
921
1088
|
pendingRunResults.push({
|
|
922
1089
|
title: t.title,
|
|
923
1090
|
file: t.file,
|
|
924
1091
|
status: t.err ? 'failed' : 'passed',
|
|
925
1092
|
error: t.err?.message,
|
|
926
1093
|
duration: t.duration,
|
|
1094
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
927
1095
|
})
|
|
928
1096
|
}
|
|
929
1097
|
const onStepAfter = step => {
|
|
930
1098
|
stepIndex += 1
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
pendingStepInfo = { index:
|
|
935
|
-
}
|
|
936
|
-
// Pause after every step — agent calls continue to advance.
|
|
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
|
+
})
|
|
937
1104
|
pauseNow()
|
|
938
1105
|
}
|
|
939
1106
|
event.dispatcher.on(event.test.after, onAfter)
|
|
@@ -975,8 +1142,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
975
1142
|
}
|
|
976
1143
|
}
|
|
977
1144
|
|
|
978
|
-
// Test had zero steps (or finished before first pause) — return completion
|
|
979
1145
|
const final = collectRunCompletion(runError?.message)
|
|
1146
|
+
await startShellSession()
|
|
980
1147
|
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
981
1148
|
})
|
|
982
1149
|
})
|
|
@@ -994,6 +1161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
994
1161
|
})
|
|
995
1162
|
|
|
996
1163
|
async function main() {
|
|
1164
|
+
installShutdownHooks()
|
|
997
1165
|
const transport = new StdioServerTransport()
|
|
998
1166
|
await server.connect(transport)
|
|
999
1167
|
}
|