@testdriverai/agent 7.8.0-canary.14 → 7.8.0-canary.16
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 +4 -0
- package/agent/lib/logger.js +15 -0
- package/agent/lib/sandbox.js +114 -64
- package/ai/skills/testdriver-find/SKILL.md +14 -20
- package/docs/_data/examples-manifest.json +46 -46
- package/docs/_scripts/extract-example-urls.js +67 -72
- package/docs/docs.json +2 -1
- package/docs/v7/examples/ai.mdx +1 -1
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/chrome-extension.mdx +1 -1
- package/docs/v7/examples/element-not-found.mdx +1 -1
- package/docs/v7/examples/exec-output.mdx +1 -1
- package/docs/v7/examples/exec-pwsh.mdx +1 -1
- package/docs/v7/examples/focus-window.mdx +1 -1
- package/docs/v7/examples/hover-image.mdx +1 -1
- 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 +1 -1
- package/docs/v7/examples/match-image.mdx +1 -1
- package/docs/v7/examples/press-keys.mdx +1 -1
- package/docs/v7/examples/scroll-keyboard.mdx +1 -1
- package/docs/v7/examples/scroll-until-image.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/docs/v7/examples/windows-installer.mdx +1 -1
- package/docs/v7/find.mdx +14 -20
- package/docs/v7/test-results-json.mdx +258 -0
- package/examples/scroll-keyboard.test.mjs +1 -1
- package/interfaces/vitest-plugin.mjs +116 -7
- package/lib/vitest/hooks.mjs +60 -0
- package/package.json +1 -1
- package/sdk.d.ts +4 -0
- package/sdk.js +41 -11
- package/setup/aws/spawn-runner.sh +44 -13
- package/vitest.config.mjs +2 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Test Result JSON"
|
|
3
|
+
sidebarTitle: "Test Result JSON"
|
|
4
|
+
description: "Per-test JSON result files with metadata, versions, and infrastructure details"
|
|
5
|
+
icon: "file-code"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
TestDriver automatically writes a JSON result file for each test case after it finishes. These files contain comprehensive metadata about the test run, including SDK and runner versions, infrastructure details, interaction statistics, and links to recordings.
|
|
11
|
+
|
|
12
|
+
Result files are written to:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
.testdriver/results/<testFile>/<testName>.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For example, a test file `tests/login.test.mjs` with a test named `"should log in"` produces:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
.testdriver/results/tests/login.test.mjs/should_log_in.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
<Note>
|
|
25
|
+
Test names are sanitized for filesystem use — special characters are replaced with underscores and names are truncated to 200 characters.
|
|
26
|
+
</Note>
|
|
27
|
+
|
|
28
|
+
## Enabling
|
|
29
|
+
|
|
30
|
+
No configuration is required. The JSON files are written automatically by the TestDriver Vitest reporter plugin whenever tests run.
|
|
31
|
+
|
|
32
|
+
## JSON Schema
|
|
33
|
+
|
|
34
|
+
Each result file is organized into logical groups:
|
|
35
|
+
|
|
36
|
+
### `versions`
|
|
37
|
+
|
|
38
|
+
| Field | Type | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `versions.sdk` | `string \| null` | TestDriver SDK version (e.g. `"7.8.0"`) |
|
|
41
|
+
| `versions.vitest` | `string \| null` | Vitest version used to run the test |
|
|
42
|
+
| `versions.api` | `string \| null` | TestDriver API server version |
|
|
43
|
+
| `versions.runnerBefore` | `string \| null` | Runner version at sandbox start |
|
|
44
|
+
| `versions.runnerAfter` | `string \| null` | Runner version after auto-update |
|
|
45
|
+
| `versions.runnerWasUpdated` | `boolean` | Whether the runner was auto-updated during provisioning |
|
|
46
|
+
|
|
47
|
+
### `test`
|
|
48
|
+
|
|
49
|
+
| Field | Type | Description |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `test.file` | `string \| null` | Relative path to the test file |
|
|
52
|
+
| `test.name` | `string \| null` | Name of the test case |
|
|
53
|
+
| `test.suite` | `string \| null` | Name of the parent `describe` block |
|
|
54
|
+
| `test.passed` | `boolean` | Whether the test passed |
|
|
55
|
+
| `test.caseId` | `string \| null` | Database ID for this test case |
|
|
56
|
+
| `test.runId` | `string \| null` | Database ID for the overall test run |
|
|
57
|
+
| `test.error` | `string \| null` | Error message if the test failed |
|
|
58
|
+
| `test.errorStack` | `string \| null` | Error stack trace if the test failed |
|
|
59
|
+
|
|
60
|
+
### `urls`
|
|
61
|
+
|
|
62
|
+
| Field | Type | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `urls.api` | `string \| null` | API root URL used for this test |
|
|
65
|
+
| `urls.console` | `string \| null` | TestDriver console base URL |
|
|
66
|
+
| `urls.vnc` | `string \| null` | VNC URL for the sandbox |
|
|
67
|
+
| `urls.testRun` | `string \| null` | Direct link to this test case in the console |
|
|
68
|
+
|
|
69
|
+
### `replay`
|
|
70
|
+
|
|
71
|
+
The `replay` object contains the recording replay URL and derived embed links. The `gifUrl` and `embedUrl` are generated automatically from the replay URL.
|
|
72
|
+
|
|
73
|
+
| Field | Type | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `replay.url` | `string \| null` | Recording replay URL |
|
|
76
|
+
| `replay.gifUrl` | `string \| null` | Animated GIF thumbnail of the recording |
|
|
77
|
+
| `replay.embedUrl` | `string \| null` | Embeddable replay URL (appends `&embed=true`) |
|
|
78
|
+
| `replay.markdown` | `string \| null` | Ready-to-use Markdown embed with GIF linking to the replay |
|
|
79
|
+
|
|
80
|
+
The `replay.markdown` field produces a clickable GIF badge you can paste directly into PR comments, README files, or issue descriptions:
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
[](https://console.testdriver.ai/replay/abc123?share=xyz)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `date`
|
|
87
|
+
|
|
88
|
+
| Field | Type | Description |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `date` | `string` | ISO 8601 timestamp when the test finished |
|
|
91
|
+
|
|
92
|
+
### `team`
|
|
93
|
+
|
|
94
|
+
| Field | Type | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `team.id` | `string \| null` | Team ID from the sandbox |
|
|
97
|
+
| `team.sessionId` | `string \| null` | SDK session ID |
|
|
98
|
+
|
|
99
|
+
### `infrastructure`
|
|
100
|
+
|
|
101
|
+
| Field | Type | Description |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `infrastructure.sandboxId` | `string \| null` | Sandbox instance ID |
|
|
104
|
+
| `infrastructure.instanceId` | `string \| null` | Instance ID |
|
|
105
|
+
| `infrastructure.os` | `string \| null` | Operating system of the sandbox (`"linux"` or `"windows"`) |
|
|
106
|
+
| `infrastructure.amiId` | `string \| null` | AWS AMI ID used for provisioning |
|
|
107
|
+
| `infrastructure.e2bTemplateId` | `string \| null` | E2B template ID used for provisioning |
|
|
108
|
+
| `infrastructure.imageVersion` | `string \| null` | Sandbox image version |
|
|
109
|
+
|
|
110
|
+
### `realtime`
|
|
111
|
+
|
|
112
|
+
| Field | Type | Description |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `realtime.channel` | `string \| null` | Ably channel name used for communication |
|
|
115
|
+
| `realtime.messageCount` | `number` | Number of messages published to the realtime channel |
|
|
116
|
+
|
|
117
|
+
### `interactions`
|
|
118
|
+
|
|
119
|
+
| Field | Type | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `interactions.total` | `number` | Total number of interactions recorded |
|
|
122
|
+
| `interactions.cached` | `number` | Number of interactions served from cache |
|
|
123
|
+
| `interactions.byType` | `object` | Breakdown of interactions by type (e.g. `find`, `click`, `assert`) |
|
|
124
|
+
|
|
125
|
+
## Example Output
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"sdkVersion": "7.8.0",
|
|
130
|
+
"vitestVersion": "4.0.0",
|
|
131
|
+
"apiVersion": "1.45.0",
|
|
132
|
+
"runnerVersionBefore": "2.1.0",
|
|
133
|
+
"runnerVersionAfter": "2.1.1",
|
|
134
|
+
"wasUpdated": true,
|
|
135
|
+
"apiUrl": "https://api.testdriver.ai",
|
|
136
|
+
"consoleUrl": "https://console.testdriver.ai",
|
|
137
|
+
"testRunLink": "https://console.testdriver.ai/runs/abc123/def456",
|
|
138
|
+
"dashcamUrl": "https://app.dashcam.io/replay/abc123",
|
|
139
|
+
"vncUrl": "wss://sandbox-123.testdriver.ai/vnc",
|
|
140
|
+
"date": "2025-01-15T14:30:00.000Z",
|
|
141
|
+
"team": {
|
|
142
|
+
"id": "team_abc123",
|
|
143
|
+
"sessionId": "sess_xyz789"
|
|
144
|
+
},
|
|
145
|
+
"infrastructure": {
|
|
146
|
+
"sandboxId": "sandbox-123",
|
|
147
|
+
"instanceId": "i-abc123",
|
|
148
|
+
"os": "linux",
|
|
149
|
+
"amiId": "ami-0abc123",
|
|
150
|
+
"e2bTemplateId": null,
|
|
151
|
+
"imageVersion": "v2.1.0"
|
|
152
|
+
},
|
|
153
|
+
"realtime": {
|
|
154
|
+
"channel": "sandbox:sandbox-123",
|
|
155
|
+
"messageCount": 42
|
|
156
|
+
},
|
|
157
|
+
"interactions": {
|
|
158
|
+
"total": 15,
|
|
159
|
+
"cached": 3,
|
|
160
|
+
"byType": {
|
|
161
|
+
"find": 8,
|
|
162
|
+
"click": 5,
|
|
163
|
+
"assert": 2
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Using Result Files in CI
|
|
170
|
+
|
|
171
|
+
Result files are useful for extracting test metadata in CI pipelines without parsing log output.
|
|
172
|
+
|
|
173
|
+
### GitHub Actions Example
|
|
174
|
+
|
|
175
|
+
Use `fromJSON` to parse a result file into a GitHub Actions expression you can reference in subsequent steps:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
- name: Run tests
|
|
179
|
+
run: npx vitest run tests/login.test.mjs
|
|
180
|
+
|
|
181
|
+
- name: Parse result
|
|
182
|
+
id: result
|
|
183
|
+
run: |
|
|
184
|
+
# Read the first JSON result file
|
|
185
|
+
FILE=$(find .testdriver/results -name '*.json' | head -n 1)
|
|
186
|
+
echo "json=$(cat "$FILE")" >> "$GITHUB_OUTPUT"
|
|
187
|
+
|
|
188
|
+
- name: Comment on PR
|
|
189
|
+
if: fromJSON(steps.result.outputs.json).test.passed == false
|
|
190
|
+
uses: actions/github-script@v7
|
|
191
|
+
with:
|
|
192
|
+
script: |
|
|
193
|
+
const result = ${{ steps.result.outputs.json }};
|
|
194
|
+
await github.rest.issues.createComment({
|
|
195
|
+
owner: context.repo.owner,
|
|
196
|
+
repo: context.repo.repo,
|
|
197
|
+
issue_number: context.issue.number,
|
|
198
|
+
body: [
|
|
199
|
+
`❌ **${result.test.name}** failed`,
|
|
200
|
+
``,
|
|
201
|
+
`Error: ${result.test.error}`,
|
|
202
|
+
``,
|
|
203
|
+
result.replay.markdown,
|
|
204
|
+
``,
|
|
205
|
+
`[View full recording](${result.urls.testRun})`
|
|
206
|
+
].join('\n')
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
You can also load all results into a matrix or iterate over them:
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
- name: Run tests
|
|
214
|
+
run: npx vitest run tests/*.test.mjs
|
|
215
|
+
|
|
216
|
+
- name: Collect results
|
|
217
|
+
id: results
|
|
218
|
+
run: |
|
|
219
|
+
# Merge all result files into a JSON array
|
|
220
|
+
echo "json=$(find .testdriver/results -name '*.json' -exec cat {} + | jq -s '.')" >> "$GITHUB_OUTPUT"
|
|
221
|
+
|
|
222
|
+
- name: Summary
|
|
223
|
+
run: |
|
|
224
|
+
echo '## Test Results' >> $GITHUB_STEP_SUMMARY
|
|
225
|
+
RESULTS='${{ steps.results.outputs.json }}'
|
|
226
|
+
echo "$RESULTS" | jq -r '.[] | "| \(.test.name) | \(if .test.passed then "✅" else "❌" end) | \(.urls.testRun) |"' >> $GITHUB_STEP_SUMMARY
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Reading Results Programmatically
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
import fs from "fs";
|
|
233
|
+
import path from "path";
|
|
234
|
+
|
|
235
|
+
const resultsDir = ".testdriver/results";
|
|
236
|
+
|
|
237
|
+
function readResults(dir) {
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const testDir of fs.readdirSync(dir, { recursive: true })) {
|
|
240
|
+
const fullPath = path.join(dir, testDir);
|
|
241
|
+
if (fullPath.endsWith(".json") && fs.statSync(fullPath).isFile()) {
|
|
242
|
+
results.push(JSON.parse(fs.readFileSync(fullPath, "utf-8")));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const results = readResults(resultsDir);
|
|
249
|
+
const passed = results.filter(r => r.test.passed);
|
|
250
|
+
const failed = results.filter(r => !r.test.passed);
|
|
251
|
+
|
|
252
|
+
console.log(`${passed.length} passed, ${failed.length} failed`);
|
|
253
|
+
for (const r of failed) {
|
|
254
|
+
console.log(` FAIL: ${r.test.name} — ${r.test.error}`);
|
|
255
|
+
console.log(` Recording: ${r.urls.testRun}`);
|
|
256
|
+
console.log(` Embed: ${r.replay.markdown}`);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
@@ -16,7 +16,7 @@ describe("Scroll Keyboard Test", () => {
|
|
|
16
16
|
// Navigate to https://www.webhamster.com/
|
|
17
17
|
await testdriver.focusApplication("Google Chrome");
|
|
18
18
|
const urlBar = await testdriver.find(
|
|
19
|
-
"
|
|
19
|
+
"the URL in the omnibox", {zoom: true}
|
|
20
20
|
);
|
|
21
21
|
await urlBar.click();
|
|
22
22
|
await testdriver.pressKeys(["ctrl", "a"]);
|
|
@@ -1218,6 +1218,122 @@ class TestDriverReporter {
|
|
|
1218
1218
|
);
|
|
1219
1219
|
console.log("");
|
|
1220
1220
|
|
|
1221
|
+
// Write per-test-case JSON result file
|
|
1222
|
+
{
|
|
1223
|
+
const testResult = meta.testResult || {};
|
|
1224
|
+
|
|
1225
|
+
// Parse replay URL to extract replayId and shareKey for embed links
|
|
1226
|
+
let replayUrl = dashcamUrl || null;
|
|
1227
|
+
let replayGifUrl = null;
|
|
1228
|
+
let replayEmbedUrl = null;
|
|
1229
|
+
let replayMarkdown = null;
|
|
1230
|
+
const replayMatch = dashcamUrl && dashcamUrl.match(/\/replay\/([a-f0-9]+)\?share=([^&\s]+)/);
|
|
1231
|
+
if (replayMatch) {
|
|
1232
|
+
const [, replayId, shareKey] = replayMatch;
|
|
1233
|
+
const apiRoot = pluginState.apiRoot;
|
|
1234
|
+
replayGifUrl = `${apiRoot}/replay/${replayId}/gif?shareKey=${shareKey}`;
|
|
1235
|
+
replayEmbedUrl = `${consoleUrl}/replay/${replayId}?share=${shareKey}&embed=true`;
|
|
1236
|
+
replayMarkdown = `[](${replayUrl})`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const resultData = {
|
|
1240
|
+
// Versions
|
|
1241
|
+
versions: {
|
|
1242
|
+
sdk: testResult.sdkVersion || null,
|
|
1243
|
+
vitest: resolveVitestVersion() || null,
|
|
1244
|
+
api: testResult.apiVersion || null,
|
|
1245
|
+
runnerBefore: testResult.runnerVersionBefore || null,
|
|
1246
|
+
runnerAfter: testResult.runnerVersionAfter || null,
|
|
1247
|
+
runnerWasUpdated: testResult.wasUpdated || false,
|
|
1248
|
+
},
|
|
1249
|
+
|
|
1250
|
+
// Test info
|
|
1251
|
+
test: {
|
|
1252
|
+
file: testResult.testFile || null,
|
|
1253
|
+
name: testResult.testName || null,
|
|
1254
|
+
suite: testResult.suiteName || null,
|
|
1255
|
+
passed: status === "passed",
|
|
1256
|
+
caseId: testCaseDbId || null,
|
|
1257
|
+
runId: testRunDbId || null,
|
|
1258
|
+
error: errorMessage || testResult.error || null,
|
|
1259
|
+
errorStack: errorStack || testResult.errorStack || null,
|
|
1260
|
+
},
|
|
1261
|
+
|
|
1262
|
+
// URLs
|
|
1263
|
+
urls: {
|
|
1264
|
+
api: testResult.apiUrl || null,
|
|
1265
|
+
console: consoleUrl || null,
|
|
1266
|
+
vnc: testResult.vncUrl || null,
|
|
1267
|
+
testRun: testCaseDbId ? `${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}` : null,
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
// Recording replay
|
|
1271
|
+
replay: {
|
|
1272
|
+
url: replayUrl,
|
|
1273
|
+
gifUrl: replayGifUrl,
|
|
1274
|
+
embedUrl: replayEmbedUrl,
|
|
1275
|
+
markdown: replayMarkdown,
|
|
1276
|
+
},
|
|
1277
|
+
|
|
1278
|
+
// Timing
|
|
1279
|
+
date: testResult.date || new Date().toISOString(),
|
|
1280
|
+
|
|
1281
|
+
// Team & session
|
|
1282
|
+
team: {
|
|
1283
|
+
id: testResult.teamId || null,
|
|
1284
|
+
sessionId: testResult.sessionId || null,
|
|
1285
|
+
},
|
|
1286
|
+
|
|
1287
|
+
// Infrastructure
|
|
1288
|
+
infrastructure: {
|
|
1289
|
+
sandboxId: testResult.sandboxId || null,
|
|
1290
|
+
instanceId: testResult.instanceId || null,
|
|
1291
|
+
os: testResult.os || null,
|
|
1292
|
+
amiId: testResult.amiId || null,
|
|
1293
|
+
e2bTemplateId: testResult.e2bTemplateId || null,
|
|
1294
|
+
imageVersion: testResult.imageVersion || null,
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1297
|
+
// Realtime
|
|
1298
|
+
realtime: {
|
|
1299
|
+
channel: testResult.realtimeChannel || null,
|
|
1300
|
+
messageCount: testResult.realtimeMessageCount || 0,
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
// Interactions
|
|
1304
|
+
interactions: testResult.interactions || { total: 0, cached: 0, byType: {} },
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// Sanitize testName for filesystem use
|
|
1308
|
+
const safeName = (test.name || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_").substring(0, 200);
|
|
1309
|
+
const resultDir = path.join(process.cwd(), ".testdriver", "results", testFile);
|
|
1310
|
+
fs.mkdirSync(resultDir, { recursive: true });
|
|
1311
|
+
|
|
1312
|
+
// Include a stable unique suffix in the filename to avoid collisions
|
|
1313
|
+
// when multiple tests in the same file share the same name.
|
|
1314
|
+
const hashSourceParts = [];
|
|
1315
|
+
if (test.id) {
|
|
1316
|
+
hashSourceParts.push(String(test.id));
|
|
1317
|
+
}
|
|
1318
|
+
if (Array.isArray(test.suitePath)) {
|
|
1319
|
+
hashSourceParts.push(test.suitePath.join(" > "));
|
|
1320
|
+
}
|
|
1321
|
+
if (test.file && (test.file.name || test.file.path)) {
|
|
1322
|
+
hashSourceParts.push(test.file.name || test.file.path);
|
|
1323
|
+
}
|
|
1324
|
+
// Fallback to the test name if no other identifiers are available.
|
|
1325
|
+
if (hashSourceParts.length === 0) {
|
|
1326
|
+
hashSourceParts.push(test.name || "unknown");
|
|
1327
|
+
}
|
|
1328
|
+
const hashSource = hashSourceParts.join(" | ");
|
|
1329
|
+
const uniqueHash = crypto.createHash("sha256").update(hashSource).digest("hex").slice(0, 8);
|
|
1330
|
+
|
|
1331
|
+
fs.writeFileSync(
|
|
1332
|
+
path.join(resultDir, `${safeName}-${uniqueHash}.json`),
|
|
1333
|
+
JSON.stringify(resultData, null, 2),
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1221
1337
|
// If there were retries, list all per-attempt dashcam URLs for debugging
|
|
1222
1338
|
if (hasRetries) {
|
|
1223
1339
|
const validAttempts = dashcamUrls.filter(a => a.url);
|
|
@@ -1229,13 +1345,6 @@ class TestDriverReporter {
|
|
|
1229
1345
|
}
|
|
1230
1346
|
}
|
|
1231
1347
|
|
|
1232
|
-
// Output parseable format for docs generation (examples only)
|
|
1233
|
-
if (testFile.startsWith("examples/")) {
|
|
1234
|
-
const testFileName = path.basename(testFile);
|
|
1235
|
-
console.log(
|
|
1236
|
-
`TESTDRIVER_EXAMPLE_URL::${testFileName}::${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}`,
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
1348
|
} catch (error) {
|
|
1240
1349
|
logger.error("Failed to report test case:", error.message);
|
|
1241
1350
|
}
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -403,6 +403,11 @@ export function TestDriver(context, options = {}) {
|
|
|
403
403
|
config.apiRoot = process.env.TD_API_ROOT;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
// Use TD_E2B_TEMPLATE_ID from environment if not provided in config
|
|
407
|
+
if (!config.e2bTemplateId && process.env.TD_E2B_TEMPLATE_ID) {
|
|
408
|
+
config.e2bTemplateId = process.env.TD_E2B_TEMPLATE_ID;
|
|
409
|
+
}
|
|
410
|
+
|
|
406
411
|
const testdriver = new TestDriverSDK(apiKey, config);
|
|
407
412
|
testdriver.__vitestContext = context.task;
|
|
408
413
|
testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
|
|
@@ -644,6 +649,61 @@ export function TestDriver(context, options = {}) {
|
|
|
644
649
|
// Clean up console spies
|
|
645
650
|
cleanupConsoleSpy(currentInstance);
|
|
646
651
|
|
|
652
|
+
// Build test result metadata for JSON report output
|
|
653
|
+
{
|
|
654
|
+
const sdkPkg = require("../../package.json");
|
|
655
|
+
const inst = currentInstance.getInstance?.() || {};
|
|
656
|
+
const sbx = currentInstance.sandbox || {};
|
|
657
|
+
const apiRoot = currentInstance.config?.TD_API_ROOT || null;
|
|
658
|
+
|
|
659
|
+
context.task.meta.testResult = {
|
|
660
|
+
// Versions
|
|
661
|
+
sdkVersion: sdkPkg.version || null,
|
|
662
|
+
apiVersion: currentInstance._apiVersion || null,
|
|
663
|
+
runnerVersionBefore: inst.runnerVersionBefore || null,
|
|
664
|
+
runnerVersionAfter: inst.runnerVersionAfter || null,
|
|
665
|
+
wasUpdated: inst.wasUpdated || false,
|
|
666
|
+
|
|
667
|
+
// URLs
|
|
668
|
+
apiUrl: apiRoot,
|
|
669
|
+
vncUrl: inst.vncUrl || inst.url || null,
|
|
670
|
+
|
|
671
|
+
// Dates
|
|
672
|
+
date: new Date().toISOString(),
|
|
673
|
+
|
|
674
|
+
// Team / session
|
|
675
|
+
teamId: sbx._teamId || null,
|
|
676
|
+
sessionId: currentInstance.getSessionId?.() || null,
|
|
677
|
+
|
|
678
|
+
// Test info
|
|
679
|
+
testFile: context.task.meta.testFile || null,
|
|
680
|
+
testName: context.task.name || null,
|
|
681
|
+
suiteName: context.task.suite?.name || null,
|
|
682
|
+
|
|
683
|
+
// Test result
|
|
684
|
+
testPassed: context.task.result?.state === "pass",
|
|
685
|
+
error: context.task.result?.errors?.[0]?.message || null,
|
|
686
|
+
errorStack: context.task.result?.errors?.[0]?.stack || null,
|
|
687
|
+
|
|
688
|
+
// Infrastructure
|
|
689
|
+
sandboxId: inst.sandboxId || inst.instanceId || null,
|
|
690
|
+
instanceId: inst.instanceId || null,
|
|
691
|
+
os: currentInstance.os || inst.os || null,
|
|
692
|
+
amiId: inst.amiId || null,
|
|
693
|
+
e2bTemplateId: inst.e2bTemplateId || null,
|
|
694
|
+
imageVersion: inst.imageVersion || null,
|
|
695
|
+
|
|
696
|
+
// Realtime
|
|
697
|
+
realtimeChannel: inst.channelName || sbx._channelName || null,
|
|
698
|
+
realtimeMessageCount: typeof sbx.getPublishCount === "function" ? sbx.getPublishCount() : 0,
|
|
699
|
+
|
|
700
|
+
// Interactions
|
|
701
|
+
interactions: currentInstance._interactionStats
|
|
702
|
+
? { ...currentInstance._interactionStats, byType: { ...currentInstance._interactionStats.byType } }
|
|
703
|
+
: { total: 0, cached: 0, byType: {} },
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
647
707
|
// Wait for connection to finish if it was initiated
|
|
648
708
|
if (currentInstance.__connectionPromise) {
|
|
649
709
|
await currentInstance.__connectionPromise.catch(() => { }); // Ignore connection errors during cleanup
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -273,6 +273,8 @@ export interface TestDriverOptions {
|
|
|
273
273
|
sandboxAmi?: string;
|
|
274
274
|
/** EC2 instance type for sandbox (e.g., 'i3.metal') */
|
|
275
275
|
sandboxInstance?: string;
|
|
276
|
+
/** E2B template ID to use when creating the sandbox (e.g., 'my-template-id') */
|
|
277
|
+
e2bTemplateId?: string;
|
|
276
278
|
/** Cache key for element finding operations. If provided, enables caching tied to this key */
|
|
277
279
|
cacheKey?: string;
|
|
278
280
|
/** Reconnect to the last used sandbox instead of creating a new one. When true, provision methods (chrome, vscode, installer, etc.) will be skipped since the application is already running. Throws error if no previous sandbox exists. */
|
|
@@ -327,6 +329,8 @@ export interface ConnectOptions {
|
|
|
327
329
|
sandboxAmi?: string;
|
|
328
330
|
/** EC2 instance type for sandbox (e.g., 'i3.metal') */
|
|
329
331
|
sandboxInstance?: string;
|
|
332
|
+
/** E2B template ID to use when creating the sandbox (e.g., 'my-template-id') */
|
|
333
|
+
e2bTemplateId?: string;
|
|
330
334
|
/** Operating system for the sandbox (default: 'linux') */
|
|
331
335
|
os?: "windows" | "linux";
|
|
332
336
|
/**
|
package/sdk.js
CHANGED
|
@@ -481,7 +481,7 @@ class Element {
|
|
|
481
481
|
let cacheKey = null;
|
|
482
482
|
let cacheThreshold = null;
|
|
483
483
|
let perCommandThresholds = null; // Per-command { screen, element } override
|
|
484
|
-
let zoom =
|
|
484
|
+
let zoom = true; // Default to enabled
|
|
485
485
|
let perCommandAi = null; // Per-command AI config override
|
|
486
486
|
|
|
487
487
|
let minConfidence = null; // Minimum confidence threshold
|
|
@@ -494,8 +494,8 @@ class Element {
|
|
|
494
494
|
// New: options is an object with cacheKey and/or cacheThreshold
|
|
495
495
|
cacheKey = options.cacheKey || null;
|
|
496
496
|
cacheThreshold = options.cacheThreshold ?? null;
|
|
497
|
-
// zoom defaults to
|
|
498
|
-
zoom = options.zoom
|
|
497
|
+
// zoom defaults to true unless explicitly set to false
|
|
498
|
+
zoom = options.zoom !== false;
|
|
499
499
|
// Minimum confidence threshold: fail find if AI confidence is below this value
|
|
500
500
|
minConfidence = options.confidence ?? null;
|
|
501
501
|
// Element type hint for prompt wrapping
|
|
@@ -568,7 +568,7 @@ class Element {
|
|
|
568
568
|
cacheKey: cacheKey,
|
|
569
569
|
os: this.sdk.os,
|
|
570
570
|
resolution: this.sdk.resolution,
|
|
571
|
-
zoom: zoom,
|
|
571
|
+
zoom: zoom === true ? 1 : zoom === false ? 0 : zoom,
|
|
572
572
|
confidence: minConfidence,
|
|
573
573
|
type: elementType,
|
|
574
574
|
ai: {
|
|
@@ -623,6 +623,11 @@ class Element {
|
|
|
623
623
|
|
|
624
624
|
// Track find interaction once at the end (fire-and-forget, don't block)
|
|
625
625
|
const sessionId = this.sdk.getSessionId();
|
|
626
|
+
const findCacheHit = response?.cacheHit || response?.cache_hit || response?.cached || false;
|
|
627
|
+
// Increment local interaction counters
|
|
628
|
+
this.sdk._interactionStats.total++;
|
|
629
|
+
this.sdk._interactionStats.byType.find = (this.sdk._interactionStats.byType.find || 0) + 1;
|
|
630
|
+
if (findCacheHit) this.sdk._interactionStats.cached++;
|
|
626
631
|
if (sessionId && this.sdk.apiClient) {
|
|
627
632
|
this.sdk.apiClient
|
|
628
633
|
.req("interaction/track", {
|
|
@@ -632,11 +637,7 @@ class Element {
|
|
|
632
637
|
timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
|
|
633
638
|
success: this._found,
|
|
634
639
|
error: findError,
|
|
635
|
-
cacheHit:
|
|
636
|
-
response?.cacheHit ||
|
|
637
|
-
response?.cache_hit ||
|
|
638
|
-
response?.cached ||
|
|
639
|
-
false,
|
|
640
|
+
cacheHit: findCacheHit,
|
|
640
641
|
selector: response?.selector,
|
|
641
642
|
selectorUsed: !!response?.selector,
|
|
642
643
|
confidence: response?.confidence ?? null,
|
|
@@ -1498,6 +1499,7 @@ class TestDriverSDK {
|
|
|
1498
1499
|
// Store sandbox configuration options
|
|
1499
1500
|
this.sandboxAmi = options.sandboxAmi || null;
|
|
1500
1501
|
this.sandboxInstance = options.sandboxInstance || null;
|
|
1502
|
+
this.e2bTemplateId = options.e2bTemplateId || null;
|
|
1501
1503
|
|
|
1502
1504
|
// Store reconnect preference from options
|
|
1503
1505
|
this.reconnect =
|
|
@@ -1616,6 +1618,12 @@ class TestDriverSDK {
|
|
|
1616
1618
|
// Uploaded to S3 at cleanup so they can be displayed alongside dashcam replays.
|
|
1617
1619
|
this._logBuffer = [];
|
|
1618
1620
|
|
|
1621
|
+
// API version discovered by _logEnvironmentInfo()
|
|
1622
|
+
this._apiVersion = null;
|
|
1623
|
+
|
|
1624
|
+
// Local interaction counters — incremented at each interaction/track call site
|
|
1625
|
+
this._interactionStats = { total: 0, cached: 0, byType: {} };
|
|
1626
|
+
|
|
1619
1627
|
// Set up event listeners once (they live for the lifetime of the SDK instance)
|
|
1620
1628
|
this._setupLogging();
|
|
1621
1629
|
|
|
@@ -2716,6 +2724,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2716
2724
|
* @param {string} options.ip - Direct IP address to connect to
|
|
2717
2725
|
* @param {string} options.sandboxAmi - AMI to use for the sandbox
|
|
2718
2726
|
* @param {string} options.sandboxInstance - Instance type for the sandbox
|
|
2727
|
+
* @param {string} options.e2bTemplateId - E2B template ID to use when creating the sandbox
|
|
2719
2728
|
* @param {string} options.os - Operating system for the sandbox (windows or linux)
|
|
2720
2729
|
* @param {boolean} options.reuseConnection - Reuse recent connection if available (default: true)
|
|
2721
2730
|
* @returns {Promise<Object>} Sandbox instance details
|
|
@@ -2804,6 +2813,12 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2804
2813
|
} else if (this.sandboxInstance) {
|
|
2805
2814
|
this.agent.sandboxInstance = this.sandboxInstance;
|
|
2806
2815
|
}
|
|
2816
|
+
// Use e2bTemplateId from connectOptions if provided, otherwise fall back to constructor value
|
|
2817
|
+
if (connectOptions.e2bTemplateId !== undefined) {
|
|
2818
|
+
this.agent.e2bTemplateId = connectOptions.e2bTemplateId;
|
|
2819
|
+
} else if (this.e2bTemplateId) {
|
|
2820
|
+
this.agent.e2bTemplateId = this.e2bTemplateId;
|
|
2821
|
+
}
|
|
2807
2822
|
// Use os from connectOptions if provided, otherwise fall back to this.os
|
|
2808
2823
|
if (connectOptions.os !== undefined) {
|
|
2809
2824
|
this.agent.sandboxOs = connectOptions.os;
|
|
@@ -3193,6 +3208,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3193
3208
|
|
|
3194
3209
|
// Track successful findAll interaction (fire-and-forget, don't block)
|
|
3195
3210
|
const sessionId = this.getSessionId();
|
|
3211
|
+
const findAllCacheHit = response.cached || false;
|
|
3212
|
+
// Increment local interaction counters
|
|
3213
|
+
this._interactionStats.total++;
|
|
3214
|
+
this._interactionStats.byType.findAll = (this._interactionStats.byType.findAll || 0) + 1;
|
|
3215
|
+
if (findAllCacheHit) this._interactionStats.cached++;
|
|
3196
3216
|
if (sessionId && this.apiClient) {
|
|
3197
3217
|
this.apiClient
|
|
3198
3218
|
.req("interaction/track", {
|
|
@@ -3202,7 +3222,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3202
3222
|
timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
|
|
3203
3223
|
success: true,
|
|
3204
3224
|
input: { count: elements.length },
|
|
3205
|
-
cacheHit:
|
|
3225
|
+
cacheHit: findAllCacheHit,
|
|
3206
3226
|
selector: response.selector,
|
|
3207
3227
|
selectorUsed: !!response.selector,
|
|
3208
3228
|
screenshotUrl: response.screenshotKey ?? null,
|
|
@@ -3248,6 +3268,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3248
3268
|
|
|
3249
3269
|
// No elements found - track interaction (fire-and-forget, don't block)
|
|
3250
3270
|
const sessionId = this.getSessionId();
|
|
3271
|
+
const noResultCacheHit = response?.cached || false;
|
|
3272
|
+
// Increment local interaction counters
|
|
3273
|
+
this._interactionStats.total++;
|
|
3274
|
+
this._interactionStats.byType.findAll = (this._interactionStats.byType.findAll || 0) + 1;
|
|
3275
|
+
if (noResultCacheHit) this._interactionStats.cached++;
|
|
3251
3276
|
if (sessionId && this.apiClient) {
|
|
3252
3277
|
this.apiClient
|
|
3253
3278
|
.req("interaction/track", {
|
|
@@ -3258,7 +3283,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3258
3283
|
success: false,
|
|
3259
3284
|
error: "No elements found",
|
|
3260
3285
|
input: { count: 0 },
|
|
3261
|
-
cacheHit:
|
|
3286
|
+
cacheHit: noResultCacheHit,
|
|
3262
3287
|
selector: response?.selector,
|
|
3263
3288
|
selectorUsed: !!response?.selector,
|
|
3264
3289
|
screenshotUrl: response?.screenshotKey ?? null,
|
|
@@ -3292,6 +3317,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3292
3317
|
|
|
3293
3318
|
// Track findAll error interaction (fire-and-forget, don't block)
|
|
3294
3319
|
const sessionId = this.getSessionId();
|
|
3320
|
+
// Increment local interaction counters
|
|
3321
|
+
this._interactionStats.total++;
|
|
3322
|
+
this._interactionStats.byType.findAll = (this._interactionStats.byType.findAll || 0) + 1;
|
|
3295
3323
|
if (sessionId && this.apiClient) {
|
|
3296
3324
|
this.apiClient
|
|
3297
3325
|
.req("interaction/track", {
|
|
@@ -3843,6 +3871,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3843
3871
|
res.on('end', () => {
|
|
3844
3872
|
try {
|
|
3845
3873
|
const info = JSON.parse(data);
|
|
3874
|
+
// Persist API version for test result metadata
|
|
3875
|
+
this._apiVersion = info.version || null;
|
|
3846
3876
|
const commit = info.commit || 'unknown';
|
|
3847
3877
|
const shortCommit = commit.substring(0, 7);
|
|
3848
3878
|
const commitUrl = commit !== 'unknown'
|