testdriverai 7.8.0-test.52 → 7.8.0-test.54
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 +33 -16
- package/examples/scroll-keyboard.test.mjs +1 -1
- package/lib/vitest/hooks.mjs +5 -0
- package/package.json +1 -1
- package/sdk.d.ts +4 -0
- package/sdk.js +9 -1
package/agent/index.js
CHANGED
|
@@ -70,6 +70,7 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
70
70
|
this.sandboxId = flags["sandbox-id"] || null;
|
|
71
71
|
this.sandboxAmi = flags["sandbox-ami"] || null;
|
|
72
72
|
this.sandboxInstance = flags["sandbox-instance"] || null;
|
|
73
|
+
this.e2bTemplateId = flags["e2b-template-id"] || null;
|
|
73
74
|
this.sandboxOs = flags.os || "linux";
|
|
74
75
|
this.ip = flags.ip || null;
|
|
75
76
|
this.workingDir = flags.workingDir || process.cwd();
|
|
@@ -2188,6 +2189,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2188
2189
|
if (this.sandboxInstance) {
|
|
2189
2190
|
sandboxConfig.instanceType = this.sandboxInstance;
|
|
2190
2191
|
}
|
|
2192
|
+
if (this.e2bTemplateId) {
|
|
2193
|
+
sandboxConfig.e2bTemplateId = this.e2bTemplateId;
|
|
2194
|
+
}
|
|
2191
2195
|
// Add keepAlive TTL if specified
|
|
2192
2196
|
if (this.keepAlive !== undefined && this.keepAlive !== null) {
|
|
2193
2197
|
sandboxConfig.keepAlive = this.keepAlive;
|
package/agent/lib/logger.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const useStderr = process.env.TD_STDIO === 'stderr';
|
|
10
|
+
const isDebug = process.env.DEBUG === 'true';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Log a message - uses stdout by default, stderr if TD_STDIO=stderr
|
|
@@ -40,6 +41,19 @@ function warn(...args) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Log a debug message - only outputs when DEBUG=true
|
|
46
|
+
* @param {...any} args - Arguments to log
|
|
47
|
+
*/
|
|
48
|
+
function debug(...args) {
|
|
49
|
+
if (!isDebug) return;
|
|
50
|
+
if (useStderr) {
|
|
51
|
+
console.error(...args);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* Check if logger is configured to use stderr
|
|
45
59
|
* @returns {boolean}
|
|
@@ -50,6 +64,7 @@ function isStderrMode() {
|
|
|
50
64
|
|
|
51
65
|
module.exports = {
|
|
52
66
|
log,
|
|
67
|
+
debug,
|
|
53
68
|
error,
|
|
54
69
|
warn,
|
|
55
70
|
isStderrMode,
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -85,7 +85,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
85
85
|
suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
logger.
|
|
88
|
+
logger.debug(`[realtime] Connecting as sdk-${this._sandboxId}...`);
|
|
89
89
|
|
|
90
90
|
await new Promise(function (resolve, reject) {
|
|
91
91
|
self._ably.connection.on("connected", resolve);
|
|
@@ -99,7 +99,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
99
99
|
|
|
100
100
|
this._sessionChannel = this._ably.channels.get(channelName);
|
|
101
101
|
|
|
102
|
-
logger.
|
|
102
|
+
logger.debug(`[realtime] Channel initialized: ${channelName}`);
|
|
103
103
|
|
|
104
104
|
// Enter presence on the session channel so the API can count connected SDK clients
|
|
105
105
|
try {
|
|
@@ -107,7 +107,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
107
107
|
sandboxId: this._sandboxId,
|
|
108
108
|
connectedAt: Date.now(),
|
|
109
109
|
});
|
|
110
|
-
logger.
|
|
110
|
+
logger.debug(`[realtime] Entered presence on session channel (sandbox=${this._sandboxId})`);
|
|
111
111
|
} catch (e) {
|
|
112
112
|
// Non-fatal — presence is used for concurrency counting, not critical path
|
|
113
113
|
logger.warn("Failed to enter presence on session channel: " + (e.message || e));
|
|
@@ -118,7 +118,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
118
118
|
var message = msg.data;
|
|
119
119
|
if (!message) return;
|
|
120
120
|
|
|
121
|
-
logger.
|
|
121
|
+
logger.debug(`[realtime] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
122
122
|
|
|
123
123
|
if (message.type === "sandbox.progress") {
|
|
124
124
|
emitter.emit(events.sandbox.progress, {
|
|
@@ -212,7 +212,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
212
212
|
var resolveAge = resolveEntry.startTime
|
|
213
213
|
? ((Date.now() - resolveEntry.startTime) / 1000).toFixed(1) + 's'
|
|
214
214
|
: '?';
|
|
215
|
-
logger.
|
|
215
|
+
logger.debug(
|
|
216
216
|
'[realtime] Promise RESOLVED: requestId=' + message.requestId +
|
|
217
217
|
' | type=' + (resolveEntry.message ? resolveEntry.message.type : 'unknown') +
|
|
218
218
|
' | age=' + resolveAge
|
|
@@ -241,7 +241,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
241
241
|
this._onFileMsg = function (msg) {
|
|
242
242
|
var message = msg.data;
|
|
243
243
|
if (!message) return;
|
|
244
|
-
logger.
|
|
244
|
+
logger.debug(`[realtime] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
245
245
|
if (message.requestId && self.ps[message.requestId]) {
|
|
246
246
|
emitter.emit(events.sandbox.received);
|
|
247
247
|
self.ps[message.requestId].resolve(message);
|
|
@@ -260,7 +260,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
260
260
|
const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
|
|
261
261
|
const pendingIds = Object.keys(this.ps);
|
|
262
262
|
const pending = pendingIds.length;
|
|
263
|
-
logger.
|
|
263
|
+
logger.debug(`[realtime][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
|
|
264
264
|
if (pending > 0) {
|
|
265
265
|
const now = Date.now();
|
|
266
266
|
for (const rid of pendingIds) {
|
|
@@ -268,20 +268,20 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
268
268
|
if (!entry) continue;
|
|
269
269
|
const type = entry.message ? entry.message.type : 'unknown';
|
|
270
270
|
const ageSec = ((now - (entry.startTime || now)) / 1000).toFixed(1);
|
|
271
|
-
logger.
|
|
271
|
+
logger.debug(`[realtime][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
}, 10000);
|
|
275
275
|
if (this._statsInterval.unref) this._statsInterval.unref();
|
|
276
276
|
|
|
277
277
|
this._ably.connection.on("disconnected", function () {
|
|
278
|
-
logger.
|
|
278
|
+
logger.debug("[realtime] Connection: disconnected - will auto-reconnect");
|
|
279
279
|
self._disconnectedAt = Date.now();
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
this._ably.connection.on("connected", function () {
|
|
283
283
|
// Log reconnection so the user knows the blip was recovered
|
|
284
|
-
logger.
|
|
284
|
+
logger.debug("[realtime] Connection: reconnected");
|
|
285
285
|
// Extend any pending command timeouts by the disconnection duration so
|
|
286
286
|
// commands whose timer was counting down while the connection was down
|
|
287
287
|
// don't get incorrectly timed out.
|
|
@@ -290,7 +290,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
290
290
|
self._disconnectedAt = null;
|
|
291
291
|
var pendingIds = Object.keys(self.ps);
|
|
292
292
|
if (pendingIds.length > 0) {
|
|
293
|
-
logger.
|
|
293
|
+
logger.debug(
|
|
294
294
|
'[realtime] Extending ' + pendingIds.length + ' pending timeout(s) by ' +
|
|
295
295
|
disconnectionDurationMs + 'ms after disconnection'
|
|
296
296
|
);
|
|
@@ -353,7 +353,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
353
353
|
var entry = subs[i];
|
|
354
354
|
if (!entry.sub) continue;
|
|
355
355
|
try {
|
|
356
|
-
logger.
|
|
356
|
+
logger.debug('[realtime] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
|
|
357
357
|
var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
|
|
358
358
|
var recovered = 0;
|
|
359
359
|
while (page) {
|
|
@@ -363,7 +363,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
363
363
|
recovered++;
|
|
364
364
|
try {
|
|
365
365
|
if (entry.handler) {
|
|
366
|
-
logger.
|
|
366
|
+
logger.debug('[realtime] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
|
|
367
367
|
entry.handler(page.items[j]);
|
|
368
368
|
}
|
|
369
369
|
} catch (replayErr) {
|
|
@@ -373,7 +373,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
373
373
|
page = page.hasNext() ? await page.next() : null;
|
|
374
374
|
}
|
|
375
375
|
totalRecovered += recovered;
|
|
376
|
-
logger.
|
|
376
|
+
logger.debug('[realtime] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
|
|
377
377
|
} catch (err) {
|
|
378
378
|
logger.error('[realtime] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
|
|
379
379
|
}
|
|
@@ -381,7 +381,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
381
381
|
if (totalRecovered > 0) {
|
|
382
382
|
logger.warn('[realtime] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
|
|
383
383
|
} else {
|
|
384
|
-
logger.
|
|
384
|
+
logger.debug('[realtime] Discontinuity recovery: no missed messages found');
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
@@ -489,6 +489,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
489
489
|
body.ci = message.ci;
|
|
490
490
|
if (message.ami) body.ami = message.ami;
|
|
491
491
|
if (message.instanceType) body.instanceType = message.instanceType;
|
|
492
|
+
if (message.e2bTemplateId) body.e2bTemplateId = message.e2bTemplateId;
|
|
492
493
|
if (message.keepAlive !== undefined) body.keepAlive = message.keepAlive;
|
|
493
494
|
}
|
|
494
495
|
|
|
@@ -540,6 +541,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
540
541
|
const noVncPort = reply.runner && reply.runner.noVncPort;
|
|
541
542
|
const runnerVncUrl = reply.runner && reply.runner.vncUrl;
|
|
542
543
|
|
|
544
|
+
// Log image version info (AMI for Windows, E2B template for Linux)
|
|
545
|
+
if (reply.imageVersion) {
|
|
546
|
+
if (isE2B) {
|
|
547
|
+
logger.log('E2B image version: v' + reply.imageVersion + (reply.e2bTemplateId ? ' (template: ' + reply.e2bTemplateId + ')' : ''));
|
|
548
|
+
} else {
|
|
549
|
+
logger.log('AMI image version: v' + reply.imageVersion + (reply.amiId ? ' (ami: ' + reply.amiId + ')' : ''));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
543
553
|
if (!isE2B) {
|
|
544
554
|
logger.log(`Runner claimed — ip=${runnerIp || 'none'}, os=${reply.runner?.os || 'unknown'}, noVncPort=${noVncPort || 'not reported'}, vncUrl=${runnerVncUrl || 'not reported'}`);
|
|
545
555
|
}
|
|
@@ -570,6 +580,13 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
570
580
|
if (data && data.ip && reply.runner) reply.runner.ip = data.ip;
|
|
571
581
|
if (data && data.runnerVersion && reply.runner) reply.runner.version = data.runnerVersion;
|
|
572
582
|
logger.log('Runner agent ready (os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
|
|
583
|
+
// Show upgrade info: if the runner's npm version differs from the baked image version,
|
|
584
|
+
// the runner was upgraded during provisioning.
|
|
585
|
+
var runnerVer = data && data.runnerVersion;
|
|
586
|
+
var imageVer = reply.imageVersion;
|
|
587
|
+
if (runnerVer && imageVer && runnerVer !== imageVer) {
|
|
588
|
+
logger.log('Runner upgraded during provisioning: v' + imageVer + ' \u2192 v' + runnerVer);
|
|
589
|
+
}
|
|
573
590
|
if (data && data.update) {
|
|
574
591
|
var u = data.update;
|
|
575
592
|
if (u.status === 'up-to-date') {
|
|
@@ -1002,7 +1019,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1002
1019
|
}
|
|
1003
1020
|
|
|
1004
1021
|
return channel.publish(eventName, message).then(function () {
|
|
1005
|
-
logger.
|
|
1022
|
+
logger.debug(`[realtime] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
1006
1023
|
});
|
|
1007
1024
|
}
|
|
1008
1025
|
|
|
@@ -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"]);
|
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;
|
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
|
@@ -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: {
|
|
@@ -1498,6 +1498,7 @@ class TestDriverSDK {
|
|
|
1498
1498
|
// Store sandbox configuration options
|
|
1499
1499
|
this.sandboxAmi = options.sandboxAmi || null;
|
|
1500
1500
|
this.sandboxInstance = options.sandboxInstance || null;
|
|
1501
|
+
this.e2bTemplateId = options.e2bTemplateId || null;
|
|
1501
1502
|
|
|
1502
1503
|
// Store reconnect preference from options
|
|
1503
1504
|
this.reconnect =
|
|
@@ -2716,6 +2717,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2716
2717
|
* @param {string} options.ip - Direct IP address to connect to
|
|
2717
2718
|
* @param {string} options.sandboxAmi - AMI to use for the sandbox
|
|
2718
2719
|
* @param {string} options.sandboxInstance - Instance type for the sandbox
|
|
2720
|
+
* @param {string} options.e2bTemplateId - E2B template ID to use when creating the sandbox
|
|
2719
2721
|
* @param {string} options.os - Operating system for the sandbox (windows or linux)
|
|
2720
2722
|
* @param {boolean} options.reuseConnection - Reuse recent connection if available (default: true)
|
|
2721
2723
|
* @returns {Promise<Object>} Sandbox instance details
|
|
@@ -2804,6 +2806,12 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2804
2806
|
} else if (this.sandboxInstance) {
|
|
2805
2807
|
this.agent.sandboxInstance = this.sandboxInstance;
|
|
2806
2808
|
}
|
|
2809
|
+
// Use e2bTemplateId from connectOptions if provided, otherwise fall back to constructor value
|
|
2810
|
+
if (connectOptions.e2bTemplateId !== undefined) {
|
|
2811
|
+
this.agent.e2bTemplateId = connectOptions.e2bTemplateId;
|
|
2812
|
+
} else if (this.e2bTemplateId) {
|
|
2813
|
+
this.agent.e2bTemplateId = this.e2bTemplateId;
|
|
2814
|
+
}
|
|
2807
2815
|
// Use os from connectOptions if provided, otherwise fall back to this.os
|
|
2808
2816
|
if (connectOptions.os !== undefined) {
|
|
2809
2817
|
this.agent.sandboxOs = connectOptions.os;
|