testdriverai 7.8.0-test.60 → 7.8.0-test.61
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/lib/logger.js +1 -1
- package/agent/lib/sandbox.js +29 -12
- package/docs/_scripts/extract-example-urls.js +67 -72
- package/docs/docs.json +2 -1
- package/docs/v7/test-results-json.mdx +258 -0
- package/interfaces/vitest-plugin.mjs +116 -7
- package/lib/vitest/hooks.mjs +55 -0
- package/package.json +1 -1
- package/sdk.js +29 -7
package/agent/lib/logger.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const useStderr = process.env.TD_STDIO === 'stderr';
|
|
10
|
-
const isDebug = process.env.
|
|
10
|
+
const isDebug = process.env.TD_DEBUG === 'true' || process.env.VERBOSE === 'true';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Log a message - uses stdout by default, stderr if TD_STDIO=stderr
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -49,6 +49,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
getPublishCount() {
|
|
53
|
+
return this._publishCount;
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
async _initAbly(ablyToken, channelName) {
|
|
53
57
|
if (this._ably) {
|
|
54
58
|
try {
|
|
@@ -177,7 +181,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
177
181
|
return rid + '(' + (e && e.message ? e.message.type : '?') + ')';
|
|
178
182
|
}).join(', ')
|
|
179
183
|
: 'none';
|
|
180
|
-
logger.
|
|
184
|
+
logger.debug(
|
|
181
185
|
'[realtime] No pending promise for requestId=' + (message.requestId || 'null') +
|
|
182
186
|
' | response type=' + (message.type || 'unknown') +
|
|
183
187
|
' | error=' + (message.error ? (message.errorMessage || 'true') : 'false') +
|
|
@@ -192,7 +196,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
192
196
|
var pendingAge = pendingEntry && pendingEntry.startTime
|
|
193
197
|
? ((Date.now() - pendingEntry.startTime) / 1000).toFixed(1) + 's'
|
|
194
198
|
: '?';
|
|
195
|
-
logger.
|
|
199
|
+
logger.debug(
|
|
196
200
|
'[realtime] Promise REJECTED: requestId=' + message.requestId +
|
|
197
201
|
' | type=' + (pendingMessage ? pendingMessage.type : 'unknown') +
|
|
198
202
|
' | age=' + pendingAge +
|
|
@@ -305,11 +309,11 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
305
309
|
});
|
|
306
310
|
|
|
307
311
|
this._ably.connection.on("suspended", function () {
|
|
308
|
-
logger.
|
|
312
|
+
logger.debug("[realtime] Connection: suspended - connection lost for extended period, will keep retrying");
|
|
309
313
|
});
|
|
310
314
|
|
|
311
315
|
this._ably.connection.on("failed", function () {
|
|
312
|
-
logger.
|
|
316
|
+
logger.debug("[realtime] Connection: failed");
|
|
313
317
|
self.apiSocketConnected = false;
|
|
314
318
|
self.instanceSocketConnected = false;
|
|
315
319
|
emitter.emit(events.error.sandbox, "Realtime connection failed");
|
|
@@ -326,7 +330,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
326
330
|
var reasonMsg = reason ? (reason.message || reason.code || String(reason)) : '';
|
|
327
331
|
|
|
328
332
|
if (current === 'attached' && stateChange.resumed === false && previous === 'attached') {
|
|
329
|
-
logger.
|
|
333
|
+
logger.debug('[realtime] Channel DISCONTINUITY detected (resumed=false)' + (reasonMsg ? ' — ' + reasonMsg : ''));
|
|
330
334
|
emitter.emit(events.sandbox.progress, {
|
|
331
335
|
step: 'discontinuity',
|
|
332
336
|
message: 'Recovering missed messages after connection interruption...',
|
|
@@ -367,7 +371,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
367
371
|
entry.handler(page.items[j]);
|
|
368
372
|
}
|
|
369
373
|
} catch (replayErr) {
|
|
370
|
-
logger.
|
|
374
|
+
logger.debug('[realtime] Error replaying recovered message: ' + (replayErr.message || replayErr));
|
|
371
375
|
}
|
|
372
376
|
}
|
|
373
377
|
page = page.hasNext() ? await page.next() : null;
|
|
@@ -375,11 +379,11 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
375
379
|
totalRecovered += recovered;
|
|
376
380
|
logger.debug('[realtime] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
|
|
377
381
|
} catch (err) {
|
|
378
|
-
logger.
|
|
382
|
+
logger.debug('[realtime] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
|
|
379
383
|
}
|
|
380
384
|
}
|
|
381
385
|
if (totalRecovered > 0) {
|
|
382
|
-
logger.
|
|
386
|
+
logger.debug('[realtime] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
|
|
383
387
|
} else {
|
|
384
388
|
logger.debug('[realtime] Discontinuity recovery: no missed messages found');
|
|
385
389
|
}
|
|
@@ -579,6 +583,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
579
583
|
if (data && data.os && reply.runner) reply.runner.os = data.os;
|
|
580
584
|
if (data && data.ip && reply.runner) reply.runner.ip = data.ip;
|
|
581
585
|
if (data && data.runnerVersion && reply.runner) reply.runner.version = data.runnerVersion;
|
|
586
|
+
// Persist version metadata for test result reporting
|
|
587
|
+
self._runnerVersionBefore = reply.imageVersion || null;
|
|
588
|
+
self._runnerVersionAfter = (data && data.runnerVersion) || reply.imageVersion || null;
|
|
589
|
+
self._wasUpdated = !!(data && data.runnerVersion && reply.imageVersion && data.runnerVersion !== reply.imageVersion);
|
|
582
590
|
logger.log('Runner agent ready (os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
|
|
583
591
|
// Show upgrade info: if the runner's npm version differs from the baked image version,
|
|
584
592
|
// the runner was upgraded during provisioning.
|
|
@@ -679,6 +687,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
679
687
|
url: url,
|
|
680
688
|
vncPort: noVncPort || undefined,
|
|
681
689
|
runner: reply.runner,
|
|
690
|
+
// Extra metadata for test result reporting
|
|
691
|
+
amiId: reply.amiId || null,
|
|
692
|
+
e2bTemplateId: reply.e2bTemplateId || null,
|
|
693
|
+
imageVersion: reply.imageVersion || null,
|
|
694
|
+
runnerVersionBefore: this._runnerVersionBefore || reply.imageVersion || null,
|
|
695
|
+
runnerVersionAfter: this._runnerVersionAfter || reply.runner?.version || null,
|
|
696
|
+
wasUpdated: this._wasUpdated || false,
|
|
697
|
+
vncUrl: url || null,
|
|
698
|
+
channelName: this._channelName || null,
|
|
682
699
|
},
|
|
683
700
|
};
|
|
684
701
|
}
|
|
@@ -1149,7 +1166,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1149
1166
|
// Send end-session control message to runner before disconnecting
|
|
1150
1167
|
if (this._sessionChannel && this._ably?.connection?.state === 'connected') {
|
|
1151
1168
|
try {
|
|
1152
|
-
logger.
|
|
1169
|
+
logger.debug('[realtime] Publishing control: type=end-session');
|
|
1153
1170
|
await this._sessionChannel.publish('control', { type: 'end-session' });
|
|
1154
1171
|
} catch (e) {
|
|
1155
1172
|
// Ignore - best effort
|
|
@@ -1159,7 +1176,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1159
1176
|
// Leave presence on session channel
|
|
1160
1177
|
if (this._sessionChannel) {
|
|
1161
1178
|
try {
|
|
1162
|
-
logger.
|
|
1179
|
+
logger.debug('[realtime] Leaving presence on session channel');
|
|
1163
1180
|
await this._sessionChannel.presence.leave();
|
|
1164
1181
|
} catch (e) {
|
|
1165
1182
|
// ignore - best effort, Ably will auto-leave on disconnect
|
|
@@ -1167,7 +1184,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1167
1184
|
}
|
|
1168
1185
|
|
|
1169
1186
|
try {
|
|
1170
|
-
logger.
|
|
1187
|
+
logger.debug('[realtime] Detaching session channel');
|
|
1171
1188
|
if (this._sessionChannel) {
|
|
1172
1189
|
await this._sessionChannel.detach();
|
|
1173
1190
|
}
|
|
@@ -1177,7 +1194,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1177
1194
|
|
|
1178
1195
|
if (this._ably) {
|
|
1179
1196
|
try {
|
|
1180
|
-
logger.
|
|
1197
|
+
logger.debug('[realtime] Closing Realtime connection');
|
|
1181
1198
|
this._ably.close();
|
|
1182
1199
|
} catch (e) {
|
|
1183
1200
|
/* ignore */
|
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Extract Example URLs from
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and updates
|
|
8
|
-
*
|
|
4
|
+
* Extract Example URLs from Test Result JSON Files
|
|
5
|
+
*
|
|
6
|
+
* Reads per-test-case JSON result files written by the vitest plugin
|
|
7
|
+
* to .testdriver/results/ and updates examples-manifest.json.
|
|
8
|
+
*
|
|
9
9
|
* Usage:
|
|
10
|
-
*
|
|
11
|
-
* node extract-example-urls.js < ci-log.txt
|
|
12
|
-
* node extract-example-urls.js --file=ci-log.txt
|
|
10
|
+
* node extract-example-urls.js --results-dir=.testdriver/results
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
13
|
const fs = require("fs");
|
|
16
14
|
const path = require("path");
|
|
17
|
-
const readline = require("readline");
|
|
18
15
|
|
|
19
16
|
const MANIFEST_PATH = path.join(__dirname, "../_data/examples-manifest.json");
|
|
20
17
|
|
|
21
|
-
// Regex to match TESTDRIVER_EXAMPLE_URL::filename::url (handles optional timestamp prefix from CI logs)
|
|
22
|
-
const URL_PATTERN = /TESTDRIVER_EXAMPLE_URL::([^:]+)::(.+)$/;
|
|
23
|
-
|
|
24
18
|
// Parse command line arguments
|
|
25
19
|
function parseArgs() {
|
|
26
20
|
const args = process.argv.slice(2);
|
|
27
21
|
const options = {
|
|
28
|
-
|
|
22
|
+
resultsDir: null,
|
|
29
23
|
help: false,
|
|
30
24
|
};
|
|
31
25
|
|
|
32
26
|
for (const arg of args) {
|
|
33
27
|
if (arg === "--help" || arg === "-h") {
|
|
34
28
|
options.help = true;
|
|
35
|
-
} else if (arg.startsWith("--
|
|
36
|
-
options.
|
|
29
|
+
} else if (arg.startsWith("--results-dir=")) {
|
|
30
|
+
options.resultsDir = arg.slice(14);
|
|
37
31
|
}
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -62,40 +56,51 @@ function saveManifest(manifest) {
|
|
|
62
56
|
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
// Process
|
|
66
|
-
function
|
|
67
|
-
const match = line.match(URL_PATTERN);
|
|
68
|
-
if (match) {
|
|
69
|
-
const [, filename, url] = match;
|
|
70
|
-
const isNew = !manifest.examples[filename];
|
|
71
|
-
|
|
72
|
-
manifest.examples[filename] = {
|
|
73
|
-
url: url.trim(),
|
|
74
|
-
lastUpdated: new Date().toISOString(),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (isNew) {
|
|
78
|
-
stats.added++;
|
|
79
|
-
} else {
|
|
80
|
-
stats.updated++;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
console.log(`${isNew ? "➕" : "🔄"} ${filename}: ${url}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Process input stream
|
|
88
|
-
async function processInput(inputStream) {
|
|
59
|
+
// Process JSON result files from .testdriver/results/
|
|
60
|
+
function processResultsDir(resultsDir) {
|
|
89
61
|
const manifest = loadManifest();
|
|
90
62
|
const stats = { added: 0, updated: 0 };
|
|
91
63
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
64
|
+
// Look for JSON files under examples/ subdirectories
|
|
65
|
+
const examplesDir = path.join(resultsDir, "examples");
|
|
66
|
+
if (!fs.existsSync(examplesDir)) {
|
|
67
|
+
console.log(`\n⚠️ No examples results found in ${examplesDir}`);
|
|
68
|
+
return stats;
|
|
69
|
+
}
|
|
96
70
|
|
|
97
|
-
|
|
98
|
-
|
|
71
|
+
// Walk example test directories (e.g., examples/assert.test.mjs/)
|
|
72
|
+
const testDirs = fs.readdirSync(examplesDir, { withFileTypes: true });
|
|
73
|
+
for (const entry of testDirs) {
|
|
74
|
+
if (!entry.isDirectory()) continue;
|
|
75
|
+
const testDir = path.join(examplesDir, entry.name);
|
|
76
|
+
const jsonFiles = fs.readdirSync(testDir).filter(f => f.endsWith(".json"));
|
|
77
|
+
|
|
78
|
+
for (const jsonFile of jsonFiles) {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(path.join(testDir, jsonFile), "utf-8");
|
|
81
|
+
const result = JSON.parse(content);
|
|
82
|
+
const testFileName = path.basename(result.test?.file || result.testFile || entry.name);
|
|
83
|
+
const url = result.urls?.testRun || result.testRunLink;
|
|
84
|
+
|
|
85
|
+
if (!url) continue;
|
|
86
|
+
|
|
87
|
+
const isNew = !manifest.examples[testFileName];
|
|
88
|
+
manifest.examples[testFileName] = {
|
|
89
|
+
url: url,
|
|
90
|
+
lastUpdated: result.date || new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (isNew) {
|
|
94
|
+
stats.added++;
|
|
95
|
+
} else {
|
|
96
|
+
stats.updated++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`${isNew ? "➕" : "🔄"} ${testFileName}: ${url}`);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`⚠️ Failed to read ${jsonFile}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
if (stats.added > 0 || stats.updated > 0) {
|
|
@@ -103,7 +108,7 @@ async function processInput(inputStream) {
|
|
|
103
108
|
console.log(`\n✨ Manifest updated: ${stats.added} added, ${stats.updated} updated`);
|
|
104
109
|
console.log(`📂 Written to: ${MANIFEST_PATH}`);
|
|
105
110
|
} else {
|
|
106
|
-
console.log("\n⚠️ No
|
|
111
|
+
console.log("\n⚠️ No example URLs found in result files");
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
return stats;
|
|
@@ -112,28 +117,24 @@ async function processInput(inputStream) {
|
|
|
112
117
|
// Show help
|
|
113
118
|
function showHelp() {
|
|
114
119
|
console.log(`
|
|
115
|
-
Extract Example URLs from
|
|
120
|
+
Extract Example URLs from Test Result JSON Files
|
|
116
121
|
|
|
117
122
|
Usage:
|
|
118
|
-
|
|
119
|
-
node extract-example-urls.js < ci-log.txt
|
|
120
|
-
node extract-example-urls.js --file=ci-log.txt
|
|
123
|
+
node extract-example-urls.js --results-dir=.testdriver/results
|
|
121
124
|
|
|
122
125
|
Options:
|
|
123
|
-
--
|
|
124
|
-
--help, -h
|
|
126
|
+
--results-dir=<path> Path to .testdriver/results directory (required)
|
|
127
|
+
--help, -h Show this help message
|
|
125
128
|
|
|
126
129
|
Description:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Updates docs/_data/examples-manifest.json with the extracted URLs.
|
|
130
|
+
Reads per-test-case JSON result files from .testdriver/results/examples/
|
|
131
|
+
and updates docs/_data/examples-manifest.json with the extracted URLs.
|
|
131
132
|
Existing entries are updated, new entries are added.
|
|
132
133
|
`);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
// Main function
|
|
136
|
-
|
|
137
|
+
function main() {
|
|
137
138
|
const options = parseArgs();
|
|
138
139
|
|
|
139
140
|
if (options.help) {
|
|
@@ -141,25 +142,19 @@ async function main() {
|
|
|
141
142
|
process.exit(0);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (options.file) {
|
|
148
|
-
if (!fs.existsSync(options.file)) {
|
|
149
|
-
console.error(`❌ File not found: ${options.file}`);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
inputStream = fs.createReadStream(options.file);
|
|
153
|
-
} else {
|
|
154
|
-
inputStream = process.stdin;
|
|
145
|
+
if (!options.resultsDir) {
|
|
146
|
+
console.error("❌ --results-dir is required. Example: --results-dir=.testdriver/results");
|
|
147
|
+
process.exit(1);
|
|
155
148
|
}
|
|
156
149
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
console.error(`❌
|
|
150
|
+
console.log("🔍 Reading example URLs from JSON result files...\n");
|
|
151
|
+
|
|
152
|
+
if (!fs.existsSync(options.resultsDir)) {
|
|
153
|
+
console.error(`❌ Results directory not found: ${options.resultsDir}`);
|
|
161
154
|
process.exit(1);
|
|
162
155
|
}
|
|
156
|
+
|
|
157
|
+
processResultsDir(options.resultsDir);
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
main();
|
package/docs/docs.json
CHANGED
|
@@ -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
|
+
```
|
|
@@ -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
|
@@ -649,6 +649,61 @@ export function TestDriver(context, options = {}) {
|
|
|
649
649
|
// Clean up console spies
|
|
650
650
|
cleanupConsoleSpy(currentInstance);
|
|
651
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
|
+
|
|
652
707
|
// Wait for connection to finish if it was initiated
|
|
653
708
|
if (currentInstance.__connectionPromise) {
|
|
654
709
|
await currentInstance.__connectionPromise.catch(() => { }); // Ignore connection errors during cleanup
|
package/package.json
CHANGED
package/sdk.js
CHANGED
|
@@ -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,
|
|
@@ -1617,6 +1618,12 @@ class TestDriverSDK {
|
|
|
1617
1618
|
// Uploaded to S3 at cleanup so they can be displayed alongside dashcam replays.
|
|
1618
1619
|
this._logBuffer = [];
|
|
1619
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
|
+
|
|
1620
1627
|
// Set up event listeners once (they live for the lifetime of the SDK instance)
|
|
1621
1628
|
this._setupLogging();
|
|
1622
1629
|
|
|
@@ -3201,6 +3208,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3201
3208
|
|
|
3202
3209
|
// Track successful findAll interaction (fire-and-forget, don't block)
|
|
3203
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++;
|
|
3204
3216
|
if (sessionId && this.apiClient) {
|
|
3205
3217
|
this.apiClient
|
|
3206
3218
|
.req("interaction/track", {
|
|
@@ -3210,7 +3222,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3210
3222
|
timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
|
|
3211
3223
|
success: true,
|
|
3212
3224
|
input: { count: elements.length },
|
|
3213
|
-
cacheHit:
|
|
3225
|
+
cacheHit: findAllCacheHit,
|
|
3214
3226
|
selector: response.selector,
|
|
3215
3227
|
selectorUsed: !!response.selector,
|
|
3216
3228
|
screenshotUrl: response.screenshotKey ?? null,
|
|
@@ -3256,6 +3268,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3256
3268
|
|
|
3257
3269
|
// No elements found - track interaction (fire-and-forget, don't block)
|
|
3258
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++;
|
|
3259
3276
|
if (sessionId && this.apiClient) {
|
|
3260
3277
|
this.apiClient
|
|
3261
3278
|
.req("interaction/track", {
|
|
@@ -3266,7 +3283,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3266
3283
|
success: false,
|
|
3267
3284
|
error: "No elements found",
|
|
3268
3285
|
input: { count: 0 },
|
|
3269
|
-
cacheHit:
|
|
3286
|
+
cacheHit: noResultCacheHit,
|
|
3270
3287
|
selector: response?.selector,
|
|
3271
3288
|
selectorUsed: !!response?.selector,
|
|
3272
3289
|
screenshotUrl: response?.screenshotKey ?? null,
|
|
@@ -3300,6 +3317,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3300
3317
|
|
|
3301
3318
|
// Track findAll error interaction (fire-and-forget, don't block)
|
|
3302
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;
|
|
3303
3323
|
if (sessionId && this.apiClient) {
|
|
3304
3324
|
this.apiClient
|
|
3305
3325
|
.req("interaction/track", {
|
|
@@ -3851,6 +3871,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3851
3871
|
res.on('end', () => {
|
|
3852
3872
|
try {
|
|
3853
3873
|
const info = JSON.parse(data);
|
|
3874
|
+
// Persist API version for test result metadata
|
|
3875
|
+
this._apiVersion = info.version || null;
|
|
3854
3876
|
const commit = info.commit || 'unknown';
|
|
3855
3877
|
const shortCommit = commit.substring(0, 7);
|
|
3856
3878
|
const commitUrl = commit !== 'unknown'
|