@testdriverai/mcp 7.10.0-canary.0 → 7.10.1-test
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/.claude-plugin/marketplace.json +17 -0
- package/agent/index.js +116 -71
- package/agent/interface.js +13 -1
- package/agent/lib/commands.js +35 -15
- package/agent/lib/provision-commands.js +92 -11
- package/agent/lib/sandbox.js +263 -30
- package/agent/lib/sdk.js +16 -12
- package/agent/lib/system.js +107 -31
- package/ai/.claude-plugin/plugin.json +89 -4
- package/ai/agents/testdriver.md +4 -8
- package/ai/skills/testdriver-assert/SKILL.md +0 -1
- package/ai/skills/testdriver-aws-setup/SKILL.md +1 -1
- package/ai/skills/testdriver-caching/SKILL.md +13 -6
- package/ai/skills/testdriver-ci-cd/SKILL.md +57 -14
- package/ai/skills/testdriver-client/SKILL.md +10 -6
- package/ai/skills/testdriver-customizing-devices/SKILL.md +7 -5
- package/ai/skills/testdriver-dashcam/SKILL.md +36 -3
- package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +28 -4
- package/ai/skills/testdriver-device-config/SKILL.md +5 -5
- package/ai/skills/testdriver-find/SKILL.md +50 -11
- package/ai/skills/testdriver-generating-tests/SKILL.md +163 -8
- package/ai/skills/testdriver-hosted/SKILL.md +1 -1
- package/ai/skills/testdriver-machine-setup/SKILL.md +331 -0
- package/ai/skills/testdriver-making-assertions/SKILL.md +78 -2
- package/ai/skills/testdriver-mcp-workflow/SKILL.md +2 -2
- package/ai/skills/testdriver-parse/SKILL.md +1 -1
- package/ai/skills/testdriver-performing-actions/SKILL.md +97 -3
- package/ai/skills/testdriver-provision/SKILL.md +0 -20
- package/ai/skills/testdriver-quickstart/SKILL.md +299 -21
- package/ai/skills/testdriver-reusable-code/SKILL.md +3 -3
- package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
- package/ai/skills/testdriver-screenshot/SKILL.md +5 -5
- package/ai/skills/testdriver-scroll/SKILL.md +30 -160
- package/ai/skills/testdriver-test-writer/SKILL.md +3 -5
- package/ai/skills/testdriver-testdriver/SKILL.md +3 -7
- package/ai/skills/testdriver-wait/SKILL.md +1 -1
- package/{docs/v7/ai.mdx → ai/skills/testdriver:ai/SKILL.md} +3 -4
- package/ai/skills/testdriver:assert/SKILL.md +315 -0
- package/ai/skills/testdriver:aws-setup/SKILL.md +448 -0
- package/ai/skills/testdriver:cache/SKILL.md +221 -0
- package/ai/skills/testdriver:caching/SKILL.md +124 -0
- package/ai/skills/testdriver:captcha/SKILL.md +158 -0
- package/ai/skills/testdriver:ci-cd/SKILL.md +659 -0
- package/ai/skills/testdriver:click/SKILL.md +286 -0
- package/ai/skills/testdriver:client/SKILL.md +477 -0
- package/ai/skills/testdriver:customizing-devices/SKILL.md +319 -0
- package/ai/skills/testdriver:dashcam/SKILL.md +451 -0
- package/ai/skills/testdriver:debugging-with-screenshots/SKILL.md +415 -0
- package/{docs/v7/device-config.mdx → ai/skills/testdriver:device-config/SKILL.md} +8 -8
- package/ai/skills/testdriver:double-click/SKILL.md +102 -0
- package/ai/skills/testdriver:elements/SKILL.md +605 -0
- package/ai/skills/testdriver:enterprise/SKILL.md +7 -0
- package/ai/skills/testdriver:errors/SKILL.md +246 -0
- package/ai/skills/testdriver:events/SKILL.md +356 -0
- package/ai/skills/testdriver:exec/SKILL.md +317 -0
- package/ai/skills/testdriver:find/SKILL.md +860 -0
- package/ai/skills/testdriver:focus-application/SKILL.md +293 -0
- package/ai/skills/testdriver:generating-tests/SKILL.md +25 -0
- package/ai/skills/testdriver:hosted/SKILL.md +156 -0
- package/ai/skills/testdriver:hover/SKILL.md +278 -0
- package/{docs/v7/locating-elements.mdx → ai/skills/testdriver:locating-elements/SKILL.md} +3 -3
- package/ai/skills/testdriver:making-assertions/SKILL.md +32 -0
- package/ai/skills/testdriver:mcp/SKILL.md +7 -0
- package/ai/skills/testdriver:mouse-down/SKILL.md +161 -0
- package/ai/skills/testdriver:mouse-up/SKILL.md +164 -0
- package/ai/skills/testdriver:parse/SKILL.md +236 -0
- package/ai/skills/testdriver:performing-actions/SKILL.md +53 -0
- package/ai/skills/testdriver:press-keys/SKILL.md +348 -0
- package/ai/skills/testdriver:provision/SKILL.md +331 -0
- package/ai/skills/testdriver:quickstart/SKILL.md +172 -0
- package/ai/skills/testdriver:redraw/SKILL.md +214 -0
- package/ai/skills/testdriver:reusable-code/SKILL.md +249 -0
- package/ai/skills/testdriver:right-click/SKILL.md +123 -0
- package/{docs/v7/running-tests.mdx → ai/skills/testdriver:running-tests/SKILL.md} +4 -4
- package/ai/skills/testdriver:screenshot/SKILL.md +248 -0
- package/ai/skills/testdriver:screenshots/SKILL.md +184 -0
- package/ai/skills/testdriver:scroll/SKILL.md +205 -0
- package/ai/skills/testdriver:secrets/SKILL.md +115 -0
- package/ai/skills/testdriver:self-hosted/SKILL.md +147 -0
- package/ai/skills/testdriver:test-results-json/SKILL.md +257 -0
- package/ai/skills/testdriver:testdriver/SKILL.md +624 -0
- package/ai/skills/testdriver:type/SKILL.md +357 -0
- package/ai/skills/testdriver:variables/SKILL.md +111 -0
- package/ai/skills/testdriver:wait/SKILL.md +50 -0
- package/{docs/v7/waiting-for-elements.mdx → ai/skills/testdriver:waiting-for-elements/SKILL.md} +3 -3
- package/ai/skills/testdriver:what-is-testdriver/SKILL.md +54 -0
- package/debugger/index.html +12 -0
- package/docs/_data/examples-manifest.json +54 -46
- package/docs/_data/examples-manifest.schema.json +12 -2
- package/docs/_scripts/extract-example-urls.js +21 -3
- package/docs/_scripts/generate-examples.js +64 -16
- package/docs/changelog.mdx +29 -2
- package/docs/claude-mcp-plugin.mdx +65 -18
- package/docs/docs.json +16 -33
- package/docs/snippets/tests/scroll-yaml.mdx +2 -2
- package/docs/v6/commands/scroll-until-image.mdx +1 -1
- package/docs/v6/commands/scroll-until-text.mdx +2 -2
- package/docs/v6/commands/scroll.mdx +2 -2
- package/docs/v7/_drafts/agents.mdx +0 -4
- package/docs/v7/_drafts/best-practices.mdx +0 -12
- package/docs/v7/_drafts/commands/scroll-until-image.mdx +1 -1
- package/docs/v7/_drafts/commands/scroll-until-text.mdx +2 -2
- package/docs/v7/_drafts/commands/scroll.mdx +1 -1
- package/docs/v7/_drafts/configuration.mdx +0 -12
- package/docs/v7/_drafts/faq.mdx +2 -10
- package/docs/v7/_drafts/readme.mdx +0 -1
- package/docs/v7/_drafts/troubleshooting.mdx +1 -4
- package/docs/v7/assert.mdx +0 -1
- package/docs/v7/aws-setup.mdx +1 -1
- package/docs/v7/caching.mdx +14 -10
- package/docs/v7/ci-cd.mdx +98 -14
- package/docs/v7/client.mdx +10 -6
- package/docs/v7/copilot/auto-healing.mdx +167 -18
- package/docs/v7/copilot/running-tests.mdx +915 -54
- package/docs/v7/customizing-devices.mdx +7 -5
- package/docs/v7/dashcam.mdx +36 -3
- package/docs/v7/debugging-with-screenshots.mdx +31 -7
- package/docs/v7/examples/ai.mdx +13 -8
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/chrome-extension.mdx +30 -13
- package/docs/v7/examples/element-not-found.mdx +1 -1
- package/docs/v7/examples/findall-coffee-icons.mdx +3 -2
- package/docs/v7/examples/formatted-logging.mdx +74 -0
- package/docs/v7/examples/hover-image.mdx +1 -1
- package/docs/v7/examples/hover-text-with-description.mdx +2 -2
- package/docs/v7/examples/hover-text.mdx +1 -1
- package/docs/v7/examples/installer.mdx +1 -1
- package/docs/v7/examples/launch-vscode-linux.mdx +27 -27
- package/docs/v7/examples/match-image.mdx +1 -1
- package/docs/v7/examples/parse.mdx +3 -2
- package/docs/v7/examples/press-keys.mdx +2 -2
- package/docs/v7/examples/scroll-keyboard.mdx +2 -2
- package/docs/v7/examples/scroll-until-text.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/docs/v7/find.mdx +50 -11
- package/docs/v7/generating-tests.mdx +166 -10
- package/docs/v7/hosted.mdx +1 -1
- package/docs/v7/making-assertions.mdx +81 -4
- package/docs/v7/parse.mdx +1 -1
- package/docs/v7/performing-actions.mdx +100 -5
- package/docs/v7/provision.mdx +0 -20
- package/docs/v7/quickstart.mdx +299 -21
- package/docs/v7/reusable-code.mdx +3 -3
- package/docs/v7/screenshot.mdx +5 -5
- package/docs/v7/scroll.mdx +30 -160
- package/docs/v7/wait.mdx +1 -1
- package/examples/chrome-extension.test.mjs +29 -12
- package/examples/config.mjs +1 -1
- package/examples/launch-vscode-linux.test.mjs +25 -25
- package/interfaces/cli/commands/init.js +84 -2
- package/interfaces/vitest-plugin.mjs +51 -61
- package/lib/core/Dashcam.js +43 -2
- package/lib/core/index.d.ts +13 -1
- package/lib/github-comment.mjs +11 -5
- package/lib/init-project.js +98 -44
- package/lib/install-clients.js +470 -0
- package/lib/provision.js +749 -0
- package/lib/resolve-channel.js +2 -2
- package/lib/sentry.js +15 -2
- package/lib/vitest/hooks.mjs +44 -34
- package/mcp-server/README.md +32 -0
- package/mcp-server/dist/core/actions.d.ts +176 -0
- package/mcp-server/dist/core/actions.js +753 -0
- package/mcp-server/dist/env-utils.d.ts +45 -0
- package/mcp-server/dist/env-utils.js +63 -0
- package/mcp-server/dist/provision-types.d.ts +6 -2
- package/mcp-server/dist/provision-types.js +3 -1
- package/mcp-server/dist/server.mjs +449 -751
- package/mcp-server/package-lock.json +384 -1
- package/mcp-server/package.json +5 -2
- package/mcp-server/src/core/actions.ts +912 -0
- package/mcp-server/src/env-utils.test.ts +82 -0
- package/mcp-server/src/env-utils.ts +77 -0
- package/mcp-server/src/provision-types.ts +4 -1
- package/mcp-server/src/server.ts +533 -954
- package/mcp-server/tsconfig.json +1 -1
- package/mcp-server/vitest.config.ts +7 -0
- package/package.json +15 -3
- package/sdk.d.ts +11 -9
- package/sdk.js +116 -746
- package/setup/aws/spawn-runner.sh +24 -3
- package/docs/v7/_drafts/caching-ai.mdx +0 -215
- package/docs/v7/_drafts/caching.mdx +0 -366
- package/docs/v7/_drafts/prompt-cache.mdx +0 -200
- package/docs/v7/copilot/creating-tests.mdx +0 -156
- package/docs/v7/copilot/github.mdx +0 -143
- package/docs/v7/copilot/setup.mdx +0 -143
- package/docs/v7/examples/exec-output.mdx +0 -84
- package/docs/v7/examples/exec-pwsh.mdx +0 -82
- package/docs/v7/examples/focus-window.mdx +0 -61
- package/docs/v7/examples/scroll-until-image.mdx +0 -82
- package/docs/v7/examples/windows-installer.mdx +0 -95
- package/examples/formatted-logging.test.mjs +0 -27
- package/examples/match-image.test.mjs +0 -55
- package/manual/exec-stream-logs.test.mjs +0 -25
package/lib/resolve-channel.js
CHANGED
|
@@ -64,10 +64,10 @@ const tdEnv = resolveTdEnv();
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Resolves the Sentry environment name.
|
|
67
|
-
* Uses the
|
|
67
|
+
* Uses the release channel (dev | test | canary | stable).
|
|
68
68
|
*/
|
|
69
69
|
function resolveSentryEnvironment() {
|
|
70
|
-
return
|
|
70
|
+
return active;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const sentryEnvironment = resolveSentryEnvironment();
|
package/lib/sentry.js
CHANGED
|
@@ -48,8 +48,8 @@ if (isEnabled()) {
|
|
|
48
48
|
"https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
|
|
49
49
|
environment: sentryEnvironment,
|
|
50
50
|
release: version,
|
|
51
|
-
sampleRate:
|
|
52
|
-
tracesSampleRate:
|
|
51
|
+
sampleRate: 0.01,
|
|
52
|
+
tracesSampleRate: 0.01,
|
|
53
53
|
enableLogs: true,
|
|
54
54
|
integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
|
|
55
55
|
// Set initial context
|
|
@@ -414,6 +414,18 @@ function startTransaction(name, op = "cli") {
|
|
|
414
414
|
return Sentry.startSpan({ name, op });
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Execute a callback within a Sentry performance span.
|
|
419
|
+
* If Sentry is not enabled, the callback is executed directly.
|
|
420
|
+
* @param {Object} options - Span options ({ name, op, attributes })
|
|
421
|
+
* @param {Function} callback - Async callback receiving the span
|
|
422
|
+
* @returns {Promise<*>} Result of the callback
|
|
423
|
+
*/
|
|
424
|
+
async function withSpan(options, callback) {
|
|
425
|
+
if (!isEnabled()) return callback(null);
|
|
426
|
+
return Sentry.startSpan(options, callback);
|
|
427
|
+
}
|
|
428
|
+
|
|
417
429
|
/**
|
|
418
430
|
* Flush pending events before process exit
|
|
419
431
|
* @param {number} timeout - Timeout in milliseconds
|
|
@@ -436,5 +448,6 @@ module.exports = {
|
|
|
436
448
|
getTraceId,
|
|
437
449
|
attachLogListeners,
|
|
438
450
|
startTransaction,
|
|
451
|
+
withSpan,
|
|
439
452
|
flush,
|
|
440
453
|
};
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -433,25 +433,20 @@ export function TestDriver(context, options = {}) {
|
|
|
433
433
|
testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
|
|
434
434
|
testDriverInstances.set(context.task, testdriver);
|
|
435
435
|
|
|
436
|
-
// Set platform metadata early so the reporter can show the correct OS from
|
|
436
|
+
// Set platform metadata early so the reporter can show the correct OS from
|
|
437
|
+
// the start. Test identity (file / suite / name) comes from Vitest's own
|
|
438
|
+
// task tree in the reporter — we deliberately do not duplicate it here.
|
|
437
439
|
if (!context.task.meta) {
|
|
438
440
|
context.task.meta = {};
|
|
439
441
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
context.task.meta.platform = platform;
|
|
450
|
-
context.task.meta.testFile = testFile;
|
|
451
|
-
context.task.meta.testOrder = 0;
|
|
452
|
-
|
|
453
|
-
// Pass test file name to SDK for debugger display
|
|
454
|
-
testdriver.testFile = testFile;
|
|
442
|
+
context.task.meta.platform = mergedOptions.os || "linux";
|
|
443
|
+
|
|
444
|
+
// Pass test file name to SDK for debugger display. Use Vitest's standard
|
|
445
|
+
// runner-task `file.filepath` rather than reconstructing identity ourselves.
|
|
446
|
+
const taskFilepath = context.task.file?.filepath || null;
|
|
447
|
+
testdriver.testFile = taskFilepath
|
|
448
|
+
? path.relative(process.cwd(), taskFilepath)
|
|
449
|
+
: null;
|
|
455
450
|
|
|
456
451
|
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
457
452
|
|
|
@@ -570,21 +565,10 @@ export function TestDriver(context, options = {}) {
|
|
|
570
565
|
context.task.meta = {};
|
|
571
566
|
}
|
|
572
567
|
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
context.task.file?.filepath || context.task.file?.name || "unknown";
|
|
578
|
-
const projectRoot = process.cwd();
|
|
579
|
-
const testFile =
|
|
580
|
-
absolutePath !== "unknown"
|
|
581
|
-
? path.relative(projectRoot, absolutePath)
|
|
582
|
-
: absolutePath;
|
|
583
|
-
|
|
584
|
-
// Set basic metadata that's always available
|
|
585
|
-
context.task.meta.platform = platform;
|
|
586
|
-
context.task.meta.testFile = testFile;
|
|
587
|
-
context.task.meta.testOrder = 0;
|
|
568
|
+
// Refresh SDK-owned metadata. Test identity (file / suite / name) is
|
|
569
|
+
// read directly from the Vitest task tree by the reporter — we only
|
|
570
|
+
// store things Vitest can't know about (platform, session, dashcam).
|
|
571
|
+
context.task.meta.platform = currentInstance.os || "linux";
|
|
588
572
|
context.task.meta.sessionId = currentInstance.getSessionId?.() || null;
|
|
589
573
|
|
|
590
574
|
// Initialize dashcamUrls array for tracking per-attempt URLs (persists across retries)
|
|
@@ -597,6 +581,11 @@ export function TestDriver(context, options = {}) {
|
|
|
597
581
|
const isRetry = attemptNumber > 1;
|
|
598
582
|
const attemptLabel = isRetry ? ` (attempt ${attemptNumber})` : "";
|
|
599
583
|
|
|
584
|
+
// Capture the current attempt's test error (before retry resets result)
|
|
585
|
+
const attemptResult = context.task.result?.errors?.[0];
|
|
586
|
+
const attemptErrorMessage = attemptResult?.message || null;
|
|
587
|
+
const attemptErrorStack = attemptResult?.stack || null;
|
|
588
|
+
|
|
600
589
|
// Stop dashcam if it was started - with timeout to prevent hanging
|
|
601
590
|
if (currentInstance._dashcam && currentInstance._dashcam.recording) {
|
|
602
591
|
try {
|
|
@@ -607,6 +596,8 @@ export function TestDriver(context, options = {}) {
|
|
|
607
596
|
attempt: attemptNumber,
|
|
608
597
|
url: dashcamUrl || null,
|
|
609
598
|
sessionId: currentInstance.getSessionId?.() || null,
|
|
599
|
+
errorMessage: attemptErrorMessage,
|
|
600
|
+
errorStack: attemptErrorStack,
|
|
610
601
|
});
|
|
611
602
|
|
|
612
603
|
// Keep backward compatibility - last attempt's URL
|
|
@@ -617,7 +608,7 @@ export function TestDriver(context, options = {}) {
|
|
|
617
608
|
globalThis.__testdriverPlugin.registerDashcamUrl(
|
|
618
609
|
context.task.id,
|
|
619
610
|
dashcamUrl,
|
|
620
|
-
platform,
|
|
611
|
+
context.task.meta.platform,
|
|
621
612
|
attemptNumber,
|
|
622
613
|
);
|
|
623
614
|
}
|
|
@@ -660,6 +651,8 @@ export function TestDriver(context, options = {}) {
|
|
|
660
651
|
url: null,
|
|
661
652
|
sessionId: currentInstance.getSessionId?.() || null,
|
|
662
653
|
error: error.message,
|
|
654
|
+
errorMessage: attemptErrorMessage,
|
|
655
|
+
errorStack: attemptErrorStack,
|
|
663
656
|
});
|
|
664
657
|
// Ensure dashcamUrl is set to null if stop failed
|
|
665
658
|
context.task.meta.dashcamUrl = null;
|
|
@@ -670,6 +663,8 @@ export function TestDriver(context, options = {}) {
|
|
|
670
663
|
attempt: attemptNumber,
|
|
671
664
|
url: null,
|
|
672
665
|
sessionId: currentInstance.getSessionId?.() || null,
|
|
666
|
+
errorMessage: attemptErrorMessage,
|
|
667
|
+
errorStack: attemptErrorStack,
|
|
673
668
|
});
|
|
674
669
|
context.task.meta.dashcamUrl = null;
|
|
675
670
|
}
|
|
@@ -684,6 +679,21 @@ export function TestDriver(context, options = {}) {
|
|
|
684
679
|
const sbx = currentInstance.sandbox || {};
|
|
685
680
|
const apiRoot = currentInstance.config?.TD_API_ROOT || null;
|
|
686
681
|
|
|
682
|
+
const taskFilepath = context.task.file?.filepath || null;
|
|
683
|
+
const testFileRel = taskFilepath
|
|
684
|
+
? path.relative(process.cwd(), taskFilepath)
|
|
685
|
+
: null;
|
|
686
|
+
const suiteParts = [];
|
|
687
|
+
{
|
|
688
|
+
let cursor = context.task.suite;
|
|
689
|
+
while (cursor && cursor.type === "suite") {
|
|
690
|
+
suiteParts.unshift(cursor.name);
|
|
691
|
+
cursor = cursor.suite;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const fullSuiteName =
|
|
695
|
+
suiteParts.length > 0 ? suiteParts.join(" > ") : null;
|
|
696
|
+
|
|
687
697
|
context.task.meta.testResult = {
|
|
688
698
|
// Versions
|
|
689
699
|
sdkVersion: sdkPkg.version || null,
|
|
@@ -704,9 +714,9 @@ export function TestDriver(context, options = {}) {
|
|
|
704
714
|
sessionId: currentInstance.getSessionId?.() || null,
|
|
705
715
|
|
|
706
716
|
// Test info
|
|
707
|
-
testFile:
|
|
717
|
+
testFile: testFileRel,
|
|
708
718
|
testName: context.task.name || null,
|
|
709
|
-
suiteName:
|
|
719
|
+
suiteName: fullSuiteName,
|
|
710
720
|
|
|
711
721
|
// Test result
|
|
712
722
|
testPassed: context.task.result?.state === "pass",
|
package/mcp-server/README.md
CHANGED
|
@@ -89,6 +89,38 @@ To use the TestDriver MCP server with GitHub Copilot coding agent:
|
|
|
89
89
|
|
|
90
90
|
For more information, see [Extending GitHub Copilot coding agent with Model Context Protocol](https://docs.github.com/en/copilot/customizing-copilot/extending-copilot-coding-agent-with-mcp).
|
|
91
91
|
|
|
92
|
+
### With Claude Code (plugin)
|
|
93
|
+
|
|
94
|
+
TestDriver ships as a [Claude Code plugin](https://docs.claude.com/en/docs/claude-code/plugins) so you get the MCP server, the `testdriver` expert agent, and all TestDriver skills with a single install.
|
|
95
|
+
|
|
96
|
+
1. Set your TestDriver API key in your shell:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export TD_API_KEY=your_api_key_from_https_console_testdriver_ai_team
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
2. Add this repo as a Claude Code plugin marketplace and install the `testdriver` plugin:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Add the marketplace (sources `.claude-plugin/marketplace.json` from this repo)
|
|
106
|
+
/plugin marketplace add testdriverai/testdriverai
|
|
107
|
+
|
|
108
|
+
# Install the plugin
|
|
109
|
+
/plugin install testdriver@testdriver
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This reads `.claude-plugin/plugin.json` (shipped inside the `testdriverai` npm package) and:
|
|
113
|
+
|
|
114
|
+
- Registers the `testdriver` MCP server (`npx -p testdriverai testdriverai-mcp`)
|
|
115
|
+
- Registers the `testdriver` sub-agent (from `ai/agents/testdriver.md`)
|
|
116
|
+
- Registers all TestDriver skills (from `ai/skills/testdriver-*/SKILL.md`)
|
|
117
|
+
|
|
118
|
+
3. In a Claude Code session, delegate to the agent with:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
@testdriver Write a test that signs into example.com and adds an item to the cart.
|
|
122
|
+
```
|
|
123
|
+
|
|
92
124
|
### With Claude Desktop / Cursor
|
|
93
125
|
|
|
94
126
|
Add to your MCP config (`~/.cursor/mcp.json` or `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDriver action core — framework-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the actual computer-use logic: it holds the singleton SDK,
|
|
5
|
+
* the element-reference map, and the "last screenshot" used by `check`, and
|
|
6
|
+
* exposes one async function per action. Every function returns a neutral
|
|
7
|
+
* {@link ActionResult} — no MCP types, no eve types — so it can be wrapped by:
|
|
8
|
+
*
|
|
9
|
+
* - the MCP server (`server.ts`), which turns images into resource URIs and
|
|
10
|
+
* calls `createToolResult`, and
|
|
11
|
+
* - eve `defineTool()` files, which return the text/code/images as eve content.
|
|
12
|
+
*
|
|
13
|
+
* Keep MCP- and eve-specific concerns (Sentry, progress heartbeats, abort
|
|
14
|
+
* racing, image stores, UI resources) in the adapters. This file only knows
|
|
15
|
+
* about the TestDriver SDK and `generateActionCode`.
|
|
16
|
+
*
|
|
17
|
+
* State is process-global and single-session by design: one sandbox at a time,
|
|
18
|
+
* mirroring how the stdio MCP server has always behaved.
|
|
19
|
+
*/
|
|
20
|
+
import { SessionStartInputSchema, type SessionStartInput } from "../provision-types.js";
|
|
21
|
+
import { type SessionState } from "../session.js";
|
|
22
|
+
export { SessionStartInputSchema, type SessionStartInput };
|
|
23
|
+
export interface ActionImage {
|
|
24
|
+
kind: "screenshot" | "cropped";
|
|
25
|
+
/** Raw base64 (no `data:` prefix). */
|
|
26
|
+
base64: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ActionResult {
|
|
29
|
+
/** Whether the action succeeded (assertions/finds may legitimately be false). */
|
|
30
|
+
ok: boolean;
|
|
31
|
+
/** Human/agent-readable summary line(s). */
|
|
32
|
+
text: string;
|
|
33
|
+
/** Structured fields for programmatic consumers and UIs. */
|
|
34
|
+
data: Record<string, unknown>;
|
|
35
|
+
/** Code to append to the test file, when the action produces test code. */
|
|
36
|
+
code?: string;
|
|
37
|
+
/** Images produced by the action (screenshots / cropped element shots). */
|
|
38
|
+
images?: ActionImage[];
|
|
39
|
+
}
|
|
40
|
+
/** Thrown when an action runs without an active/valid session. */
|
|
41
|
+
export declare class NoActiveSessionError extends Error {
|
|
42
|
+
readonly code: "NO_SESSION" | "SESSION_EXPIRED";
|
|
43
|
+
readonly expiredSessionId?: string;
|
|
44
|
+
constructor(code: "NO_SESSION" | "SESSION_EXPIRED", message: string, expiredSessionId?: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Optional adapter-supplied recovery hook. When the singleton SDK is missing or
|
|
48
|
+
* stale at action time, {@link requireActiveSession} calls this to obtain the
|
|
49
|
+
* params needed to rebuild the connection from the still-alive sandbox, then
|
|
50
|
+
* reconnects before throwing NO_SESSION.
|
|
51
|
+
*
|
|
52
|
+
* Why a hook instead of mcp-core owning the handle: the durable facts (sandbox
|
|
53
|
+
* id, config) and the API key must survive a host process recycle, but this
|
|
54
|
+
* module's globals do NOT — they reset on the very recycle we're recovering
|
|
55
|
+
* from. So the *durable owner* (eve's per-session state) registers a resolver
|
|
56
|
+
* that reads its own durable store and re-resolves the key per call. Hosts with
|
|
57
|
+
* one long-lived process (stdio MCP server, CLI) register nothing: the resolver
|
|
58
|
+
* stays null and behavior is unchanged — `requireActiveSession` throws the same
|
|
59
|
+
* NO_SESSION/SESSION_EXPIRED as before.
|
|
60
|
+
*
|
|
61
|
+
* The resolver returns `null` when it has nothing to recover from (no sandbox
|
|
62
|
+
* provisioned this session), in which case we fall through to the normal throw.
|
|
63
|
+
* It is re-registered each step by the adapter (module globals reset on recycle),
|
|
64
|
+
* so a stale closure can't outlive the process it was bound to.
|
|
65
|
+
*/
|
|
66
|
+
export type ReconnectResolver = () => Promise<ReconnectParams | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Register (or clear, with `null`) the recovery hook. Durable adapters call this
|
|
69
|
+
* at the start of each step with a resolver bound to the current tool context.
|
|
70
|
+
*/
|
|
71
|
+
export declare function setReconnectResolver(resolver: ReconnectResolver | null): void;
|
|
72
|
+
/** Expose internals the adapters legitimately need (read-only intent). */
|
|
73
|
+
export declare function getSdk(): any;
|
|
74
|
+
export declare function getLastScreenshotBase64(): string | null;
|
|
75
|
+
export declare function getElementRef(ref: string): {
|
|
76
|
+
element: any;
|
|
77
|
+
description: string;
|
|
78
|
+
coords: {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
centerX: number;
|
|
82
|
+
centerY: number;
|
|
83
|
+
};
|
|
84
|
+
} | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Cheap, side-effect-free check for a usable in-process session: a live SDK and
|
|
87
|
+
* a current session that hasn't expired. Durable adapters call this *before*
|
|
88
|
+
* doing any expensive reconnect prep (e.g. re-resolving an API key), so the
|
|
89
|
+
* common warm-process path stays free. Note: unlike `requireActiveSession`, this
|
|
90
|
+
* does NOT refresh the keepAlive window — it only reports liveness.
|
|
91
|
+
*/
|
|
92
|
+
export declare function hasLiveSession(): boolean;
|
|
93
|
+
export interface SessionStartHooks {
|
|
94
|
+
/** Called before a long await; return a stop fn. Lets adapters heartbeat. */
|
|
95
|
+
onProgress?: (message: string) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Start a session and provision a sandbox. The adapter is responsible for
|
|
99
|
+
* progress/heartbeat and abort; this performs the provisioning and returns the
|
|
100
|
+
* neutral result (including the initial screenshot and provision code).
|
|
101
|
+
*/
|
|
102
|
+
export declare function sessionStart(params: SessionStartInput, resolved: {
|
|
103
|
+
os: "linux" | "windows";
|
|
104
|
+
e2bTemplateId?: string;
|
|
105
|
+
apiKey?: string;
|
|
106
|
+
}, hooks?: SessionStartHooks): Promise<ActionResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Parameters needed to rebuild the singleton SDK and reconnect to a sandbox
|
|
109
|
+
* that is still alive on the server but whose in-process connection was lost
|
|
110
|
+
* (e.g. the host process was recycled between durable workflow steps).
|
|
111
|
+
*/
|
|
112
|
+
export interface ReconnectParams {
|
|
113
|
+
/** Cloud sandbox id (`sb-…`) to reconnect to. */
|
|
114
|
+
sandboxId: string;
|
|
115
|
+
os: "linux" | "windows";
|
|
116
|
+
keepAlive: number;
|
|
117
|
+
apiKey: string;
|
|
118
|
+
apiRoot?: string;
|
|
119
|
+
e2bTemplateId?: string;
|
|
120
|
+
/** Self-hosted instance IP, when the original session used one. */
|
|
121
|
+
ip?: string;
|
|
122
|
+
testFile?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Rebuild the process-global SDK and reconnect it to an existing sandbox by id,
|
|
126
|
+
* skipping provisioning. This is the recovery path for hosts that do NOT keep a
|
|
127
|
+
* single long-lived process: eve runs each turn as a durable workflow on Vercel
|
|
128
|
+
* and may park or recycle the process between steps, which drops the SDK's
|
|
129
|
+
* realtime connection and wipes this module's singletons. The sandbox itself can
|
|
130
|
+
* still be alive on the server (the API holds it for `keepAlive`/grace), so we
|
|
131
|
+
* re-`connect({ sandboxId })` and re-register a local session instead of failing
|
|
132
|
+
* the action with NO_SESSION.
|
|
133
|
+
*
|
|
134
|
+
* Returns the (re)activated session id. Throws if the reconnect fails (the
|
|
135
|
+
* sandbox is gone) — callers translate that into a SESSION_EXPIRED so the agent
|
|
136
|
+
* knows to call session_start again.
|
|
137
|
+
*/
|
|
138
|
+
export declare function reconnectSession(params: ReconnectParams): Promise<string>;
|
|
139
|
+
/**
|
|
140
|
+
* Ensure there is a usable session before an action, reconnecting from durable
|
|
141
|
+
* state if the in-process singleton was lost. Returns true when a fresh
|
|
142
|
+
* reconnect happened (the caller may want to recapture context), false when the
|
|
143
|
+
* existing session was already usable.
|
|
144
|
+
*
|
|
145
|
+
* Durable adapters call this at the top of each tool, passing the sandbox id and
|
|
146
|
+
* config they persisted at session_start. Hosts with a single long-lived process
|
|
147
|
+
* (the stdio MCP server, the CLI) never persist a sandbox id, so they pass none
|
|
148
|
+
* and this is a no-op — `requireActiveSession()` inside each action still guards
|
|
149
|
+
* them exactly as before.
|
|
150
|
+
*/
|
|
151
|
+
export declare function ensureActiveSession(params?: ReconnectParams): Promise<boolean>;
|
|
152
|
+
/** Disconnect the SDK (best-effort); used by adapters on cancel/teardown. */
|
|
153
|
+
export declare function disconnect(): Promise<void>;
|
|
154
|
+
export declare function sessionStatus(): ActionResult;
|
|
155
|
+
export declare function sessionExtend(additionalMs: number): ActionResult;
|
|
156
|
+
export declare function find(description: string, timeout?: number): Promise<ActionResult>;
|
|
157
|
+
export declare function findAll(description: string, timeout?: number): Promise<ActionResult>;
|
|
158
|
+
export declare function click(ref: string, action?: "click" | "double-click" | "right-click"): Promise<ActionResult>;
|
|
159
|
+
export declare function hover(ref: string): Promise<ActionResult>;
|
|
160
|
+
export declare function findAndClick(description: string, action?: "click" | "double-click" | "right-click"): Promise<ActionResult>;
|
|
161
|
+
export declare function type(text: string, secret?: boolean, delay?: number): Promise<ActionResult>;
|
|
162
|
+
export declare function pressKeys(keys: string[]): Promise<ActionResult>;
|
|
163
|
+
export declare function scroll(direction?: "up" | "down" | "left" | "right", amount?: number): Promise<ActionResult>;
|
|
164
|
+
export declare function focusApplication(name: string): Promise<ActionResult>;
|
|
165
|
+
export declare function wait(timeout: number): Promise<ActionResult>;
|
|
166
|
+
export declare function exec(language: "sh" | "pwsh", code: string, timeout?: number): Promise<ActionResult>;
|
|
167
|
+
export declare function assert(assertion: string): Promise<ActionResult>;
|
|
168
|
+
/**
|
|
169
|
+
* AI analysis of the current screen vs. a "before" state. Does NOT generate
|
|
170
|
+
* code. `referenceImage` (bare base64), if provided, is used as the before
|
|
171
|
+
* image instead of the last captured screenshot.
|
|
172
|
+
*/
|
|
173
|
+
export declare function check(task: string, referenceImage?: string): Promise<ActionResult>;
|
|
174
|
+
/** Capture a screenshot for the user (cursor visible). */
|
|
175
|
+
export declare function screenshot(): Promise<ActionResult>;
|
|
176
|
+
export type { SessionState };
|