testdriverai 7.2.78 → 7.2.80
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/agent/index.js +42 -1
- package/agent/lib/debugger-server.js +6 -0
- package/ai/agents/testdriver.md +72 -5
- package/interfaces/cli/commands/init.js +47 -479
- package/lib/captcha/solver.js +64 -2
- package/lib/init-project.js +426 -0
- package/mcp-server/dist/server.mjs +118 -3
- package/package.json +3 -2
- package/sdk.js +12 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize a TestDriver project with all necessary files and configuration
|
|
7
|
+
* @param {Object} options - Initialization options
|
|
8
|
+
* @param {string} [options.targetDir] - Target directory (defaults to current working directory)
|
|
9
|
+
* @param {string} [options.apiKey] - TestDriver API key (will be saved to .env)
|
|
10
|
+
* @param {boolean} [options.skipInstall=false] - Skip npm install step
|
|
11
|
+
* @param {boolean} [options.interactive=false] - Whether to prompt for missing values (CLI mode)
|
|
12
|
+
* @returns {Promise<{success: boolean, results: string[], errors: string[]}>}
|
|
13
|
+
*/
|
|
14
|
+
async function initProject(options = {}) {
|
|
15
|
+
const targetDir = options.targetDir || process.cwd();
|
|
16
|
+
const results = [];
|
|
17
|
+
const errors = [];
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Create target directory if it doesn't exist
|
|
21
|
+
if (!fs.existsSync(targetDir)) {
|
|
22
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
23
|
+
results.push(`✓ Created directory: ${targetDir}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 1. Setup package.json
|
|
27
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
28
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
29
|
+
const packageJson = {
|
|
30
|
+
name: path.basename(targetDir),
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
description: "TestDriver.ai test suite",
|
|
33
|
+
type: "module",
|
|
34
|
+
scripts: {
|
|
35
|
+
test: "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:ui": "vitest --ui",
|
|
38
|
+
},
|
|
39
|
+
keywords: ["testdriver", "testing", "e2e"],
|
|
40
|
+
author: "",
|
|
41
|
+
license: "ISC",
|
|
42
|
+
engines: {
|
|
43
|
+
node: ">=20.19.0",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
47
|
+
results.push("✓ Created package.json");
|
|
48
|
+
} else {
|
|
49
|
+
results.push("⊘ package.json already exists, skipping");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Create test directory and example files
|
|
53
|
+
const testDir = path.join(targetDir, "tests");
|
|
54
|
+
if (!fs.existsSync(testDir)) {
|
|
55
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create login snippet file
|
|
59
|
+
const loginSnippetFile = path.join(testDir, "login.js");
|
|
60
|
+
if (!fs.existsSync(loginSnippetFile)) {
|
|
61
|
+
const loginSnippetContent = `/**
|
|
62
|
+
* Login snippet - reusable login function
|
|
63
|
+
*
|
|
64
|
+
* This demonstrates how to create reusable test snippets that can be
|
|
65
|
+
* imported and used across multiple test files.
|
|
66
|
+
*/
|
|
67
|
+
export async function login(testdriver) {
|
|
68
|
+
|
|
69
|
+
// The password is displayed on screen, have TestDriver extract it
|
|
70
|
+
const password = await testdriver.extract('the password');
|
|
71
|
+
|
|
72
|
+
// Find the username field
|
|
73
|
+
const usernameField = await testdriver.find(
|
|
74
|
+
'username input'
|
|
75
|
+
);
|
|
76
|
+
await usernameField.click();
|
|
77
|
+
|
|
78
|
+
// Type username
|
|
79
|
+
await testdriver.type('standard_user');
|
|
80
|
+
|
|
81
|
+
// Enter password form earlier
|
|
82
|
+
// Marked as secret so it's not logged or stored
|
|
83
|
+
await testdriver.pressKeys(['tab']);
|
|
84
|
+
await testdriver.type(password, { secret: true });
|
|
85
|
+
|
|
86
|
+
// Submit the form
|
|
87
|
+
await testdriver.find('submit button on the login form').click();
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
fs.writeFileSync(loginSnippetFile, loginSnippetContent);
|
|
91
|
+
results.push("✓ Created login snippet: tests/login.js");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create example test file
|
|
95
|
+
const testFile = path.join(testDir, "example.test.js");
|
|
96
|
+
if (!fs.existsSync(testFile)) {
|
|
97
|
+
const vitestContent = `import { test, expect } from 'vitest';
|
|
98
|
+
import { TestDriver } from 'testdriverai/vitest/hooks';
|
|
99
|
+
import { login } from './login.js';
|
|
100
|
+
|
|
101
|
+
test('should login and add item to cart', async (context) => {
|
|
102
|
+
|
|
103
|
+
// Create TestDriver instance - automatically connects to sandbox
|
|
104
|
+
const testdriver = TestDriver(context);
|
|
105
|
+
|
|
106
|
+
// Launch chrome and navigate to demo app
|
|
107
|
+
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
|
|
108
|
+
|
|
109
|
+
// Use the login snippet to handle authentication
|
|
110
|
+
// This demonstrates how to reuse test logic across multiple tests
|
|
111
|
+
await login(testdriver);
|
|
112
|
+
|
|
113
|
+
// Add item to cart
|
|
114
|
+
const addToCartButton = await testdriver.find(
|
|
115
|
+
'add to cart button under TestDriver Hat'
|
|
116
|
+
);
|
|
117
|
+
await addToCartButton.click();
|
|
118
|
+
|
|
119
|
+
// Open cart
|
|
120
|
+
const cartButton = await testdriver.find(
|
|
121
|
+
'cart button in the top right corner'
|
|
122
|
+
);
|
|
123
|
+
await cartButton.click();
|
|
124
|
+
|
|
125
|
+
// Verify item in cart
|
|
126
|
+
const result = await testdriver.assert('There is an item in the cart');
|
|
127
|
+
expect(result).toBeTruthy();
|
|
128
|
+
|
|
129
|
+
});
|
|
130
|
+
`;
|
|
131
|
+
fs.writeFileSync(testFile, vitestContent);
|
|
132
|
+
results.push("✓ Created test file: tests/example.test.js");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Create vitest.config.js
|
|
136
|
+
const configFile = path.join(targetDir, "vitest.config.js");
|
|
137
|
+
if (!fs.existsSync(configFile)) {
|
|
138
|
+
const configContent = `import { defineConfig } from 'vitest/config';
|
|
139
|
+
import TestDriver from 'testdriverai/vitest';
|
|
140
|
+
|
|
141
|
+
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
142
|
+
export default defineConfig({
|
|
143
|
+
test: {
|
|
144
|
+
testTimeout: 300000,
|
|
145
|
+
hookTimeout: 300000,
|
|
146
|
+
reporters: [
|
|
147
|
+
'default',
|
|
148
|
+
TestDriver(),
|
|
149
|
+
],
|
|
150
|
+
setupFiles: ['testdriverai/vitest/setup'],
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
`;
|
|
154
|
+
fs.writeFileSync(configFile, configContent);
|
|
155
|
+
results.push("✓ Created vitest.config.js");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 4. Create/update .gitignore
|
|
159
|
+
const gitignorePath = path.join(targetDir, ".gitignore");
|
|
160
|
+
let gitignoreContent = "";
|
|
161
|
+
if (fs.existsSync(gitignorePath)) {
|
|
162
|
+
gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
|
|
163
|
+
if (!gitignoreContent.includes(".env")) {
|
|
164
|
+
const ignoresToAdd = [
|
|
165
|
+
"",
|
|
166
|
+
"# TestDriver.ai",
|
|
167
|
+
".env",
|
|
168
|
+
"node_modules/",
|
|
169
|
+
"test-results/",
|
|
170
|
+
"*.log",
|
|
171
|
+
];
|
|
172
|
+
const newContent = gitignoreContent.trim() + "\n" + ignoresToAdd.join("\n") + "\n";
|
|
173
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
174
|
+
results.push("✓ Updated .gitignore");
|
|
175
|
+
} else {
|
|
176
|
+
results.push("⊘ .env already in .gitignore");
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const ignoresToAdd = [
|
|
180
|
+
"# TestDriver.ai",
|
|
181
|
+
".env",
|
|
182
|
+
"node_modules/",
|
|
183
|
+
"test-results/",
|
|
184
|
+
"*.log",
|
|
185
|
+
];
|
|
186
|
+
fs.writeFileSync(gitignorePath, ignoresToAdd.join("\n") + "\n");
|
|
187
|
+
results.push("✓ Created .gitignore");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 5. Create GitHub Actions workflow
|
|
191
|
+
const workflowDir = path.join(targetDir, ".github", "workflows");
|
|
192
|
+
if (!fs.existsSync(workflowDir)) {
|
|
193
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const workflowFile = path.join(workflowDir, "testdriver.yml");
|
|
197
|
+
if (!fs.existsSync(workflowFile)) {
|
|
198
|
+
const workflowContent = `name: TestDriver.ai Tests
|
|
199
|
+
|
|
200
|
+
on:
|
|
201
|
+
push:
|
|
202
|
+
branches: [ main, master ]
|
|
203
|
+
pull_request:
|
|
204
|
+
branches: [ main, master ]
|
|
205
|
+
|
|
206
|
+
jobs:
|
|
207
|
+
test:
|
|
208
|
+
runs-on: ubuntu-latest
|
|
209
|
+
|
|
210
|
+
steps:
|
|
211
|
+
- uses: actions/checkout@v4
|
|
212
|
+
|
|
213
|
+
- name: Setup Node.js
|
|
214
|
+
uses: actions/setup-node@v4
|
|
215
|
+
with:
|
|
216
|
+
node-version: '20'
|
|
217
|
+
cache: 'npm'
|
|
218
|
+
|
|
219
|
+
- name: Install dependencies
|
|
220
|
+
run: npm ci
|
|
221
|
+
|
|
222
|
+
- name: Run TestDriver.ai tests
|
|
223
|
+
env:
|
|
224
|
+
TD_API_KEY: \${{ secrets.TD_API_KEY }}
|
|
225
|
+
run: npx vitest run
|
|
226
|
+
|
|
227
|
+
- name: Upload test results
|
|
228
|
+
if: always()
|
|
229
|
+
uses: actions/upload-artifact@v4
|
|
230
|
+
with:
|
|
231
|
+
name: test-results
|
|
232
|
+
path: test-results/
|
|
233
|
+
retention-days: 30
|
|
234
|
+
`;
|
|
235
|
+
fs.writeFileSync(workflowFile, workflowContent);
|
|
236
|
+
results.push("✓ Created GitHub workflow: .github/workflows/testdriver.yml");
|
|
237
|
+
} else {
|
|
238
|
+
results.push("⊘ GitHub workflow already exists");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 6. Create VSCode MCP config
|
|
242
|
+
const vscodeDir = path.join(targetDir, ".vscode");
|
|
243
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
244
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const mcpConfigFile = path.join(vscodeDir, "mcp.json");
|
|
248
|
+
if (!fs.existsSync(mcpConfigFile)) {
|
|
249
|
+
const mcpConfig = {
|
|
250
|
+
inputs: [
|
|
251
|
+
{
|
|
252
|
+
type: "promptString",
|
|
253
|
+
id: "testdriver-api-key",
|
|
254
|
+
description: "TestDriver API Key From https://console.testdriver.ai/team",
|
|
255
|
+
password: true,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
servers: {
|
|
259
|
+
testdriver: {
|
|
260
|
+
command: "npx",
|
|
261
|
+
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
262
|
+
env: {
|
|
263
|
+
TD_API_KEY: "${input:testdriver-api-key}",
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
269
|
+
results.push("✓ Created MCP config: .vscode/mcp.json");
|
|
270
|
+
} else {
|
|
271
|
+
results.push("⊘ MCP config already exists");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 7. Create VSCode extensions recommendations
|
|
275
|
+
const extensionsFile = path.join(vscodeDir, "extensions.json");
|
|
276
|
+
if (!fs.existsSync(extensionsFile)) {
|
|
277
|
+
const extensionsConfig = {
|
|
278
|
+
recommendations: [
|
|
279
|
+
"vitest.explorer",
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
fs.writeFileSync(extensionsFile, JSON.stringify(extensionsConfig, null, 2) + "\n");
|
|
283
|
+
results.push("✓ Created extensions config: .vscode/extensions.json");
|
|
284
|
+
} else {
|
|
285
|
+
results.push("⊘ Extensions config already exists");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 8. Copy TestDriver skills
|
|
289
|
+
const skillsDestDir = path.join(targetDir, ".github", "skills");
|
|
290
|
+
const possibleSkillsSources = [
|
|
291
|
+
path.join(targetDir, "node_modules", "testdriverai", "ai", "skills"),
|
|
292
|
+
path.join(__dirname, "..", "ai", "skills"),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
let skillsSourceDir = null;
|
|
296
|
+
for (const source of possibleSkillsSources) {
|
|
297
|
+
if (fs.existsSync(source)) {
|
|
298
|
+
skillsSourceDir = source;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (skillsSourceDir) {
|
|
304
|
+
if (!fs.existsSync(skillsDestDir)) {
|
|
305
|
+
fs.mkdirSync(skillsDestDir, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const skillDirs = fs.readdirSync(skillsSourceDir);
|
|
309
|
+
let copiedCount = 0;
|
|
310
|
+
|
|
311
|
+
for (const skillDir of skillDirs) {
|
|
312
|
+
const sourcePath = path.join(skillsSourceDir, skillDir);
|
|
313
|
+
const destPath = path.join(skillsDestDir, skillDir);
|
|
314
|
+
|
|
315
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
316
|
+
if (!fs.existsSync(destPath)) {
|
|
317
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const skillFile = path.join(sourcePath, "SKILL.md");
|
|
321
|
+
if (fs.existsSync(skillFile)) {
|
|
322
|
+
fs.copyFileSync(skillFile, path.join(destPath, "SKILL.md"));
|
|
323
|
+
copiedCount++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (copiedCount > 0) {
|
|
329
|
+
results.push(`✓ Copied ${copiedCount} skills to .github/skills/`);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
results.push("⚠ Skills directory not found (will be available after npm install)");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 9. Copy TestDriver agents
|
|
336
|
+
const agentsDestDir = path.join(targetDir, ".github", "agents");
|
|
337
|
+
const possibleAgentsSources = [
|
|
338
|
+
path.join(targetDir, "node_modules", "testdriverai", "ai", "agents"),
|
|
339
|
+
path.join(__dirname, "..", "ai", "agents"),
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
let agentsSourceDir = null;
|
|
343
|
+
for (const source of possibleAgentsSources) {
|
|
344
|
+
if (fs.existsSync(source)) {
|
|
345
|
+
agentsSourceDir = source;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (agentsSourceDir) {
|
|
351
|
+
if (!fs.existsSync(agentsDestDir)) {
|
|
352
|
+
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
356
|
+
let copiedCount = 0;
|
|
357
|
+
|
|
358
|
+
for (const agentFile of agentFiles) {
|
|
359
|
+
const sourcePath = path.join(agentsSourceDir, agentFile);
|
|
360
|
+
const agentName = agentFile.replace(".md", "");
|
|
361
|
+
const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
|
|
362
|
+
|
|
363
|
+
if (!fs.existsSync(destPath)) {
|
|
364
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
365
|
+
copiedCount++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (copiedCount > 0) {
|
|
370
|
+
results.push(`✓ Copied ${copiedCount} agent(s) to .github/agents/`);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
results.push("⚠ Agents directory not found (will be available after npm install)");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 10. Handle API key if provided
|
|
377
|
+
if (options.apiKey) {
|
|
378
|
+
const envPath = path.join(targetDir, ".env");
|
|
379
|
+
let envContent = "";
|
|
380
|
+
|
|
381
|
+
if (fs.existsSync(envPath)) {
|
|
382
|
+
envContent = fs.readFileSync(envPath, "utf8");
|
|
383
|
+
if (!envContent.includes("TD_API_KEY=")) {
|
|
384
|
+
envContent += "\n";
|
|
385
|
+
} else {
|
|
386
|
+
// Replace existing key
|
|
387
|
+
envContent = envContent.replace(/^TD_API_KEY=.*$/m, "");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const newEnvContent = envContent.trim() + `\nTD_API_KEY=${options.apiKey}\n`;
|
|
392
|
+
fs.writeFileSync(envPath, newEnvContent);
|
|
393
|
+
results.push("✓ Saved API key to .env");
|
|
394
|
+
} else {
|
|
395
|
+
results.push("ℹ No API key provided - add it to .env manually:");
|
|
396
|
+
results.push(" TD_API_KEY=your_api_key");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 11. Install dependencies (unless skipped)
|
|
400
|
+
if (!options.skipInstall) {
|
|
401
|
+
results.push("\n📦 Installing dependencies...");
|
|
402
|
+
try {
|
|
403
|
+
execSync("npm install -D vitest testdriverai@beta && npm install dotenv", {
|
|
404
|
+
cwd: targetDir,
|
|
405
|
+
stdio: "pipe",
|
|
406
|
+
});
|
|
407
|
+
results.push("✓ Dependencies installed successfully");
|
|
408
|
+
} catch (error) {
|
|
409
|
+
errors.push("Failed to install dependencies. Run manually:");
|
|
410
|
+
errors.push(" npm install -D vitest testdriverai@beta");
|
|
411
|
+
errors.push(" npm install dotenv");
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
results.push("\nℹ Skipped dependency installation. Run manually:");
|
|
415
|
+
results.push(" npm install -D vitest testdriverai@beta");
|
|
416
|
+
results.push(" npm install dotenv");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { success: true, results, errors };
|
|
420
|
+
} catch (error) {
|
|
421
|
+
errors.push(`Initialization failed: ${error.message}`);
|
|
422
|
+
return { success: false, results, errors };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = { initProject };
|
|
@@ -14,7 +14,7 @@ import * as Sentry from "@sentry/node";
|
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as os from "os";
|
|
16
16
|
import * as path from "path";
|
|
17
|
-
import { fileURLToPath } from "url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { generateActionCode } from "./codegen.js";
|
|
20
20
|
import { getProvisionOptions, SessionStartInputSchema } from "./provision-types.js";
|
|
@@ -406,7 +406,26 @@ Debug mode (connect to existing sandbox):
|
|
|
406
406
|
logger.debug("session_start: Preview mode", { preview: previewMode });
|
|
407
407
|
// Get IP from params or environment (for self-hosted instances)
|
|
408
408
|
const instanceIp = params.ip || process.env.TD_IP;
|
|
409
|
-
|
|
409
|
+
// Get API key - check multiple sources for GitHub Copilot coding agent compatibility
|
|
410
|
+
// 1. TD_API_KEY (standard environment variable)
|
|
411
|
+
// 2. COPILOT_MCP_TD_API_KEY (fallback for GitHub Copilot coding agent)
|
|
412
|
+
const apiKey = process.env.TD_API_KEY || process.env.COPILOT_MCP_TD_API_KEY || "";
|
|
413
|
+
if (!apiKey) {
|
|
414
|
+
logger.error("session_start: No API key found", {
|
|
415
|
+
hasTD_API_KEY: !!process.env.TD_API_KEY,
|
|
416
|
+
hasCOPILOT_MCP_TD_API_KEY: !!process.env.COPILOT_MCP_TD_API_KEY,
|
|
417
|
+
availableEnvVars: Object.keys(process.env).filter(k => k.includes('TD') || k.includes('COPILOT_MCP'))
|
|
418
|
+
});
|
|
419
|
+
return createToolResult(false, "No API key found. Please set TD_API_KEY or COPILOT_MCP_TD_API_KEY environment variable.", {
|
|
420
|
+
error: "Missing API key",
|
|
421
|
+
hint: "For GitHub Copilot coding agent, create a Copilot environment secret named COPILOT_MCP_TD_API_KEY"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
logger.debug("session_start: API key found", {
|
|
425
|
+
source: process.env.TD_API_KEY ? "TD_API_KEY" : "COPILOT_MCP_TD_API_KEY",
|
|
426
|
+
keyPrefix: apiKey.substring(0, 7) + "..."
|
|
427
|
+
});
|
|
428
|
+
sdk = new TestDriverSDK(apiKey, {
|
|
410
429
|
os: params.os,
|
|
411
430
|
logging: false,
|
|
412
431
|
apiRoot,
|
|
@@ -992,7 +1011,29 @@ registerAppTool(server, "find_and_click", {
|
|
|
992
1011
|
const found = element.found();
|
|
993
1012
|
if (!found) {
|
|
994
1013
|
logger.warn("find_and_click: Element not found", { description: params.description });
|
|
995
|
-
|
|
1014
|
+
// Capture screenshot to show current state even when element not found
|
|
1015
|
+
const rawResponse = element._response || {};
|
|
1016
|
+
const duration = Date.now() - startTime;
|
|
1017
|
+
// Store cropped image (screenshot) for resource serving
|
|
1018
|
+
let croppedImageResourceUri;
|
|
1019
|
+
const croppedImage = rawResponse.croppedImage;
|
|
1020
|
+
if (croppedImage) {
|
|
1021
|
+
const imageData = croppedImage.startsWith('data:')
|
|
1022
|
+
? croppedImage.replace(/^data:image\/\w+;base64,/, '')
|
|
1023
|
+
: croppedImage;
|
|
1024
|
+
croppedImageResourceUri = storeImage(imageData, "screenshot");
|
|
1025
|
+
delete rawResponse.croppedImage;
|
|
1026
|
+
}
|
|
1027
|
+
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
1028
|
+
delete rawResponse.extractedText;
|
|
1029
|
+
delete rawResponse.pixelDiffImage;
|
|
1030
|
+
return createToolResult(false, `Element not found: "${params.description}"`, {
|
|
1031
|
+
...rawResponse,
|
|
1032
|
+
action: "find_and_click",
|
|
1033
|
+
error: "Element not found",
|
|
1034
|
+
croppedImageResourceUri,
|
|
1035
|
+
duration
|
|
1036
|
+
});
|
|
996
1037
|
}
|
|
997
1038
|
const coords = element.getCoordinates();
|
|
998
1039
|
// Store element ref for later use (in case user wants to interact again)
|
|
@@ -1587,6 +1628,80 @@ Only use 'screenshot' when you explicitly want to show something to the human us
|
|
|
1587
1628
|
return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
|
|
1588
1629
|
}
|
|
1589
1630
|
});
|
|
1631
|
+
// Init - Initialize a new TestDriver project
|
|
1632
|
+
server.registerTool("init", {
|
|
1633
|
+
description: `Initialize a new TestDriver project with Vitest SDK examples.
|
|
1634
|
+
|
|
1635
|
+
This creates:
|
|
1636
|
+
- package.json with proper dependencies
|
|
1637
|
+
- Example test files (tests/example.test.js, tests/login.js)
|
|
1638
|
+
- vitest.config.js
|
|
1639
|
+
- .gitignore
|
|
1640
|
+
- GitHub Actions workflow (.github/workflows/testdriver.yml)
|
|
1641
|
+
- VSCode MCP config (.vscode/mcp.json)
|
|
1642
|
+
- VSCode extensions recommendations (.vscode/extensions.json)
|
|
1643
|
+
- TestDriver skills (.github/skills/)
|
|
1644
|
+
- TestDriver agents (.github/agents/)
|
|
1645
|
+
- .env file with API key (if provided)
|
|
1646
|
+
|
|
1647
|
+
API Key: The apiKey parameter is optional. If not provided, you'll need to manually add TD_API_KEY to the .env file after initialization. The project structure will still be created successfully.`,
|
|
1648
|
+
inputSchema: z.object({
|
|
1649
|
+
directory: z.string().optional().describe("Target directory (defaults to current working directory)"),
|
|
1650
|
+
apiKey: z.string().optional().describe("TestDriver API key (will be saved to .env)"),
|
|
1651
|
+
skipInstall: z.boolean().default(false).describe("Skip npm install step"),
|
|
1652
|
+
}),
|
|
1653
|
+
}, async (params) => {
|
|
1654
|
+
const startTime = Date.now();
|
|
1655
|
+
const targetDir = params.directory ? path.resolve(params.directory) : process.cwd();
|
|
1656
|
+
logger.info("init: Starting", { targetDir, hasApiKey: !!params.apiKey, skipInstall: params.skipInstall });
|
|
1657
|
+
try {
|
|
1658
|
+
// Import the shared init logic (dynamic import for ESM/CJS compatibility)
|
|
1659
|
+
const initProjectPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "lib", "init-project.js");
|
|
1660
|
+
const { initProject } = await import(pathToFileURL(initProjectPath).href);
|
|
1661
|
+
// Run the shared init logic
|
|
1662
|
+
const result = await initProject({
|
|
1663
|
+
targetDir,
|
|
1664
|
+
apiKey: params.apiKey,
|
|
1665
|
+
skipInstall: params.skipInstall,
|
|
1666
|
+
});
|
|
1667
|
+
const duration = Date.now() - startTime;
|
|
1668
|
+
logger.info("init: Completed", { targetDir, duration, success: result.success });
|
|
1669
|
+
const nextSteps = `
|
|
1670
|
+
|
|
1671
|
+
📚 Next steps:
|
|
1672
|
+
|
|
1673
|
+
1. Run your tests:
|
|
1674
|
+
npx vitest run
|
|
1675
|
+
|
|
1676
|
+
2. Use AI agents to write tests:
|
|
1677
|
+
Open VSCode/Cursor and use @testdriver agent
|
|
1678
|
+
|
|
1679
|
+
3. MCP server configured:
|
|
1680
|
+
TestDriver tools available via MCP in .vscode/mcp.json
|
|
1681
|
+
|
|
1682
|
+
4. For CI/CD, add TD_API_KEY to your GitHub repository secrets:
|
|
1683
|
+
Settings → Secrets → Actions → New repository secret
|
|
1684
|
+
|
|
1685
|
+
Learn more at https://docs.testdriver.ai/v7/getting-started/
|
|
1686
|
+
`;
|
|
1687
|
+
const allMessages = [...result.results, ...result.errors.map((e) => `⚠️ ${e}`)];
|
|
1688
|
+
return createToolResult(result.success, result.success
|
|
1689
|
+
? `✅ TestDriver project initialized successfully!\n\n${allMessages.join("\n")}${nextSteps}`
|
|
1690
|
+
: `⚠️ TestDriver project initialization completed with errors:\n\n${allMessages.join("\n")}`, {
|
|
1691
|
+
action: "init",
|
|
1692
|
+
targetDir,
|
|
1693
|
+
filesCreated: result.results.length,
|
|
1694
|
+
hasApiKey: !!params.apiKey,
|
|
1695
|
+
errors: result.errors,
|
|
1696
|
+
duration
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
logger.error("init: Failed", { error: String(error), targetDir });
|
|
1701
|
+
captureException(error, { tags: { tool: "init" }, extra: { targetDir } });
|
|
1702
|
+
throw error;
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1590
1705
|
// Start the server
|
|
1591
1706
|
async function main() {
|
|
1592
1707
|
logger.info("Starting TestDriver MCP Server", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.80",
|
|
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",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"serve-report": "npx http-server . -p 8080 -o report.html",
|
|
63
63
|
"report": "npm run generate-report && npm run serve-report",
|
|
64
64
|
"generate:skills": "node scripts/generate-skills.js",
|
|
65
|
-
"
|
|
65
|
+
"build:mcp": "cd mcp-server && npm ci && npm run build",
|
|
66
|
+
"prepublishOnly": "npm run build:mcp"
|
|
66
67
|
},
|
|
67
68
|
"author": "",
|
|
68
69
|
"license": "ISC",
|
package/sdk.js
CHANGED
|
@@ -1279,12 +1279,24 @@ class TestDriverSDK {
|
|
|
1279
1279
|
// Use provided API key or fall back to environment variable
|
|
1280
1280
|
const resolvedApiKey = apiKey || process.env.TD_API_KEY;
|
|
1281
1281
|
|
|
1282
|
+
// Handle preview mode with backwards compatibility for headless option
|
|
1283
|
+
// Preview can be "browser" (default), "ide", or "none" (headless)
|
|
1284
|
+
let previewMode = options.preview || process.env.TD_PREVIEW;
|
|
1285
|
+
|
|
1286
|
+
// Backwards compatibility: headless: true maps to preview: "none"
|
|
1287
|
+
if (options.headless === true && !options.preview) {
|
|
1288
|
+
previewMode = "none";
|
|
1289
|
+
} else if (!previewMode) {
|
|
1290
|
+
previewMode = "browser"; // default
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1282
1293
|
// Set up environment with API key
|
|
1283
1294
|
const environment = {
|
|
1284
1295
|
TD_API_KEY: resolvedApiKey,
|
|
1285
1296
|
TD_API_ROOT: options.apiRoot || "https://testdriver-api.onrender.com",
|
|
1286
1297
|
TD_RESOLUTION: options.resolution || "1366x768",
|
|
1287
1298
|
TD_ANALYTICS: options.analytics !== false,
|
|
1299
|
+
TD_PREVIEW: previewMode,
|
|
1288
1300
|
...options.environment,
|
|
1289
1301
|
};
|
|
1290
1302
|
|