@testdriverai/agent 7.8.0-canary.15 → 7.8.0-canary.18
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 +73 -39
- package/ai/skills/testdriver-find/SKILL.md +14 -20
- package/docs/_scripts/extract-example-urls.js +67 -72
- package/docs/docs.json +2 -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/examples/scroll.test.mjs +1 -12
- 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/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.TD_DEBUG === 'true' || process.env.VERBOSE === '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
|
@@ -29,7 +29,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
29
29
|
this._lastConnectParams = null;
|
|
30
30
|
this._teamId = null;
|
|
31
31
|
this._sandboxId = null;
|
|
32
|
-
this._disconnectedAt = null; // tracks when
|
|
32
|
+
this._disconnectedAt = null; // tracks when Realtime connection dropped (for timeout extension on reconnect)
|
|
33
33
|
|
|
34
34
|
// Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
|
|
35
35
|
this._publishLastTime = 0;
|
|
@@ -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 {
|
|
@@ -85,21 +89,21 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
85
89
|
suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
|
|
86
90
|
});
|
|
87
91
|
|
|
88
|
-
logger.
|
|
92
|
+
logger.debug(`[realtime] Connecting as sdk-${this._sandboxId}...`);
|
|
89
93
|
|
|
90
94
|
await new Promise(function (resolve, reject) {
|
|
91
95
|
self._ably.connection.on("connected", resolve);
|
|
92
96
|
self._ably.connection.on("failed", function () {
|
|
93
|
-
reject(new Error("
|
|
97
|
+
reject(new Error("Realtime connection failed"));
|
|
94
98
|
});
|
|
95
99
|
setTimeout(function () {
|
|
96
|
-
reject(new Error("
|
|
100
|
+
reject(new Error("Realtime connection timeout"));
|
|
97
101
|
}, 30000);
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
this._sessionChannel = this._ably.channels.get(channelName);
|
|
101
105
|
|
|
102
|
-
logger.
|
|
106
|
+
logger.debug(`[realtime] Channel initialized: ${channelName}`);
|
|
103
107
|
|
|
104
108
|
// Enter presence on the session channel so the API can count connected SDK clients
|
|
105
109
|
try {
|
|
@@ -107,7 +111,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
107
111
|
sandboxId: this._sandboxId,
|
|
108
112
|
connectedAt: Date.now(),
|
|
109
113
|
});
|
|
110
|
-
logger.
|
|
114
|
+
logger.debug(`[realtime] Entered presence on session channel (sandbox=${this._sandboxId})`);
|
|
111
115
|
} catch (e) {
|
|
112
116
|
// Non-fatal — presence is used for concurrency counting, not critical path
|
|
113
117
|
logger.warn("Failed to enter presence on session channel: " + (e.message || e));
|
|
@@ -118,7 +122,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
118
122
|
var message = msg.data;
|
|
119
123
|
if (!message) return;
|
|
120
124
|
|
|
121
|
-
logger.
|
|
125
|
+
logger.debug(`[realtime] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
122
126
|
|
|
123
127
|
if (message.type === "sandbox.progress") {
|
|
124
128
|
emitter.emit(events.sandbox.progress, {
|
|
@@ -177,8 +181,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
177
181
|
return rid + '(' + (e && e.message ? e.message.type : '?') + ')';
|
|
178
182
|
}).join(', ')
|
|
179
183
|
: 'none';
|
|
180
|
-
logger.
|
|
181
|
-
'[
|
|
184
|
+
logger.debug(
|
|
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') +
|
|
184
188
|
' | currently pending: [' + pendingSummary + ']'
|
|
@@ -192,8 +196,8 @@ 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.
|
|
196
|
-
'[
|
|
199
|
+
logger.debug(
|
|
200
|
+
'[realtime] Promise REJECTED: requestId=' + message.requestId +
|
|
197
201
|
' | type=' + (pendingMessage ? pendingMessage.type : 'unknown') +
|
|
198
202
|
' | age=' + pendingAge +
|
|
199
203
|
' | error=' + (message.errorMessage || 'Sandbox error')
|
|
@@ -212,8 +216,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
212
216
|
var resolveAge = resolveEntry.startTime
|
|
213
217
|
? ((Date.now() - resolveEntry.startTime) / 1000).toFixed(1) + 's'
|
|
214
218
|
: '?';
|
|
215
|
-
logger.
|
|
216
|
-
'[
|
|
219
|
+
logger.debug(
|
|
220
|
+
'[realtime] Promise RESOLVED: requestId=' + message.requestId +
|
|
217
221
|
' | type=' + (resolveEntry.message ? resolveEntry.message.type : 'unknown') +
|
|
218
222
|
' | age=' + resolveAge
|
|
219
223
|
);
|
|
@@ -241,7 +245,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
241
245
|
this._onFileMsg = function (msg) {
|
|
242
246
|
var message = msg.data;
|
|
243
247
|
if (!message) return;
|
|
244
|
-
logger.
|
|
248
|
+
logger.debug(`[realtime] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
245
249
|
if (message.requestId && self.ps[message.requestId]) {
|
|
246
250
|
emitter.emit(events.sandbox.received);
|
|
247
251
|
self.ps[message.requestId].resolve(message);
|
|
@@ -260,7 +264,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
260
264
|
const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
|
|
261
265
|
const pendingIds = Object.keys(this.ps);
|
|
262
266
|
const pending = pendingIds.length;
|
|
263
|
-
logger.
|
|
267
|
+
logger.debug(`[realtime][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
|
|
264
268
|
if (pending > 0) {
|
|
265
269
|
const now = Date.now();
|
|
266
270
|
for (const rid of pendingIds) {
|
|
@@ -268,20 +272,20 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
268
272
|
if (!entry) continue;
|
|
269
273
|
const type = entry.message ? entry.message.type : 'unknown';
|
|
270
274
|
const ageSec = ((now - (entry.startTime || now)) / 1000).toFixed(1);
|
|
271
|
-
logger.
|
|
275
|
+
logger.debug(`[realtime][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
|
|
272
276
|
}
|
|
273
277
|
}
|
|
274
278
|
}, 10000);
|
|
275
279
|
if (this._statsInterval.unref) this._statsInterval.unref();
|
|
276
280
|
|
|
277
281
|
this._ably.connection.on("disconnected", function () {
|
|
278
|
-
logger.
|
|
282
|
+
logger.debug("[realtime] Connection: disconnected - will auto-reconnect");
|
|
279
283
|
self._disconnectedAt = Date.now();
|
|
280
284
|
});
|
|
281
285
|
|
|
282
286
|
this._ably.connection.on("connected", function () {
|
|
283
287
|
// Log reconnection so the user knows the blip was recovered
|
|
284
|
-
logger.
|
|
288
|
+
logger.debug("[realtime] Connection: reconnected");
|
|
285
289
|
// Extend any pending command timeouts by the disconnection duration so
|
|
286
290
|
// commands whose timer was counting down while the connection was down
|
|
287
291
|
// don't get incorrectly timed out.
|
|
@@ -290,8 +294,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
290
294
|
self._disconnectedAt = null;
|
|
291
295
|
var pendingIds = Object.keys(self.ps);
|
|
292
296
|
if (pendingIds.length > 0) {
|
|
293
|
-
logger.
|
|
294
|
-
'[
|
|
297
|
+
logger.debug(
|
|
298
|
+
'[realtime] Extending ' + pendingIds.length + ' pending timeout(s) by ' +
|
|
295
299
|
disconnectionDurationMs + 'ms after disconnection'
|
|
296
300
|
);
|
|
297
301
|
for (var i = 0; i < pendingIds.length; i++) {
|
|
@@ -305,14 +309,14 @@ 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
|
-
emitter.emit(events.error.sandbox, "
|
|
319
|
+
emitter.emit(events.error.sandbox, "Realtime connection failed");
|
|
316
320
|
});
|
|
317
321
|
|
|
318
322
|
// ─── Channel discontinuity detection ──────────────────────────────
|
|
@@ -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...',
|
|
@@ -353,7 +357,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
353
357
|
var entry = subs[i];
|
|
354
358
|
if (!entry.sub) continue;
|
|
355
359
|
try {
|
|
356
|
-
logger.
|
|
360
|
+
logger.debug('[realtime] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
|
|
357
361
|
var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
|
|
358
362
|
var recovered = 0;
|
|
359
363
|
while (page) {
|
|
@@ -363,25 +367,25 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
363
367
|
recovered++;
|
|
364
368
|
try {
|
|
365
369
|
if (entry.handler) {
|
|
366
|
-
logger.
|
|
370
|
+
logger.debug('[realtime] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
|
|
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;
|
|
374
378
|
}
|
|
375
379
|
totalRecovered += recovered;
|
|
376
|
-
logger.
|
|
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
|
-
logger.
|
|
388
|
+
logger.debug('[realtime] Discontinuity recovery: no missed messages found');
|
|
385
389
|
}
|
|
386
390
|
}
|
|
387
391
|
|
|
@@ -489,6 +493,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
489
493
|
body.ci = message.ci;
|
|
490
494
|
if (message.ami) body.ami = message.ami;
|
|
491
495
|
if (message.instanceType) body.instanceType = message.instanceType;
|
|
496
|
+
if (message.e2bTemplateId) body.e2bTemplateId = message.e2bTemplateId;
|
|
492
497
|
if (message.keepAlive !== undefined) body.keepAlive = message.keepAlive;
|
|
493
498
|
}
|
|
494
499
|
|
|
@@ -540,6 +545,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
540
545
|
const noVncPort = reply.runner && reply.runner.noVncPort;
|
|
541
546
|
const runnerVncUrl = reply.runner && reply.runner.vncUrl;
|
|
542
547
|
|
|
548
|
+
// Log image version info (AMI for Windows, E2B template for Linux)
|
|
549
|
+
if (reply.imageVersion) {
|
|
550
|
+
if (isE2B) {
|
|
551
|
+
logger.log('E2B image version: v' + reply.imageVersion + (reply.e2bTemplateId ? ' (template: ' + reply.e2bTemplateId + ')' : ''));
|
|
552
|
+
} else {
|
|
553
|
+
logger.log('AMI image version: v' + reply.imageVersion + (reply.amiId ? ' (ami: ' + reply.amiId + ')' : ''));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
543
557
|
if (!isE2B) {
|
|
544
558
|
logger.log(`Runner claimed — ip=${runnerIp || 'none'}, os=${reply.runner?.os || 'unknown'}, noVncPort=${noVncPort || 'not reported'}, vncUrl=${runnerVncUrl || 'not reported'}`);
|
|
545
559
|
}
|
|
@@ -569,7 +583,18 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
569
583
|
if (data && data.os && reply.runner) reply.runner.os = data.os;
|
|
570
584
|
if (data && data.ip && reply.runner) reply.runner.ip = data.ip;
|
|
571
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);
|
|
572
590
|
logger.log('Runner agent ready (os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
|
|
591
|
+
// Show upgrade info: if the runner's npm version differs from the baked image version,
|
|
592
|
+
// the runner was upgraded during provisioning.
|
|
593
|
+
var runnerVer = data && data.runnerVersion;
|
|
594
|
+
var imageVer = reply.imageVersion;
|
|
595
|
+
if (runnerVer && imageVer && runnerVer !== imageVer) {
|
|
596
|
+
logger.log('Runner upgraded during provisioning: v' + imageVer + ' \u2192 v' + runnerVer);
|
|
597
|
+
}
|
|
573
598
|
if (data && data.update) {
|
|
574
599
|
var u = data.update;
|
|
575
600
|
if (u.status === 'up-to-date') {
|
|
@@ -662,6 +687,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
662
687
|
url: url,
|
|
663
688
|
vncPort: noVncPort || undefined,
|
|
664
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,
|
|
665
699
|
},
|
|
666
700
|
};
|
|
667
701
|
}
|
|
@@ -809,7 +843,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
809
843
|
function onFailed() {
|
|
810
844
|
clearTimeout(timer);
|
|
811
845
|
self._ably.connection.off("connected", onConnected);
|
|
812
|
-
reject(new Error("
|
|
846
|
+
reject(new Error("Realtime connection failed while waiting to send"));
|
|
813
847
|
}
|
|
814
848
|
self._ably.connection.once("connected", onConnected);
|
|
815
849
|
self._ably.connection.once("failed", onFailed);
|
|
@@ -881,7 +915,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
881
915
|
return rid + '(' + (e && e.message ? e.message.type : '?') + ', ' + age + ')';
|
|
882
916
|
}).join(', ');
|
|
883
917
|
logger.error(
|
|
884
|
-
'[
|
|
918
|
+
'[realtime] Promise TIMEOUT: requestId=' + requestId +
|
|
885
919
|
' | type=' + message.type +
|
|
886
920
|
' | timeout=' + timeout + 'ms' +
|
|
887
921
|
' | all pending: [' + pendingSummary + ']'
|
|
@@ -930,7 +964,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
930
964
|
timeoutId = setTimeout(timeoutFn, newRemaining);
|
|
931
965
|
if (timeoutId.unref) timeoutId.unref();
|
|
932
966
|
logger.log(
|
|
933
|
-
'[
|
|
967
|
+
'[realtime] Extended timeout for requestId=' + requestId +
|
|
934
968
|
' by ' + disconnectionDurationMs + 'ms (new remaining: ' + Math.round(newRemaining / 1000) + 's)'
|
|
935
969
|
);
|
|
936
970
|
},
|
|
@@ -1002,7 +1036,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1002
1036
|
}
|
|
1003
1037
|
|
|
1004
1038
|
return channel.publish(eventName, message).then(function () {
|
|
1005
|
-
logger.
|
|
1039
|
+
logger.debug(`[realtime] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
1006
1040
|
});
|
|
1007
1041
|
}
|
|
1008
1042
|
|
|
@@ -1132,7 +1166,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1132
1166
|
// Send end-session control message to runner before disconnecting
|
|
1133
1167
|
if (this._sessionChannel && this._ably?.connection?.state === 'connected') {
|
|
1134
1168
|
try {
|
|
1135
|
-
logger.
|
|
1169
|
+
logger.debug('[realtime] Publishing control: type=end-session');
|
|
1136
1170
|
await this._sessionChannel.publish('control', { type: 'end-session' });
|
|
1137
1171
|
} catch (e) {
|
|
1138
1172
|
// Ignore - best effort
|
|
@@ -1142,7 +1176,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1142
1176
|
// Leave presence on session channel
|
|
1143
1177
|
if (this._sessionChannel) {
|
|
1144
1178
|
try {
|
|
1145
|
-
logger.
|
|
1179
|
+
logger.debug('[realtime] Leaving presence on session channel');
|
|
1146
1180
|
await this._sessionChannel.presence.leave();
|
|
1147
1181
|
} catch (e) {
|
|
1148
1182
|
// ignore - best effort, Ably will auto-leave on disconnect
|
|
@@ -1150,7 +1184,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1150
1184
|
}
|
|
1151
1185
|
|
|
1152
1186
|
try {
|
|
1153
|
-
logger.
|
|
1187
|
+
logger.debug('[realtime] Detaching session channel');
|
|
1154
1188
|
if (this._sessionChannel) {
|
|
1155
1189
|
await this._sessionChannel.detach();
|
|
1156
1190
|
}
|
|
@@ -1160,7 +1194,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1160
1194
|
|
|
1161
1195
|
if (this._ably) {
|
|
1162
1196
|
try {
|
|
1163
|
-
logger.
|
|
1197
|
+
logger.debug('[realtime] Closing Realtime connection');
|
|
1164
1198
|
this._ably.close();
|
|
1165
1199
|
} catch (e) {
|
|
1166
1200
|
/* ignore */
|
|
@@ -49,8 +49,8 @@ const element = await testdriver.find(description, options)
|
|
|
49
49
|
- `"any"` — No wrapping, uses the description as-is (default behavior)
|
|
50
50
|
</ParamField>
|
|
51
51
|
|
|
52
|
-
<ParamField path="zoom" type="boolean" default={
|
|
53
|
-
|
|
52
|
+
<ParamField path="zoom" type="boolean" default={true}>
|
|
53
|
+
Two-phase zoom mode for better precision in crowded UIs with many similar elements. Enabled by default.
|
|
54
54
|
</ParamField>
|
|
55
55
|
|
|
56
56
|
<ParamField path="ai" type="object">
|
|
@@ -332,14 +332,19 @@ The `timeout` option:
|
|
|
332
332
|
- Returns the element (check `element.found()` if not throwing on failure)
|
|
333
333
|
- Set to `0` to disable polling and make a single attempt
|
|
334
334
|
|
|
335
|
-
## Zoom Mode
|
|
335
|
+
## Zoom Mode
|
|
336
336
|
|
|
337
|
-
|
|
337
|
+
Zoom mode is **enabled by default**. It uses a two-phase approach for better precision when locating elements, especially in crowded UIs with many similar elements.
|
|
338
|
+
|
|
339
|
+
To disable zoom for a specific find call, pass `zoom: false`:
|
|
338
340
|
|
|
339
341
|
```javascript
|
|
340
|
-
//
|
|
341
|
-
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar'
|
|
342
|
+
// Zoom is on by default — no option needed
|
|
343
|
+
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar');
|
|
342
344
|
await extensionsBtn.click();
|
|
345
|
+
|
|
346
|
+
// Disable zoom for a specific call if needed
|
|
347
|
+
const largeButton = await testdriver.find('big hero button', { zoom: false });
|
|
343
348
|
```
|
|
344
349
|
|
|
345
350
|
### How Zoom Mode Works
|
|
@@ -352,22 +357,11 @@ await extensionsBtn.click();
|
|
|
352
357
|
This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
|
|
353
358
|
|
|
354
359
|
<Tip>
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
- Targeting elements in dense UI areas
|
|
359
|
-
- The default locate is clicking the wrong similar element
|
|
360
|
-
- You get an AI verification rejection like "The crosshair is located in the empty space of the browser's tab bar/title bar area" — this means the initial locate was imprecise and zoom will help the AI pinpoint the correct element
|
|
360
|
+
You may want to disable zoom with `zoom: false` when:
|
|
361
|
+
- Targeting large, isolated elements where the extra precision isn't needed
|
|
362
|
+
- You want to speed up find calls in simple UIs
|
|
361
363
|
</Tip>
|
|
362
364
|
|
|
363
|
-
```javascript
|
|
364
|
-
// Without zoom - may click wrong icon in toolbar
|
|
365
|
-
const icon = await testdriver.find('settings icon');
|
|
366
|
-
|
|
367
|
-
// With zoom - better precision for crowded areas
|
|
368
|
-
const icon = await testdriver.find('settings icon', { zoom: true });
|
|
369
|
-
```
|
|
370
|
-
|
|
371
365
|
## Cache Options
|
|
372
366
|
|
|
373
367
|
Control caching behavior to optimize performance, especially when using dynamic variables in prompts.
|