testdriverai 7.2.62 → 7.2.64

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [7.2.64](https://github.com/testdriverai/testdriverai/compare/v7.2.63...v7.2.64) (2026-02-02)
2
+
3
+
4
+
5
+ ## [7.2.63](https://github.com/testdriverai/testdriverai/compare/v7.2.62...v7.2.63) (2026-01-30)
6
+
7
+
8
+
1
9
  ## [7.2.62](https://github.com/testdriverai/testdriverai/compare/v7.2.61...v7.2.62) (2026-01-30)
2
10
 
3
11
 
package/agent/index.js CHANGED
@@ -110,8 +110,8 @@ class TestDriverAgent extends EventEmitter2 {
110
110
  this.sandbox = createSandbox(this.emitter, this.analytics, this.session);
111
111
 
112
112
  // Attach Sentry log listeners to capture CLI logs as breadcrumbs
113
- const sentry = require("../lib/sentry");
114
- sentry.attachLogListeners(this.emitter);
113
+ const sentry = require("../lib/sentry");
114
+ sentry.attachLogListeners(this.emitter);
115
115
 
116
116
  // Set the OS for the sandbox to use
117
117
  this.sandbox.os = this.sandboxOs;
@@ -191,7 +191,14 @@ class TestDriverAgent extends EventEmitter2 {
191
191
  // allows us to save the current state, run lifecycle hooks, and track analytics
192
192
  async exit(failed = true, shouldSave = false, shouldRunPostrun = false) {
193
193
  const { formatter } = require("../sdk-log-formatter.js");
194
- this.emitter.emit(events.log.narration, formatter.getPrefix("disconnect") + " " + theme.yellow.bold("Exiting") + theme.dim("..."), true);
194
+ this.emitter.emit(
195
+ events.log.narration,
196
+ formatter.getPrefix("disconnect") +
197
+ " " +
198
+ theme.yellow.bold("Exiting") +
199
+ theme.dim("..."),
200
+ true,
201
+ );
195
202
 
196
203
  // Clean up redraw interval
197
204
  if (this.redraw && this.redraw.cleanup) {
@@ -240,9 +247,7 @@ class TestDriverAgent extends EventEmitter2 {
240
247
  if (errorContext) {
241
248
  this.emitter.emit(events.error.fatal, errorContext);
242
249
  } else {
243
- this.emitter.emit(
244
- events.error.fatal,error,
245
- );
250
+ this.emitter.emit(events.error.fatal, error);
246
251
  }
247
252
 
248
253
  if (skipPostrun) {
@@ -436,17 +441,15 @@ class TestDriverAgent extends EventEmitter2 {
436
441
  let mousePosition = await this.system.getMousePosition();
437
442
  let activeWindow = await this.system.activeWin();
438
443
 
439
- let response = await this.sdk.req(
440
- "check",
441
- {
442
- tasks: this.tasks,
443
- images,
444
- mousePosition,
445
- activeWindow,
446
- }
447
- );
444
+ let response = await this.sdk.req("check", {
445
+ tasks: this.tasks,
446
+ images,
447
+ mousePosition,
448
+ activeWindow,
449
+ });
448
450
 
449
- this.emitter.emit(events.log.markdown.static, response.data);
451
+ // Use log.log (not markdown.static) so output goes through console spy to sandbox
452
+ this.emitter.emit(events.log.log, response.data);
450
453
 
451
454
  this.lastScreenshot = thisScreenshot;
452
455
 
@@ -877,7 +880,7 @@ commands:
877
880
  currentTask,
878
881
  dry = false,
879
882
  validateAndLoop = false,
880
- shouldSave = true
883
+ shouldSave = true,
881
884
  ) {
882
885
  // Check if execution has been stopped
883
886
  if (this.stopped) {
@@ -900,15 +903,12 @@ commands:
900
903
 
901
904
  this.lastScreenshot = await this.system.captureScreenBase64();
902
905
 
903
- let message = await this.sdk.req(
904
- "input",
905
- {
906
- input: currentTask,
907
- mousePosition: await this.system.getMousePosition(),
908
- activeWindow: await this.system.activeWin(),
909
- image: this.lastScreenshot,
910
- }
911
- );
906
+ let message = await this.sdk.req("input", {
907
+ input: currentTask,
908
+ mousePosition: await this.system.getMousePosition(),
909
+ activeWindow: await this.system.activeWin(),
910
+ image: this.lastScreenshot,
911
+ });
912
912
 
913
913
  this.emitter.emit(events.log.log, message.data);
914
914
 
@@ -1625,8 +1625,8 @@ ${regression}
1625
1625
 
1626
1626
  // Returns the path to the last sandbox file
1627
1627
  getLastSandboxFilePath() {
1628
- const testdriverDir = path.join(process.cwd(), '.testdriver');
1629
- return path.join(testdriverDir, 'last-sandbox');
1628
+ const testdriverDir = path.join(process.cwd(), ".testdriver");
1629
+ return path.join(testdriverDir, "last-sandbox");
1630
1630
  }
1631
1631
 
1632
1632
  // Returns full sandbox info from last-sandbox file (no timeout - let API validate)
@@ -1647,7 +1647,7 @@ ${regression}
1647
1647
 
1648
1648
  return {
1649
1649
  sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
1650
- os: sandboxInfo.os || 'linux',
1650
+ os: sandboxInfo.os || "linux",
1651
1651
  ami: sandboxInfo.ami || null,
1652
1652
  instanceType: sandboxInfo.instanceType || null,
1653
1653
  timestamp: sandboxInfo.timestamp || null,
@@ -1662,7 +1662,7 @@ ${regression}
1662
1662
  // Returns sandboxId to use if AMI/instance type match current requirements
1663
1663
  getRecentSandboxId() {
1664
1664
  const sandboxInfo = this.getLastSandboxId();
1665
-
1665
+
1666
1666
  if (!sandboxInfo || !sandboxInfo.sandboxId) {
1667
1667
  return null;
1668
1668
  }
@@ -1689,13 +1689,13 @@ ${regression}
1689
1689
  saveLastSandboxId(sandboxId, osType = "linux") {
1690
1690
  const lastSandboxFile = this.getLastSandboxFilePath();
1691
1691
  const testdriverDir = path.dirname(lastSandboxFile);
1692
-
1692
+
1693
1693
  try {
1694
1694
  // Ensure .testdriver directory exists
1695
1695
  if (!fs.existsSync(testdriverDir)) {
1696
1696
  fs.mkdirSync(testdriverDir, { recursive: true });
1697
1697
  }
1698
-
1698
+
1699
1699
  const sandboxInfo = {
1700
1700
  sandboxId: sandboxId,
1701
1701
  os: osType,
@@ -1756,15 +1756,9 @@ ${regression}
1756
1756
  // Also clear this.sandboxId to prevent reconnection attempts
1757
1757
  this.sandboxId = null;
1758
1758
  if (!this.config.CI && !this.newSandbox) {
1759
- this.emitter.emit(
1760
- events.log.log,
1761
- theme.dim("--`new` flag detected, will create a new sandbox"),
1762
- );
1759
+ this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
1763
1760
  } else if (this.newSandbox) {
1764
- this.emitter.emit(
1765
- events.log.log,
1766
- theme.dim("--new-sandbox flag detected, will create a new sandbox"),
1767
- );
1761
+ this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
1768
1762
  }
1769
1763
  }
1770
1764
 
@@ -1801,7 +1795,7 @@ ${regression}
1801
1795
  theme.dim(`using recent sandbox: ${recentId}`),
1802
1796
  );
1803
1797
  this.sandboxId = recentId;
1804
-
1798
+
1805
1799
  try {
1806
1800
  let instance = await this.connectToSandboxDirect(
1807
1801
  this.sandboxId,
@@ -1849,13 +1843,17 @@ ${regression}
1849
1843
  console.error("Failed to reconnect to sandbox:", error);
1850
1844
  }
1851
1845
  }
1852
-
1846
+
1853
1847
  // Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
1854
1848
  if (!this.instance) {
1855
1849
  const { formatter } = require("../sdk-log-formatter.js");
1856
1850
  this.emitter.emit(
1857
1851
  events.log.narration,
1858
- formatter.getPrefix("connect") + " " + theme.green.bold("Creating") + " " + theme.cyan(`new sandbox...`),
1852
+ formatter.getPrefix("connect") +
1853
+ " " +
1854
+ theme.green.bold("Creating") +
1855
+ " " +
1856
+ theme.cyan(`new sandbox...`),
1859
1857
  );
1860
1858
  // We don't have resiliency/retries baked in, so let's at least give it 1 attempt
1861
1859
  // to see if that fixes the issue.
@@ -1868,11 +1866,12 @@ ${regression}
1868
1866
  });
1869
1867
 
1870
1868
  // Extract the sandbox ID from the newly created sandbox
1871
- this.sandboxId = newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1872
-
1869
+ this.sandboxId =
1870
+ newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1871
+
1873
1872
  // Use the configured sandbox OS type
1874
1873
  this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
1875
-
1874
+
1876
1875
  let instance = await this.connectToSandboxDirect(
1877
1876
  this.sandboxId,
1878
1877
  true, // always persist by default
@@ -2001,7 +2000,6 @@ ${regression}
2001
2000
  }
2002
2001
 
2003
2002
  async renderSandbox(instance, headless = false) {
2004
-
2005
2003
  if (!headless) {
2006
2004
  let url;
2007
2005
 
@@ -2055,7 +2053,13 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2055
2053
  }
2056
2054
 
2057
2055
  const { formatter } = require("../sdk-log-formatter.js");
2058
- this.emitter.emit(events.log.narration, formatter.getPrefix("connect") + " " + theme.green.bold("Authenticating") + theme.dim("..."));
2056
+ this.emitter.emit(
2057
+ events.log.narration,
2058
+ formatter.getPrefix("connect") +
2059
+ " " +
2060
+ theme.green.bold("Authenticating") +
2061
+ theme.dim("..."),
2062
+ );
2059
2063
  let ableToAuth = await this.sandbox.auth(this.config.TD_API_KEY);
2060
2064
 
2061
2065
  if (!ableToAuth) {
@@ -2069,7 +2073,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2069
2073
 
2070
2074
  async connectToSandboxDirect(sandboxId, persist = false, keepAlive = null) {
2071
2075
  const { formatter } = require("../sdk-log-formatter.js");
2072
- this.emitter.emit(events.log.narration, formatter.getPrefix("connect") + " " + theme.green.bold("Connecting") + " " + theme.cyan(`to sandbox...`));
2076
+ this.emitter.emit(
2077
+ events.log.narration,
2078
+ formatter.getPrefix("connect") +
2079
+ " " +
2080
+ theme.green.bold("Connecting") +
2081
+ " " +
2082
+ theme.cyan(`to sandbox...`),
2083
+ );
2073
2084
  let reply = await this.sandbox.connect(sandboxId, persist, keepAlive);
2074
2085
 
2075
2086
  // reply includes { success, url, sandbox: {...} }
@@ -2111,15 +2122,18 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2111
2122
  let response = await this.sandbox.send(sandboxConfig, 60000 * 8);
2112
2123
 
2113
2124
  // Check if queued (all slots in use)
2114
- if (response.type === 'create.queued') {
2125
+ if (response.type === "create.queued") {
2115
2126
  this.emitter.emit(
2116
2127
  events.log.narration,
2117
- formatter.getPrefix("queue") + " " + theme.yellow.bold("Waiting") + " " +
2118
- theme.dim(response.message),
2128
+ formatter.getPrefix("queue") +
2129
+ " " +
2130
+ theme.yellow.bold("Waiting") +
2131
+ " " +
2132
+ theme.dim(response.message),
2119
2133
  );
2120
2134
 
2121
2135
  // Wait then retry
2122
- await new Promise(resolve => setTimeout(resolve, retryDelay));
2136
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
2123
2137
  continue;
2124
2138
  }
2125
2139
 
@@ -2138,10 +2152,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2138
2152
  // should be start of new session
2139
2153
  // If sandbox is connected, get system info; otherwise pass empty objects
2140
2154
  const isSandboxConnected = this.sandbox.apiSocketConnected;
2141
-
2155
+
2142
2156
  const sessionRes = await this.sdk.req("session/start", {
2143
- systemInformationOsInfo: isSandboxConnected ? await this.system.getSystemInformationOsInfo() : {},
2144
- mousePosition: isSandboxConnected ? await this.system.getMousePosition() : {},
2157
+ systemInformationOsInfo: isSandboxConnected
2158
+ ? await this.system.getSystemInformationOsInfo()
2159
+ : {},
2160
+ mousePosition: isSandboxConnected
2161
+ ? await this.system.getMousePosition()
2162
+ : {},
2145
2163
  activeWindow: isSandboxConnected ? await this.system.activeWin() : {},
2146
2164
  });
2147
2165
 
@@ -2152,7 +2170,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2152
2170
  }
2153
2171
 
2154
2172
  this.session.set(sessionRes.data.id);
2155
-
2173
+
2156
2174
  // Set Sentry session trace context for distributed tracing
2157
2175
  // This links CLI errors/logs to the same trace as API calls
2158
2176
  try {
@@ -10,14 +10,14 @@ const { events } = require("../events");
10
10
  */
11
11
  function getSentryTraceHeaders(sessionId) {
12
12
  if (!sessionId) return {};
13
-
13
+
14
14
  // Same logic as API: derive trace ID from session ID
15
- const traceId = crypto.createHash('md5').update(sessionId).digest('hex');
16
- const spanId = crypto.randomBytes(8).toString('hex');
17
-
15
+ const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
16
+ const spanId = crypto.randomBytes(8).toString("hex");
17
+
18
18
  return {
19
- 'sentry-trace': `${traceId}-${spanId}-1`,
20
- 'baggage': `sentry-trace_id=${traceId},sentry-sample_rate=1.0,sentry-sampled=true`
19
+ "sentry-trace": `${traceId}-${spanId}-1`,
20
+ baggage: `sentry-trace_id=${traceId},sentry-sample_rate=1.0,sentry-sampled=true`,
21
21
  };
22
22
  }
23
23
 
@@ -36,6 +36,11 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
36
36
  this.os = null; // Store OS value to send with every message
37
37
  this.sessionInstance = sessionInstance; // Store session instance to include in messages
38
38
  this.traceId = null; // Sentry trace ID for debugging
39
+ this.reconnectAttempts = 0;
40
+ this.maxReconnectAttempts = 5;
41
+ this.intentionalDisconnect = false;
42
+ this.apiRoot = null;
43
+ this.apiKey = null;
39
44
  }
40
45
 
41
46
  /**
@@ -90,12 +95,16 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
90
95
  });
91
96
 
92
97
  const requestId = message.requestId;
93
-
98
+
94
99
  // Set up timeout to prevent hanging requests
95
100
  const timeoutId = setTimeout(() => {
96
101
  if (this.ps[requestId]) {
97
102
  delete this.ps[requestId];
98
- rejectPromise(new Error(`Sandbox message '${message.type}' timed out after ${timeout}ms`));
103
+ rejectPromise(
104
+ new Error(
105
+ `Sandbox message '${message.type}' timed out after ${timeout}ms`,
106
+ ),
107
+ );
99
108
  }
100
109
  }, timeout);
101
110
 
@@ -115,12 +124,13 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
115
124
 
116
125
  return p;
117
126
  }
118
-
127
+
119
128
  // Return a rejected promise if socket is not available
120
- return Promise.reject(new Error('Sandbox socket not connected'));
129
+ return Promise.reject(new Error("Sandbox socket not connected"));
121
130
  }
122
131
 
123
132
  async auth(apiKey) {
133
+ this.apiKey = apiKey;
124
134
  let reply = await this.send({
125
135
  type: "authenticate",
126
136
  apiKey,
@@ -128,15 +138,17 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
128
138
 
129
139
  if (reply.success) {
130
140
  this.authenticated = true;
131
-
141
+
132
142
  // Log and store the Sentry trace ID for debugging
133
143
  if (reply.traceId) {
134
144
  this.traceId = reply.traceId;
135
- console.log('');
145
+ console.log("");
136
146
  console.log(`🔗 View Trace:`);
137
- console.log(`https://testdriver.sentry.io/explore/traces/trace/${reply.traceId}`);
147
+ console.log(
148
+ `https://testdriver.sentry.io/explore/traces/trace/${reply.traceId}`,
149
+ );
138
150
  }
139
-
151
+
140
152
  emitter.emit(events.sandbox.authenticated, { traceId: reply.traceId });
141
153
  return true;
142
154
  }
@@ -161,24 +173,58 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
161
173
  }
162
174
  }
163
175
 
176
+ async handleConnectionLoss() {
177
+ if (this.intentionalDisconnect) return;
178
+
179
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
180
+ const errorMsg =
181
+ "Unable to reconnect to TestDriver sandbox after multiple attempts. Please check your internet connection.";
182
+ emitter.emit(events.error.sandbox, errorMsg);
183
+ console.error(errorMsg);
184
+ return;
185
+ }
186
+
187
+ this.reconnectAttempts++;
188
+ const delay = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 30000);
189
+
190
+ console.log(
191
+ `[Sandbox] Connection lost. Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
192
+ );
193
+
194
+ setTimeout(async () => {
195
+ try {
196
+ await this.boot(this.apiRoot);
197
+ if (this.apiKey) {
198
+ await this.auth(this.apiKey);
199
+ }
200
+ console.log("[Sandbox] Reconnected successfully.");
201
+ } catch (e) {
202
+ // Ignore error here as the boot's error handler will trigger handleConnectionLoss again
203
+ }
204
+ }, delay);
205
+ }
206
+
164
207
  async boot(apiRoot) {
208
+ if (apiRoot) this.apiRoot = apiRoot;
165
209
  return new Promise((resolve, reject) => {
166
210
  // Get session ID for Sentry trace headers
167
211
  const sessionId = this.sessionInstance?.get();
168
-
212
+
169
213
  if (!sessionId) {
170
- console.warn('[Sandbox] No session ID available at boot time - Sentry tracing will not be available');
214
+ console.warn(
215
+ "[Sandbox] No session ID available at boot time - Sentry tracing will not be available",
216
+ );
171
217
  }
172
-
218
+
173
219
  const sentryHeaders = getSentryTraceHeaders(sessionId);
174
220
 
175
221
  // Build WebSocket URL with Sentry trace headers as query params
176
222
  const wsUrl = new URL(apiRoot.replace("https://", "wss://"));
177
- if (sentryHeaders['sentry-trace']) {
178
- wsUrl.searchParams.set('sentry-trace', sentryHeaders['sentry-trace']);
223
+ if (sentryHeaders["sentry-trace"]) {
224
+ wsUrl.searchParams.set("sentry-trace", sentryHeaders["sentry-trace"]);
179
225
  }
180
- if (sentryHeaders['baggage']) {
181
- wsUrl.searchParams.set('baggage', sentryHeaders['baggage']);
226
+ if (sentryHeaders["baggage"]) {
227
+ wsUrl.searchParams.set("baggage", sentryHeaders["baggage"]);
182
228
  }
183
229
 
184
230
  this.socket = new WebSocket(wsUrl.toString());
@@ -189,6 +235,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
189
235
  // Emit a clear error event for API key issues
190
236
  reject();
191
237
  this.apiSocketConnected = false;
238
+ this.handleConnectionLoss();
192
239
  });
193
240
 
194
241
  this.socket.on("error", (err) => {
@@ -197,10 +244,13 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
197
244
  clearInterval(this.heartbeat);
198
245
  emitter.emit(events.error.sandbox, err);
199
246
  this.apiSocketConnected = false;
200
- throw err;
247
+ this.handleConnectionLoss();
248
+ // We don't throw here to avoid crashing the process, let reconnection handle it
249
+ reject(err);
201
250
  });
202
251
 
203
252
  this.socket.on("open", async () => {
253
+ this.reconnectAttempts = 0;
204
254
  this.apiSocketConnected = true;
205
255
 
206
256
  this.heartbeat = setInterval(() => {
@@ -216,7 +266,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
216
266
  let message = JSON.parse(raw);
217
267
 
218
268
  // Handle progress messages (no requestId needed)
219
- if (message.type === 'sandbox.progress') {
269
+ if (message.type === "sandbox.progress") {
220
270
  emitter.emit(events.sandbox.progress, {
221
271
  step: message.step,
222
272
  message: message.message,
@@ -250,11 +300,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
250
300
  * Close the WebSocket connection and clean up resources
251
301
  */
252
302
  close() {
303
+ this.intentionalDisconnect = true;
253
304
  if (this.heartbeat) {
254
305
  clearInterval(this.heartbeat);
255
306
  this.heartbeat = null;
256
307
  }
257
-
308
+
258
309
  if (this.socket) {
259
310
  try {
260
311
  this.socket.close();
@@ -263,12 +314,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
263
314
  }
264
315
  this.socket = null;
265
316
  }
266
-
317
+
267
318
  this.apiSocketConnected = false;
268
319
  this.instanceSocketConnected = false;
269
320
  this.authenticated = false;
270
321
  this.instance = null;
271
-
322
+
272
323
  // Silently clear pending promises without rejecting
273
324
  // (rejecting causes unhandled promise rejections during cleanup)
274
325
  this.ps = {};
@@ -27,6 +27,7 @@ describe('My Test Suite', () => {
27
27
  - `provision.vscode()` - Launch VS Code with optional extensions
28
28
  - `provision.installer()` - Download and install applications
29
29
  - `provision.electron()` - Launch Electron applications
30
+ - `provision.dashcam()` - Initialize Dashcam recording with logging
30
31
 
31
32
  ---
32
33
 
@@ -248,6 +249,66 @@ it('should launch Electron app', async (context) => {
248
249
 
249
250
  ---
250
251
 
252
+ ## provision.dashcam()
253
+
254
+ Initialize Dashcam recording with logging. Use this when you want to start Dashcam recording without launching a specific application (e.g., for custom application launches or testing scenarios).
255
+
256
+ ```javascript
257
+ await testdriver.provision.dashcam({
258
+ logPath: '/tmp/myapp.log',
259
+ logName: 'My Application Log',
260
+ title: 'Custom Test Recording'
261
+ });
262
+ ```
263
+
264
+ **Options:**
265
+ | Option | Type | Default | Description |
266
+ |--------|------|---------|-------------|
267
+ | `logPath` | string | auto-generated | Path to log file to track |
268
+ | `logName` | string | `'TestDriver Log'` | Display name for the log |
269
+ | `webLogs` | boolean | `true` | Enable web log tracking |
270
+ | `title` | string | - | Custom title for the recording |
271
+
272
+ **Example - Basic Recording:**
273
+
274
+ ```javascript
275
+ it('should record a custom application test', async (context) => {
276
+ const testdriver = TestDriver(context, ());
277
+
278
+ // Start Dashcam recording
279
+ await testdriver.provision.dashcam();
280
+
281
+ // Launch your custom application
282
+ await testdriver.exec('sh', './my-custom-app.sh &', 10000);
283
+
284
+ // Interact with the application
285
+ await testdriver.find('main window').click();
286
+
287
+ const result = await testdriver.assert('Application is running');
288
+ expect(result).toBeTruthy();
289
+ });
290
+ ```
291
+
292
+ **Example - Custom Log File:**
293
+
294
+ ```javascript
295
+ it('should record with custom log file', async (context) => {
296
+ const testdriver = TestDriver(context, ());
297
+
298
+ // Start Dashcam with custom log tracking
299
+ await testdriver.provision.dashcam({
300
+ logPath: '/var/log/myapp/application.log',
301
+ logName: 'My App Logs',
302
+ title: 'Custom App Test'
303
+ });
304
+
305
+ // Your test code here
306
+ await testdriver.exec('sh', 'myapp --start', 30000);
307
+ });
308
+ ```
309
+
310
+ ---
311
+
251
312
  ## How Provision Methods Work
252
313
 
253
314
  When you call a provision method:
@@ -112,7 +112,7 @@ class Dashcam {
112
112
  shell,
113
113
  "npm prefix -g",
114
114
  40000,
115
- false,
115
+ process.env.DEBUG == "true" ? false : true,
116
116
  );
117
117
 
118
118
  if (this.client.os === "windows") {
@@ -140,7 +140,7 @@ class Dashcam {
140
140
  shell,
141
141
  `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" auth ${key}`,
142
142
  120000,
143
- false,
143
+ process.env.DEBUG == "true" ? false : true,
144
144
  );
145
145
  this._log("debug", "Auth output:", authOutput);
146
146
  } else {
@@ -149,7 +149,7 @@ class Dashcam {
149
149
  shell,
150
150
  `TD_API_ROOT="${apiRoot}" dashcam auth ${key}`,
151
151
  120000,
152
- false,
152
+ process.env.DEBUG == "true" ? false : true,
153
153
  );
154
154
  this._log("debug", "Auth output:", authOutput);
155
155
  }
@@ -173,7 +173,7 @@ class Dashcam {
173
173
  shell,
174
174
  `New-Item -ItemType File -Path "${path}" -Force`,
175
175
  10000,
176
- false,
176
+ process.env.DEBUG == "true" ? false : true,
177
177
  );
178
178
  this._log("debug", "Create log file output:", createFileOutput);
179
179
 
@@ -182,19 +182,24 @@ class Dashcam {
182
182
  shell,
183
183
  `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=file --file="${path}" --name="${name}"`,
184
184
  120000,
185
- false,
185
+ process.env.DEBUG == "true" ? false : true,
186
186
  );
187
187
  this._log("debug", "Add log tracking output:", addLogOutput);
188
188
  } else {
189
189
  // Create log file
190
- await this.client.exec(shell, `touch ${path}`, 10000, false);
190
+ await this.client.exec(
191
+ shell,
192
+ `touch ${path}`,
193
+ 10000,
194
+ process.env.DEBUG == "true" ? false : true,
195
+ );
191
196
 
192
197
  // Add log tracking with TD_API_ROOT
193
198
  const addLogOutput = await this.client.exec(
194
199
  shell,
195
200
  `TD_API_ROOT="${apiRoot}" dashcam logs --add --type=file --file="${path}" --name="${name}"`,
196
201
  10000,
197
- false,
202
+ process.env.DEBUG == "true" ? false : true,
198
203
  );
199
204
  this._log("debug", "Add log tracking output:", addLogOutput);
200
205
  }
@@ -216,7 +221,7 @@ class Dashcam {
216
221
  shell,
217
222
  `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=application --application="${application}" --name="${name}"`,
218
223
  120000,
219
- false,
224
+ process.env.DEBUG == "true" ? false : true,
220
225
  );
221
226
  this._log("debug", "Add application log tracking output:", addLogOutput);
222
227
  } else {
@@ -224,7 +229,7 @@ class Dashcam {
224
229
  shell,
225
230
  `TD_API_ROOT="${apiRoot}" dashcam logs --add --type=application --application="${application}" --name="${name}"`,
226
231
  10000,
227
- false,
232
+ process.env.DEBUG == "true" ? false : true,
228
233
  );
229
234
  this._log("debug", "Add application log tracking output:", addLogOutput);
230
235
  }
@@ -246,7 +251,7 @@ class Dashcam {
246
251
  shell,
247
252
  `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=web --pattern="${pattern}" --name="${name}"`,
248
253
  120000,
249
- false,
254
+ process.env.DEBUG == "true" ? false : true,
250
255
  );
251
256
  this._log("debug", "Add web log tracking output:", addLogOutput);
252
257
  } else {
@@ -254,7 +259,7 @@ class Dashcam {
254
259
  shell,
255
260
  `TD_API_ROOT="${apiRoot}" dashcam logs --add --type=web --pattern="${pattern}" --name="${name}"`,
256
261
  10000,
257
- false,
262
+ process.env.DEBUG == "true" ? false : true,
258
263
  );
259
264
  this._log("debug", "Add web log tracking output:", addLogOutput);
260
265
  }
@@ -310,7 +315,7 @@ class Dashcam {
310
315
  shell,
311
316
  startScript,
312
317
  10000,
313
- false,
318
+ process.env.DEBUG == "true" ? false : true,
314
319
  );
315
320
  this._log("debug", "Start-Process output:", startOutput);
316
321
 
@@ -320,7 +325,7 @@ class Dashcam {
320
325
  shell,
321
326
  `Get-Content "${outputFile}" -ErrorAction SilentlyContinue`,
322
327
  10000,
323
- false,
328
+ process.env.DEBUG == "true" ? false : true,
324
329
  );
325
330
  this._log("debug", "Dashcam record output:", dashcamOutput);
326
331
 
@@ -339,6 +344,8 @@ class Dashcam {
339
344
  await this.client.exec(
340
345
  shell,
341
346
  `TD_API_ROOT="${apiRoot}" dashcam record${titleArg} >/dev/null 2>&1 &`,
347
+ 10000,
348
+ process.env.DEBUG == "true" ? false : true,
342
349
  );
343
350
  this._log("debug", "Dashcam recording started");
344
351
  }
@@ -382,7 +389,7 @@ class Dashcam {
382
389
  shell,
383
390
  `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`,
384
391
  300000,
385
- process.env.DEBUG == "true" ? true : false,
392
+ process.env.DEBUG == "true" ? false : true,
386
393
  );
387
394
  this._log("debug", "Dashcam stop command output:", output);
388
395
  } else {
@@ -392,7 +399,7 @@ class Dashcam {
392
399
  shell,
393
400
  `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`,
394
401
  300000,
395
- process.env.DEBUG == "true" ? true : false,
402
+ process.env.DEBUG == "true" ? false : true,
396
403
  );
397
404
  this._log("debug", "Dashcam command output:", output);
398
405
  }
@@ -114,12 +114,10 @@ function cleanupAllInstances() {
114
114
  // Register cleanup handlers for various exit scenarios
115
115
  process.on("exit", cleanupAllInstances);
116
116
  process.on("SIGINT", () => {
117
- console.log("\n[TestDriver] Received SIGINT, cleaning up instances...");
118
117
  cleanupAllInstances();
119
118
  // Don't call process.exit here - let the signal handler do its job
120
119
  });
121
120
  process.on("SIGTERM", () => {
122
- console.log("\n[TestDriver] Received SIGTERM, cleaning up instances...");
123
121
  cleanupAllInstances();
124
122
  // Don't call process.exit here - let the signal handler do its job
125
123
  });
@@ -142,14 +140,6 @@ beforeEach(async (context) => {
142
140
  return;
143
141
  }
144
142
 
145
- // Verify required parameters are available
146
- if (process.env.TD_OS === "windows") {
147
- console.log(
148
- "[TestDriver] startup check: TWOCAPTCHA_API_KEY is " +
149
- (process.env.TWOCAPTCHA_API_KEY ? "REQUIRED" : "MISSING"),
150
- );
151
- }
152
-
153
143
  if (!process.env.AWS_LAUNCH_TEMPLATE_ID || !process.env.AMI_ID) {
154
144
  throw new Error(
155
145
  "[TestDriver] TD_OS=windows requires AWS_LAUNCH_TEMPLATE_ID and AMI_ID environment variables",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.62",
3
+ "version": "7.2.64",
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
@@ -769,6 +769,18 @@ export interface ProvisionElectronOptions {
769
769
  args?: string[];
770
770
  }
771
771
 
772
+ /** Options for provision.dashcam */
773
+ export interface ProvisionDashcamOptions {
774
+ /** Path to log file (auto-generated if not provided) */
775
+ logPath?: string;
776
+ /** Display name for the log (default: 'TestDriver Log') */
777
+ logName?: string;
778
+ /** Enable web log tracking (default: true) */
779
+ webLogs?: boolean;
780
+ /** Custom title for the recording */
781
+ title?: string;
782
+ }
783
+
772
784
  /** Provision API for launching applications */
773
785
  export interface ProvisionAPI {
774
786
  /**
@@ -801,6 +813,12 @@ export interface ProvisionAPI {
801
813
  * @param options - Electron launch options
802
814
  */
803
815
  electron(options: ProvisionElectronOptions): Promise<void>;
816
+
817
+ /**
818
+ * Initialize Dashcam recording with logging
819
+ * @param options - Dashcam options
820
+ */
821
+ dashcam(options?: ProvisionDashcamOptions): Promise<void>;
804
822
  }
805
823
 
806
824
  /** Dashcam API for screen recording */
@@ -1229,6 +1247,7 @@ export default class TestDriverSDK {
1229
1247
  // AI Methods (Exploratory Loop)
1230
1248
 
1231
1249
  /**
1250
+ * @deprecated Use ai() instead
1232
1251
  * Execute a natural language task using AI
1233
1252
  * This is the SDK equivalent of the CLI's exploratory loop
1234
1253
  *
@@ -1238,11 +1257,11 @@ export default class TestDriverSDK {
1238
1257
  *
1239
1258
  * @example
1240
1259
  * // Simple execution
1241
- * await client.act('Click the submit button');
1260
+ * await client.ai('Click the submit button');
1242
1261
  *
1243
1262
  * @example
1244
1263
  * // With validation loop
1245
- * const result = await client.act('Fill out the contact form', { validateAndLoop: true });
1264
+ * const result = await client.ai('Fill out the contact form', { validateAndLoop: true });
1246
1265
  * console.log(result); // AI's final assessment
1247
1266
  */
1248
1267
  act(
@@ -1251,7 +1270,6 @@ export default class TestDriverSDK {
1251
1270
  ): Promise<string | void>;
1252
1271
 
1253
1272
  /**
1254
- * @deprecated Use act() instead
1255
1273
  * Execute a natural language task using AI
1256
1274
  *
1257
1275
  * @param task - Natural language description of what to do
package/sdk.js CHANGED
@@ -1506,64 +1506,7 @@ class TestDriverSDK {
1506
1506
  await this._dashcam.addWebLog("**", "Web Logs");
1507
1507
  }
1508
1508
 
1509
- // Set up Chrome profile with preferences
1510
1509
  const shell = this.os === "windows" ? "pwsh" : "sh";
1511
- const userDataDir =
1512
- this.os === "windows"
1513
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1514
- : "/tmp/testdriver-chrome-profile";
1515
-
1516
- // Create user data directory and Default profile directory
1517
- const defaultProfileDir =
1518
- this.os === "windows"
1519
- ? `${userDataDir}\\Default`
1520
- : `${userDataDir}/Default`;
1521
-
1522
- const createDirCmd =
1523
- this.os === "windows"
1524
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1525
- : `mkdir -p "${defaultProfileDir}"`;
1526
-
1527
- await this.exec(shell, createDirCmd, 60000, true);
1528
-
1529
- // Write Chrome preferences
1530
- const chromePrefs = {
1531
- credentials_enable_service: false,
1532
- profile: {
1533
- password_manager_enabled: false,
1534
- default_content_setting_values: {},
1535
- },
1536
- signin: {
1537
- allowed: false,
1538
- },
1539
- sync: {
1540
- requested: false,
1541
- first_setup_complete: true,
1542
- sync_all_os_types: false,
1543
- },
1544
- autofill: {
1545
- enabled: false,
1546
- },
1547
- local_state: {
1548
- browser: {
1549
- has_seen_welcome_page: true,
1550
- },
1551
- },
1552
- };
1553
-
1554
- const prefsPath =
1555
- this.os === "windows"
1556
- ? `${defaultProfileDir}\\Preferences`
1557
- : `${defaultProfileDir}/Preferences`;
1558
-
1559
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
1560
- const writePrefCmd =
1561
- this.os === "windows"
1562
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1563
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1564
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1565
-
1566
- await this.exec(shell, writePrefCmd, 60000, true);
1567
1510
 
1568
1511
  // Build Chrome launch command
1569
1512
  const chromeArgs = [];
@@ -1575,7 +1518,6 @@ class TestDriverSDK {
1575
1518
  "--no-first-run",
1576
1519
  "--no-experiments",
1577
1520
  "--disable-infobars",
1578
- `--user-data-dir=${userDataDir}`,
1579
1521
  );
1580
1522
 
1581
1523
  // Add remote debugging port for captcha solving support
@@ -1788,64 +1730,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1788
1730
  await this._dashcam.addWebLog("**", "Web Logs");
1789
1731
  }
1790
1732
 
1791
- // Set up Chrome profile with preferences
1792
- const userDataDir =
1793
- this.os === "windows"
1794
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1795
- : "/tmp/testdriver-chrome-profile";
1796
-
1797
- // Create user data directory and Default profile directory
1798
- const defaultProfileDir =
1799
- this.os === "windows"
1800
- ? `${userDataDir}\\Default`
1801
- : `${userDataDir}/Default`;
1802
-
1803
- const createDirCmd =
1804
- this.os === "windows"
1805
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1806
- : `mkdir -p "${defaultProfileDir}"`;
1807
-
1808
- await this.exec(shell, createDirCmd, 60000, true);
1809
-
1810
- // Write Chrome preferences
1811
- const chromePrefs = {
1812
- credentials_enable_service: false,
1813
- profile: {
1814
- password_manager_enabled: false,
1815
- default_content_setting_values: {},
1816
- },
1817
- signin: {
1818
- allowed: false,
1819
- },
1820
- sync: {
1821
- requested: false,
1822
- first_setup_complete: true,
1823
- sync_all_os_types: false,
1824
- },
1825
- autofill: {
1826
- enabled: false,
1827
- },
1828
- local_state: {
1829
- browser: {
1830
- has_seen_welcome_page: true,
1831
- },
1832
- },
1833
- };
1834
-
1835
- const prefsPath =
1836
- this.os === "windows"
1837
- ? `${defaultProfileDir}\\Preferences`
1838
- : `${defaultProfileDir}/Preferences`;
1839
-
1840
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
1841
- const writePrefCmd =
1842
- this.os === "windows"
1843
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1844
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1845
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1846
-
1847
- await this.exec(shell, writePrefCmd, 60000, true);
1848
-
1849
1733
  // Build Chrome launch command
1850
1734
  const chromeArgs = [];
1851
1735
  if (maximized) chromeArgs.push("--start-maximized");
@@ -1856,7 +1740,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1856
1740
  "--no-experiments",
1857
1741
  "--disable-infobars",
1858
1742
  "--disable-features=ChromeLabs",
1859
- `--user-data-dir=${userDataDir}`,
1860
1743
  );
1861
1744
 
1862
1745
  // Add remote debugging port for captcha solving support
@@ -2160,6 +2043,58 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2160
2043
 
2161
2044
  await this.focusApplication("Electron");
2162
2045
  },
2046
+
2047
+ /**
2048
+ * Initialize Dashcam recording with logging
2049
+ * @param {Object} options - Dashcam options
2050
+ * @param {string} [options.logPath] - Path to log file (auto-generated if not provided)
2051
+ * @param {string} [options.logName='TestDriver Log'] - Display name for the log
2052
+ * @param {boolean} [options.webLogs=true] - Enable web log tracking
2053
+ * @param {string} [options.title] - Custom title for the recording
2054
+ * @returns {Promise<void>}
2055
+ */
2056
+ dashcam: async (options = {}) => {
2057
+ const {
2058
+ logPath,
2059
+ logName = "TestDriver Log",
2060
+ webLogs = true,
2061
+ title,
2062
+ } = options;
2063
+
2064
+ // Ensure dashcam is available
2065
+ if (!this._dashcam) {
2066
+ console.warn(
2067
+ "[provision.dashcam] Dashcam is not available. Skipping.",
2068
+ );
2069
+ return;
2070
+ }
2071
+
2072
+ // Set custom title if provided
2073
+ if (title) {
2074
+ this._dashcam.setTitle(title);
2075
+ }
2076
+
2077
+ // Add file log tracking
2078
+ const actualLogPath =
2079
+ logPath ||
2080
+ (this.os === "windows"
2081
+ ? "C:\\Users\\testdriver\\testdriver.log"
2082
+ : "/tmp/testdriver.log");
2083
+
2084
+ await this._dashcam.addFileLog(actualLogPath, logName);
2085
+
2086
+ // Add web log tracking if enabled
2087
+ if (webLogs) {
2088
+ await this._dashcam.addWebLog("**", "Web Logs");
2089
+ }
2090
+
2091
+ // Start recording if not already recording
2092
+ if (!(await this._dashcam.isRecording())) {
2093
+ await this._dashcam.start();
2094
+ }
2095
+
2096
+ console.log("[provision.dashcam] ✅ Dashcam recording started");
2097
+ },
2163
2098
  };
2164
2099
 
2165
2100
  // Wrap all provision methods with reconnect check using Proxy
@@ -3499,28 +3434,28 @@ CAPTCHA_SOLVER_EOF`,
3499
3434
  *
3500
3435
  * @example
3501
3436
  * // Simple execution
3502
- * const result = await client.act('Click the submit button');
3437
+ * const result = await client.ai('Click the submit button');
3503
3438
  * console.log(result.success); // true
3504
3439
  *
3505
3440
  * @example
3506
3441
  * // With custom retry limit
3507
- * const result = await client.act('Fill out the contact form', { tries: 10 });
3442
+ * const result = await client.ai('Fill out the contact form', { tries: 10 });
3508
3443
  * console.log(`Completed in ${result.tries} tries`);
3509
3444
  *
3510
3445
  * @example
3511
3446
  * // Handle failures
3512
3447
  * try {
3513
- * await client.act('Complete the checkout process', { tries: 3 });
3448
+ * await client.ai('Complete the checkout process', { tries: 3 });
3514
3449
  * } catch (error) {
3515
3450
  * console.log(`Failed after ${error.tries} tries: ${error.message}`);
3516
3451
  * }
3517
3452
  */
3518
- async act(task, options = {}) {
3453
+ async ai(task, options = {}) {
3519
3454
  this._ensureConnected();
3520
3455
 
3521
3456
  const { tries = 7 } = options;
3522
3457
 
3523
- this.analytics.track("sdk.act", { task, tries });
3458
+ this.analytics.track("sdk.ai", { task, tries });
3524
3459
 
3525
3460
  const { events } = require("./agent/events.js");
3526
3461
  const startTime = Date.now();
@@ -3529,7 +3464,7 @@ CAPTCHA_SOLVER_EOF`,
3529
3464
  const originalCheckLimit = this.agent.checkLimit;
3530
3465
  this.agent.checkLimit = tries;
3531
3466
 
3532
- // Reset check count for this act() call
3467
+ // Reset check count for this ai() call
3533
3468
  const originalCheckCount = this.agent.checkCount;
3534
3469
  this.agent.checkCount = 0;
3535
3470
 
@@ -3596,7 +3531,7 @@ CAPTCHA_SOLVER_EOF`,
3596
3531
  }
3597
3532
 
3598
3533
  /**
3599
- * @deprecated Use act() instead
3534
+ * @deprecated Use ai() instead
3600
3535
  * Execute a natural language task using AI
3601
3536
  *
3602
3537
  * @param {string} task - Natural language description of what to do
@@ -3604,8 +3539,8 @@ CAPTCHA_SOLVER_EOF`,
3604
3539
  * @param {number} [options.tries=7] - Maximum number of check/retry attempts
3605
3540
  * @returns {Promise<ActResult>} Result object with success status and details
3606
3541
  */
3607
- async ai(task, options) {
3608
- return await this.act(task, options);
3542
+ async act(task, options) {
3543
+ return await this.ai(task, options);
3609
3544
  }
3610
3545
  }
3611
3546