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 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;
@@ -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,
@@ -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.log(`[realtime] Connecting as sdk-${this._sandboxId}...`);
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.log(`[realtime] Channel initialized: ${channelName}`);
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.log(`[realtime] Entered presence on session channel (sandbox=${this._sandboxId})`);
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.log(`[realtime] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
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.log(
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.log(`[realtime] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
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.log(`[realtime][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
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.log(`[realtime][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
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.log("[realtime] Connection: disconnected - will auto-reconnect");
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.log("[realtime] Connection: reconnected");
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.log(
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.log('[realtime] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
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.log('[realtime] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
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.log('[realtime] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
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.log('[realtime] Discontinuity recovery: no missed messages found');
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.log(`[realtime] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
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
- "testdriver-sandbox.vercel.app/login, the URL in the omnibox showing the current page", {zoom: true}
19
+ "the URL in the omnibox", {zoom: true}
20
20
  );
21
21
  await urlBar.click();
22
22
  await testdriver.pressKeys(["ctrl", "a"]);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.8.0-test.52",
3
+ "version": "7.8.0-test.54",
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",
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;