testdriverai 7.5.26 → 7.6.0-test.1
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/CHANGELOG.md +28 -19
- package/README.md +1 -0
- package/agent/lib/config.js +3 -1
- package/agent/lib/sandbox.js +6 -4
- package/ai/agents/testdriver.md +0 -3
- package/ai/skills/testdriver-exec/SKILL.md +23 -40
- package/ai/skills/testdriver-test-writer/SKILL.md +0 -3
- package/ai/skills/testdriver-testdriver/SKILL.md +0 -3
- package/channel.json +9 -0
- package/docs/images/content/parse/output.png +0 -0
- package/docs/v6/commands/exec.mdx +15 -21
- package/docs/v7/_drafts/agents.mdx +4 -13
- package/docs/v7/_drafts/commands/exec.mdx +15 -21
- package/docs/v7/exec.mdx +36 -64
- package/docs/v7/quickstart.mdx +1 -1
- package/interfaces/cli/commands/init.js +2 -1
- package/interfaces/vitest-plugin.mjs +23 -2
- package/lib/core/Dashcam.js +23 -2
- package/lib/init-project.js +67 -27
- package/lib/vitest/hooks.mjs +2 -1
- package/mcp-server/README.md +12 -2
- package/mcp-server/dist/codegen.js +1 -1
- package/mcp-server/dist/server.mjs +55 -5
- package/mcp-server/src/codegen.ts +1 -1
- package/mcp-server/src/server.ts +49 -5
- package/package.json +6 -2
- package/sdk.d.ts +2 -2
- package/sdk.js +49 -4
- package/vitest.config.mjs +56 -12
|
@@ -9,6 +9,7 @@ import { setTestRunInfo } from "./shared-test-state.mjs";
|
|
|
9
9
|
|
|
10
10
|
// Use createRequire to import CommonJS modules without esbuild processing
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
+
const channelConfig = require("../channel.json");
|
|
12
13
|
|
|
13
14
|
// Import Sentry for error reporting
|
|
14
15
|
const Sentry = require("@sentry/node");
|
|
@@ -763,7 +764,7 @@ export default function testDriverPlugin(options = {}) {
|
|
|
763
764
|
pluginState.apiRoot =
|
|
764
765
|
options.apiRoot ||
|
|
765
766
|
process.env.TD_API_ROOT ||
|
|
766
|
-
|
|
767
|
+
channelConfig.channels[channelConfig.active];
|
|
767
768
|
pluginState.ciProvider = detectCI();
|
|
768
769
|
pluginState.gitInfo = getGitInfo();
|
|
769
770
|
|
|
@@ -822,7 +823,7 @@ class TestDriverReporter {
|
|
|
822
823
|
pluginState.apiRoot =
|
|
823
824
|
this.options.apiRoot ||
|
|
824
825
|
process.env.TD_API_ROOT ||
|
|
825
|
-
|
|
826
|
+
channelConfig.channels[channelConfig.active];
|
|
826
827
|
logger.debug("API key from options:", !!this.options.apiKey);
|
|
827
828
|
logger.debug("API key from env (at onInit):", !!process.env.TD_API_KEY);
|
|
828
829
|
logger.debug("API root from options:", this.options.apiRoot);
|
|
@@ -1258,6 +1259,26 @@ function getConsoleUrl(apiRoot) {
|
|
|
1258
1259
|
return `http://localhost:3001`;
|
|
1259
1260
|
}
|
|
1260
1261
|
|
|
1262
|
+
// Render PR previews: map API service to Web service
|
|
1263
|
+
// canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
|
|
1264
|
+
// testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
|
|
1265
|
+
const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
|
|
1266
|
+
if (renderPrMatch) {
|
|
1267
|
+
const [, prefix, suffix, prSuffix] = renderPrMatch;
|
|
1268
|
+
// Map API naming to Web naming:
|
|
1269
|
+
// canary-api -> canary-web
|
|
1270
|
+
// testdriver-api-i4m4 -> web-i4m4
|
|
1271
|
+
let webPrefix;
|
|
1272
|
+
if (prefix === 'testdriver' && suffix) {
|
|
1273
|
+
// testdriver-api-i4m4 -> web-i4m4
|
|
1274
|
+
webPrefix = 'web' + suffix;
|
|
1275
|
+
} else {
|
|
1276
|
+
// canary-api -> canary-web
|
|
1277
|
+
webPrefix = prefix + '-web';
|
|
1278
|
+
}
|
|
1279
|
+
return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1261
1282
|
// Other tunnels or unknown hosts: return as-is
|
|
1262
1283
|
return apiRoot;
|
|
1263
1284
|
}
|
package/lib/core/Dashcam.js
CHANGED
|
@@ -80,8 +80,9 @@ class Dashcam {
|
|
|
80
80
|
* @private
|
|
81
81
|
*/
|
|
82
82
|
_getApiRoot() {
|
|
83
|
+
const channelConfig = require("../../channel.json");
|
|
83
84
|
return (
|
|
84
|
-
this.client.config?.TD_API_ROOT ||
|
|
85
|
+
this.client.config?.TD_API_ROOT || channelConfig.channels[channelConfig.active]
|
|
85
86
|
);
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -91,7 +92,7 @@ class Dashcam {
|
|
|
91
92
|
* @param {string} apiRoot - The API root URL
|
|
92
93
|
* @returns {string} The corresponding console URL
|
|
93
94
|
*/
|
|
94
|
-
static getConsoleUrl(apiRoot = "
|
|
95
|
+
static getConsoleUrl(apiRoot = (() => { const c = require("../../channel.json"); return c.channels[c.active]; })()) {
|
|
95
96
|
// Allow explicit override via env (e.g. VITE_DOMAIN from .env)
|
|
96
97
|
if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
|
|
97
98
|
|
|
@@ -110,6 +111,26 @@ class Dashcam {
|
|
|
110
111
|
return "http://localhost:3001";
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// Render PR previews: map API service to Web service
|
|
115
|
+
// canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
|
|
116
|
+
// testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
|
|
117
|
+
const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
|
|
118
|
+
if (renderPrMatch) {
|
|
119
|
+
const [, prefix, suffix, prSuffix] = renderPrMatch;
|
|
120
|
+
// Map API naming to Web naming:
|
|
121
|
+
// canary-api -> canary-web
|
|
122
|
+
// testdriver-api-i4m4 -> web-i4m4
|
|
123
|
+
let webPrefix;
|
|
124
|
+
if (prefix === 'testdriver' && suffix) {
|
|
125
|
+
// testdriver-api-i4m4 -> web-i4m4
|
|
126
|
+
webPrefix = 'web' + suffix;
|
|
127
|
+
} else {
|
|
128
|
+
// canary-api -> canary-web
|
|
129
|
+
webPrefix = prefix + '-web';
|
|
130
|
+
}
|
|
131
|
+
return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
// Cloudflare tunnels, custom domains, etc.: the web console is served
|
|
114
135
|
// from the same origin as the API, so return apiRoot as-is.
|
|
115
136
|
return apiRoot;
|
package/lib/init-project.js
CHANGED
|
@@ -337,40 +337,80 @@ jobs:
|
|
|
337
337
|
progress("⊘ GitHub workflow already exists");
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
// 6.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
// 6. Setup MCP configuration
|
|
341
|
+
// When triggered from VS Code extension, create .vscode/mcp.json silently
|
|
342
|
+
// When triggered from CLI, use interactive add-mcp for user to select their MCP client
|
|
343
|
+
const isVscodeInit = process.env.TD_INIT_SOURCE === "vscode";
|
|
344
|
+
|
|
345
|
+
if (isVscodeInit) {
|
|
346
|
+
// VS Code extension: create .vscode/mcp.json directly
|
|
347
|
+
const vscodeDir = path.join(targetDir, ".vscode");
|
|
348
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
349
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
350
|
+
}
|
|
345
351
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
const mcpConfigFile = path.join(vscodeDir, "mcp.json");
|
|
353
|
+
if (!fs.existsSync(mcpConfigFile)) {
|
|
354
|
+
const mcpConfig = {
|
|
355
|
+
inputs: [
|
|
356
|
+
{
|
|
357
|
+
type: "promptString",
|
|
358
|
+
id: "testdriver-api-key",
|
|
359
|
+
description: "TestDriver API Key From https://console.testdriver.ai/team",
|
|
360
|
+
password: true,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
servers: {
|
|
364
|
+
testdriver: {
|
|
365
|
+
command: "npx",
|
|
366
|
+
args: ["-p", "testdriverai", "testdriverai-mcp"],
|
|
367
|
+
env: {
|
|
368
|
+
TD_API_KEY: "${input:testdriver-api-key}",
|
|
369
|
+
},
|
|
363
370
|
},
|
|
364
371
|
},
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
372
|
+
};
|
|
373
|
+
fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
374
|
+
progress("✓ Created MCP config: .vscode/mcp.json");
|
|
375
|
+
} else {
|
|
376
|
+
progress("⊘ MCP config already exists");
|
|
377
|
+
}
|
|
369
378
|
} else {
|
|
370
|
-
|
|
379
|
+
// CLI: use add-mcp for interactive MCP client selection
|
|
380
|
+
progress("🔧 Setting up MCP integration...");
|
|
381
|
+
try {
|
|
382
|
+
const addMcpResult = require("child_process").spawnSync(
|
|
383
|
+
"npx",
|
|
384
|
+
[
|
|
385
|
+
"add-mcp",
|
|
386
|
+
"testdriver",
|
|
387
|
+
"--command",
|
|
388
|
+
"npx -p testdriverai testdriverai-mcp",
|
|
389
|
+
"--env",
|
|
390
|
+
"TD_API_KEY",
|
|
391
|
+
],
|
|
392
|
+
{
|
|
393
|
+
cwd: targetDir,
|
|
394
|
+
stdio: "inherit", // Pass through stdin/stdout for interactive prompts
|
|
395
|
+
shell: process.platform === "win32",
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (addMcpResult.status === 0) {
|
|
400
|
+
progress("✓ MCP configured via add-mcp");
|
|
401
|
+
} else if (addMcpResult.status !== null) {
|
|
402
|
+
progress("⚠ MCP setup skipped or failed - you can run 'npx add-mcp testdriver' later");
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
progress("⚠ Could not run add-mcp - you can run 'npx add-mcp testdriver' later");
|
|
406
|
+
}
|
|
371
407
|
}
|
|
372
408
|
|
|
373
409
|
// 7. Create VSCode extensions recommendations
|
|
410
|
+
const vscodeDir = path.join(targetDir, ".vscode");
|
|
411
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
412
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
413
|
+
}
|
|
374
414
|
const extensionsFile = path.join(vscodeDir, "extensions.json");
|
|
375
415
|
if (!fs.existsSync(extensionsFile)) {
|
|
376
416
|
const extensionsConfig = {
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import TestDriverSDK from "../../sdk.js";
|
|
|
22
22
|
|
|
23
23
|
// Use createRequire to import CommonJS modules
|
|
24
24
|
const require = createRequire(import.meta.url);
|
|
25
|
+
const channelConfig = require("../../channel.json");
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Minimum required Vitest major version
|
|
@@ -255,7 +256,7 @@ async function uploadLogsToReplay(client, dashcamUrl) {
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
// Use the SDK's configured API root (matches what the SDK uses for all other API calls)
|
|
258
|
-
const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT ||
|
|
259
|
+
const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active];
|
|
259
260
|
|
|
260
261
|
console.log(`[TestDriver] Uploading logs for replay ${replayId} to ${apiRoot}...`);
|
|
261
262
|
|
package/mcp-server/README.md
CHANGED
|
@@ -11,9 +11,19 @@ MCP server that enables AI agents to iteratively build TestDriver tests with vis
|
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
|
-
###
|
|
14
|
+
### Quick Install (Recommended)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Use `add-mcp` to automatically configure TestDriver for your MCP client:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx add-mcp testdriver
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This will prompt you to select your MCP client (VS Code, Cursor, Claude Desktop, etc.) and configure it automatically.
|
|
23
|
+
|
|
24
|
+
### Manual Configuration
|
|
25
|
+
|
|
26
|
+
If you prefer to configure manually, add the following to your MCP config file:
|
|
17
27
|
|
|
18
28
|
```json
|
|
19
29
|
{
|
|
@@ -141,7 +141,7 @@ export function generateActionCode(action, args, result) {
|
|
|
141
141
|
return `const assertResult = await testdriver.assert("${escapeString(assertion)}");\nexpect(assertResult).toBeTruthy();`;
|
|
142
142
|
}
|
|
143
143
|
case "exec": {
|
|
144
|
-
const language = args.language || "
|
|
144
|
+
const language = args.language || "sh";
|
|
145
145
|
const code = args.code;
|
|
146
146
|
const timeout = args.timeout;
|
|
147
147
|
if (code.includes("\n")) {
|
|
@@ -23,9 +23,11 @@ import { sessionManager } from "./session.js";
|
|
|
23
23
|
// Sentry
|
|
24
24
|
// =============================================================================
|
|
25
25
|
// Read version from main package.json (../../package.json from mcp-server/dist/)
|
|
26
|
-
const
|
|
27
|
-
const packageJson = JSON.parse(fs.readFileSync(
|
|
26
|
+
const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
27
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
|
|
28
28
|
const version = packageJson.version || "1.0.0";
|
|
29
|
+
const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
|
|
30
|
+
const releaseChannel = channelConfig.active || "dev";
|
|
29
31
|
const isSentryEnabled = () => {
|
|
30
32
|
if (process.env.TD_TELEMETRY === "false") {
|
|
31
33
|
return false;
|
|
@@ -37,7 +39,7 @@ if (isSentryEnabled()) {
|
|
|
37
39
|
Sentry.init({
|
|
38
40
|
dsn: process.env.SENTRY_DSN ||
|
|
39
41
|
"https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
|
|
40
|
-
environment:
|
|
42
|
+
environment: releaseChannel,
|
|
41
43
|
release: version,
|
|
42
44
|
sampleRate: 1.0,
|
|
43
45
|
tracesSampleRate: 1.0,
|
|
@@ -687,6 +689,7 @@ registerAppTool(server, "find", {
|
|
|
687
689
|
const duration = Date.now() - startTime;
|
|
688
690
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
689
691
|
let croppedImageResourceUri;
|
|
692
|
+
let screenshotResourceUri;
|
|
690
693
|
const croppedImage = rawResponse.croppedImage;
|
|
691
694
|
if (croppedImage) {
|
|
692
695
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -696,6 +699,20 @@ registerAppTool(server, "find", {
|
|
|
696
699
|
// Remove croppedImage from response to avoid context bloat
|
|
697
700
|
delete rawResponse.croppedImage;
|
|
698
701
|
}
|
|
702
|
+
else if (!found) {
|
|
703
|
+
// Element not found and no cropped image - capture a fresh screenshot
|
|
704
|
+
// so the user can see what's currently visible on screen
|
|
705
|
+
try {
|
|
706
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
707
|
+
if (screenshotBase64) {
|
|
708
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
709
|
+
logger.debug("find: Captured screenshot for not-found state");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (e) {
|
|
713
|
+
logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
699
716
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
700
717
|
delete rawResponse.extractedText;
|
|
701
718
|
delete rawResponse.pixelDiffImage;
|
|
@@ -717,6 +734,7 @@ registerAppTool(server, "find", {
|
|
|
717
734
|
element: elementInfo,
|
|
718
735
|
ref: elementRef,
|
|
719
736
|
croppedImageResourceUri,
|
|
737
|
+
screenshotResourceUri,
|
|
720
738
|
duration,
|
|
721
739
|
}, generatedCode);
|
|
722
740
|
}
|
|
@@ -786,6 +804,7 @@ registerAppTool(server, "findall", {
|
|
|
786
804
|
const duration = Date.now() - startTime;
|
|
787
805
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
788
806
|
let croppedImageResourceUri;
|
|
807
|
+
let screenshotResourceUri;
|
|
789
808
|
const croppedImage = rawResponse.croppedImage;
|
|
790
809
|
if (croppedImage) {
|
|
791
810
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -795,6 +814,20 @@ registerAppTool(server, "findall", {
|
|
|
795
814
|
// Remove croppedImage from response to avoid context bloat
|
|
796
815
|
delete rawResponse.croppedImage;
|
|
797
816
|
}
|
|
817
|
+
else if (count === 0) {
|
|
818
|
+
// No elements found and no cropped image - capture a fresh screenshot
|
|
819
|
+
// so the user can see what's currently visible on screen
|
|
820
|
+
try {
|
|
821
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
822
|
+
if (screenshotBase64) {
|
|
823
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
824
|
+
logger.debug("findall: Captured screenshot for not-found state");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
798
831
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
799
832
|
delete rawResponse.extractedText;
|
|
800
833
|
delete rawResponse.pixelDiffImage;
|
|
@@ -810,6 +843,7 @@ registerAppTool(server, "findall", {
|
|
|
810
843
|
refs,
|
|
811
844
|
elements: elementInfos,
|
|
812
845
|
croppedImageResourceUri,
|
|
846
|
+
screenshotResourceUri,
|
|
813
847
|
duration,
|
|
814
848
|
}, generatedCode);
|
|
815
849
|
}
|
|
@@ -1029,6 +1063,7 @@ registerAppTool(server, "find_and_click", {
|
|
|
1029
1063
|
const duration = Date.now() - startTime;
|
|
1030
1064
|
// Store cropped image (screenshot) for resource serving
|
|
1031
1065
|
let croppedImageResourceUri;
|
|
1066
|
+
let screenshotResourceUri;
|
|
1032
1067
|
const croppedImage = rawResponse.croppedImage;
|
|
1033
1068
|
if (croppedImage) {
|
|
1034
1069
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -1037,6 +1072,20 @@ registerAppTool(server, "find_and_click", {
|
|
|
1037
1072
|
croppedImageResourceUri = storeImage(imageData, "screenshot");
|
|
1038
1073
|
delete rawResponse.croppedImage;
|
|
1039
1074
|
}
|
|
1075
|
+
else {
|
|
1076
|
+
// No cropped image - capture a fresh screenshot so the user can see
|
|
1077
|
+
// what's currently visible on screen when element was not found
|
|
1078
|
+
try {
|
|
1079
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1080
|
+
if (screenshotBase64) {
|
|
1081
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
1082
|
+
logger.debug("find_and_click: Captured screenshot for not-found state");
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch (e) {
|
|
1086
|
+
logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1040
1089
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
1041
1090
|
delete rawResponse.extractedText;
|
|
1042
1091
|
delete rawResponse.pixelDiffImage;
|
|
@@ -1045,6 +1094,7 @@ registerAppTool(server, "find_and_click", {
|
|
|
1045
1094
|
action: "find_and_click",
|
|
1046
1095
|
error: "Element not found",
|
|
1047
1096
|
croppedImageResourceUri,
|
|
1097
|
+
screenshotResourceUri,
|
|
1048
1098
|
duration
|
|
1049
1099
|
});
|
|
1050
1100
|
}
|
|
@@ -1372,9 +1422,9 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1372
1422
|
});
|
|
1373
1423
|
// Exec
|
|
1374
1424
|
server.registerTool("exec", {
|
|
1375
|
-
description: "Execute
|
|
1425
|
+
description: "Execute shell or PowerShell commands in the sandbox",
|
|
1376
1426
|
inputSchema: z.object({
|
|
1377
|
-
language: z.enum(["
|
|
1427
|
+
language: z.enum(["sh", "pwsh"]).default("sh"),
|
|
1378
1428
|
code: z.string().describe("Code to execute"),
|
|
1379
1429
|
timeout: z.number().default(30000).describe("Timeout in ms"),
|
|
1380
1430
|
}),
|
|
@@ -161,7 +161,7 @@ export function generateActionCode(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
case "exec": {
|
|
164
|
-
const language = (args.language as string) || "
|
|
164
|
+
const language = (args.language as string) || "sh";
|
|
165
165
|
const code = args.code as string;
|
|
166
166
|
const timeout = args.timeout as number | undefined;
|
|
167
167
|
|
package/mcp-server/src/server.ts
CHANGED
|
@@ -30,9 +30,11 @@ import { sessionManager, type SessionState } from "./session.js";
|
|
|
30
30
|
// =============================================================================
|
|
31
31
|
|
|
32
32
|
// Read version from main package.json (../../package.json from mcp-server/dist/)
|
|
33
|
-
const
|
|
34
|
-
const packageJson = JSON.parse(fs.readFileSync(
|
|
33
|
+
const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
34
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
|
|
35
35
|
const version = packageJson.version || "1.0.0";
|
|
36
|
+
const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
|
|
37
|
+
const releaseChannel = channelConfig.active || "dev";
|
|
36
38
|
|
|
37
39
|
const isSentryEnabled = () => {
|
|
38
40
|
if (process.env.TD_TELEMETRY === "false") {
|
|
@@ -47,7 +49,7 @@ if (isSentryEnabled()) {
|
|
|
47
49
|
dsn:
|
|
48
50
|
process.env.SENTRY_DSN ||
|
|
49
51
|
"https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
|
|
50
|
-
environment:
|
|
52
|
+
environment: releaseChannel,
|
|
51
53
|
release: version,
|
|
52
54
|
sampleRate: 1.0,
|
|
53
55
|
tracesSampleRate: 1.0,
|
|
@@ -867,6 +869,7 @@ registerAppTool(
|
|
|
867
869
|
|
|
868
870
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
869
871
|
let croppedImageResourceUri: string | undefined;
|
|
872
|
+
let screenshotResourceUri: string | undefined;
|
|
870
873
|
const croppedImage = rawResponse.croppedImage;
|
|
871
874
|
if (croppedImage) {
|
|
872
875
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -875,6 +878,18 @@ registerAppTool(
|
|
|
875
878
|
croppedImageResourceUri = storeImage(imageData, "cropped");
|
|
876
879
|
// Remove croppedImage from response to avoid context bloat
|
|
877
880
|
delete rawResponse.croppedImage;
|
|
881
|
+
} else if (!found) {
|
|
882
|
+
// Element not found and no cropped image - capture a fresh screenshot
|
|
883
|
+
// so the user can see what's currently visible on screen
|
|
884
|
+
try {
|
|
885
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
886
|
+
if (screenshotBase64) {
|
|
887
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
888
|
+
logger.debug("find: Captured screenshot for not-found state");
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
892
|
+
}
|
|
878
893
|
}
|
|
879
894
|
|
|
880
895
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -904,6 +919,7 @@ registerAppTool(
|
|
|
904
919
|
element: elementInfo,
|
|
905
920
|
ref: elementRef,
|
|
906
921
|
croppedImageResourceUri,
|
|
922
|
+
screenshotResourceUri,
|
|
907
923
|
duration,
|
|
908
924
|
},
|
|
909
925
|
generatedCode
|
|
@@ -988,6 +1004,7 @@ registerAppTool(
|
|
|
988
1004
|
|
|
989
1005
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
990
1006
|
let croppedImageResourceUri: string | undefined;
|
|
1007
|
+
let screenshotResourceUri: string | undefined;
|
|
991
1008
|
const croppedImage = rawResponse.croppedImage;
|
|
992
1009
|
if (croppedImage) {
|
|
993
1010
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -996,6 +1013,18 @@ registerAppTool(
|
|
|
996
1013
|
croppedImageResourceUri = storeImage(imageData, "cropped");
|
|
997
1014
|
// Remove croppedImage from response to avoid context bloat
|
|
998
1015
|
delete rawResponse.croppedImage;
|
|
1016
|
+
} else if (count === 0) {
|
|
1017
|
+
// No elements found and no cropped image - capture a fresh screenshot
|
|
1018
|
+
// so the user can see what's currently visible on screen
|
|
1019
|
+
try {
|
|
1020
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1021
|
+
if (screenshotBase64) {
|
|
1022
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
1023
|
+
logger.debug("findall: Captured screenshot for not-found state");
|
|
1024
|
+
}
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
1027
|
+
}
|
|
999
1028
|
}
|
|
1000
1029
|
|
|
1001
1030
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -1019,6 +1048,7 @@ registerAppTool(
|
|
|
1019
1048
|
refs,
|
|
1020
1049
|
elements: elementInfos,
|
|
1021
1050
|
croppedImageResourceUri,
|
|
1051
|
+
screenshotResourceUri,
|
|
1022
1052
|
duration,
|
|
1023
1053
|
},
|
|
1024
1054
|
generatedCode
|
|
@@ -1317,6 +1347,7 @@ registerAppTool(
|
|
|
1317
1347
|
|
|
1318
1348
|
// Store cropped image (screenshot) for resource serving
|
|
1319
1349
|
let croppedImageResourceUri: string | undefined;
|
|
1350
|
+
let screenshotResourceUri: string | undefined;
|
|
1320
1351
|
const croppedImage = rawResponse.croppedImage;
|
|
1321
1352
|
if (croppedImage) {
|
|
1322
1353
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -1324,6 +1355,18 @@ registerAppTool(
|
|
|
1324
1355
|
: croppedImage;
|
|
1325
1356
|
croppedImageResourceUri = storeImage(imageData, "screenshot");
|
|
1326
1357
|
delete rawResponse.croppedImage;
|
|
1358
|
+
} else {
|
|
1359
|
+
// No cropped image - capture a fresh screenshot so the user can see
|
|
1360
|
+
// what's currently visible on screen when element was not found
|
|
1361
|
+
try {
|
|
1362
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1363
|
+
if (screenshotBase64) {
|
|
1364
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
1365
|
+
logger.debug("find_and_click: Captured screenshot for not-found state");
|
|
1366
|
+
}
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
1369
|
+
}
|
|
1327
1370
|
}
|
|
1328
1371
|
|
|
1329
1372
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -1338,6 +1381,7 @@ registerAppTool(
|
|
|
1338
1381
|
action: "find_and_click",
|
|
1339
1382
|
error: "Element not found",
|
|
1340
1383
|
croppedImageResourceUri,
|
|
1384
|
+
screenshotResourceUri,
|
|
1341
1385
|
duration
|
|
1342
1386
|
}
|
|
1343
1387
|
);
|
|
@@ -1760,9 +1804,9 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1760
1804
|
server.registerTool(
|
|
1761
1805
|
"exec",
|
|
1762
1806
|
{
|
|
1763
|
-
description: "Execute
|
|
1807
|
+
description: "Execute shell or PowerShell commands in the sandbox",
|
|
1764
1808
|
inputSchema: z.object({
|
|
1765
|
-
language: z.enum(["
|
|
1809
|
+
language: z.enum(["sh", "pwsh"]).default("sh"),
|
|
1766
1810
|
code: z.string().describe("Code to execute"),
|
|
1767
1811
|
timeout: z.number().default(30000).describe("Timeout in ms"),
|
|
1768
1812
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0-test.1",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "sdk.js",
|
|
6
6
|
"types": "sdk.d.ts",
|
|
@@ -47,7 +47,11 @@
|
|
|
47
47
|
"bundle": "node build.mjs",
|
|
48
48
|
"test": "mocha test/*",
|
|
49
49
|
"test:sdk": "vitest run",
|
|
50
|
-
"test:sdk:
|
|
50
|
+
"test:sdk:dev": "vitest run --project dev",
|
|
51
|
+
"test:sdk:staging": "vitest run --project staging",
|
|
52
|
+
"test:sdk:canary": "vitest run --project canary",
|
|
53
|
+
"test:sdk:stable": "vitest run --project stable",
|
|
54
|
+
"test:sdk:windows": "TD_OS=windows vitest run",
|
|
51
55
|
"test:sdk:mac": "TEST_PLATFORM=mac vitest run",
|
|
52
56
|
"test:sdk:linux": "TEST_PLATFORM=linux vitest run",
|
|
53
57
|
"test:sdk:watch": "vitest",
|
package/sdk.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type ClickAction =
|
|
|
13
13
|
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
14
14
|
export type ScrollMethod = "keyboard" | "mouse";
|
|
15
15
|
export type TextMatchMethod = "ai" | "turbo";
|
|
16
|
-
export type ExecLanguage = "
|
|
16
|
+
export type ExecLanguage = "sh" | "pwsh";
|
|
17
17
|
/**
|
|
18
18
|
* Preview mode for live test visualization
|
|
19
19
|
* - "browser": Opens debugger in default browser (default)
|
|
@@ -218,7 +218,7 @@ export type KeyboardKey =
|
|
|
218
218
|
| "optionright";
|
|
219
219
|
|
|
220
220
|
export interface TestDriverOptions {
|
|
221
|
-
/** API endpoint URL (default: 'https://
|
|
221
|
+
/** API endpoint URL (default depends on release channel: latest → 'https://api.testdriver.ai') */
|
|
222
222
|
apiRoot?: string;
|
|
223
223
|
/** Sandbox resolution (default: '1366x768') */
|
|
224
224
|
resolution?: string;
|