testdriverai 7.3.40 → 7.3.42

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.3.42](https://github.com/testdriverai/testdriverai/compare/v7.3.41...v7.3.42) (2026-02-25)
2
+
3
+
4
+
5
+ ## [7.3.41](https://github.com/testdriverai/testdriverai/compare/v7.3.40...v7.3.41) (2026-02-25)
6
+
7
+
8
+
1
9
  ## [7.3.40](https://github.com/testdriverai/testdriverai/compare/v7.3.39...v7.3.40) (2026-02-25)
2
10
 
3
11
 
@@ -108,6 +108,11 @@ class Dashcam {
108
108
  */
109
109
  async _getDashcamPath() {
110
110
  const shell = this._getShell();
111
+
112
+ if (this.client.os === "windows") {
113
+ return "C:\\Program Files\\nodejs\\dashcam.cmd";
114
+ }
115
+
111
116
  const npmPrefix = await this.client.exec(
112
117
  shell,
113
118
  "npm prefix -g",
@@ -115,9 +120,6 @@ class Dashcam {
115
120
  process.env.DEBUG == "true" ? false : true,
116
121
  );
117
122
 
118
- if (this.client.os === "windows") {
119
- return "C:\\Program Files\\nodejs\\dashcam.cmd";
120
- }
121
123
  return npmPrefix.trim() + "/bin/dashcam";
122
124
  }
123
125
 
@@ -104,22 +104,25 @@ function serialiseConsoleArgs(args) {
104
104
  }
105
105
 
106
106
  /**
107
- * Forward a console message to every active sandbox client.
108
- * Called from the (single) console spy.
107
+ * Buffer a console message into every active client's local log buffer.
108
+ * Replaces the old forwardToAllSandboxes which sent data over websocket
109
+ * to the sandbox for dashcam file-log capture. Logs are now uploaded
110
+ * directly to S3 from the vitest client at cleanup time.
109
111
  */
110
- function forwardToAllSandboxes(args) {
112
+ function bufferConsoleToClients(args, level) {
111
113
  if (_consoleSpy.activeClients.size === 0) return;
112
114
 
113
115
  const message = serialiseConsoleArgs(args);
114
- const encoded = Buffer.from(message, "utf8").toString("base64");
115
116
 
116
117
  for (const client of _consoleSpy.activeClients) {
117
- if (client.sandbox && client.sandbox.instanceSocketConnected) {
118
- try {
119
- client.sandbox.send({ type: "output", output: encoded });
120
- } catch {
121
- // fire-and-forget don't let one broken socket block the others
122
- }
118
+ if (client._logBuffer) {
119
+ client._logBuffer.push({
120
+ time: Date.now(),
121
+ line: message,
122
+ level: level || "log",
123
+ source: "console",
124
+ logFile: "console",
125
+ });
123
126
  }
124
127
  }
125
128
  }
@@ -146,16 +149,16 @@ function installConsoleSpy() {
146
149
  info: console.info.bind(console),
147
150
  };
148
151
 
149
- const makeHandler = (originalFn) => (...args) => {
150
- originalFn(...args); // Let Vitest's reporter capture the output
151
- forwardToAllSandboxes(args); // Forward to all sandbox dashcam streams
152
+ const makeHandler = (originalFn, level) => (...args) => {
153
+ originalFn(...args); // Let Vitest's reporter capture the output
154
+ bufferConsoleToClients(args, level); // Buffer into local log store for S3 upload
152
155
  };
153
156
 
154
157
  _consoleSpy.spies = {
155
- log: vi.spyOn(console, "log").mockImplementation(makeHandler(_consoleSpy.originals.log)),
156
- error: vi.spyOn(console, "error").mockImplementation(makeHandler(_consoleSpy.originals.error)),
157
- warn: vi.spyOn(console, "warn").mockImplementation(makeHandler(_consoleSpy.originals.warn)),
158
- info: vi.spyOn(console, "info").mockImplementation(makeHandler(_consoleSpy.originals.info)),
158
+ log: vi.spyOn(console, "log").mockImplementation(makeHandler(_consoleSpy.originals.log, "log")),
159
+ error: vi.spyOn(console, "error").mockImplementation(makeHandler(_consoleSpy.originals.error, "error")),
160
+ warn: vi.spyOn(console, "warn").mockImplementation(makeHandler(_consoleSpy.originals.warn, "warn")),
161
+ info: vi.spyOn(console, "info").mockImplementation(makeHandler(_consoleSpy.originals.info, "info")),
159
162
  };
160
163
 
161
164
  if (debugConsoleSpy) {
@@ -214,6 +217,110 @@ function cleanupConsoleSpy(client) {
214
217
  const testDriverInstances = new WeakMap();
215
218
  const lifecycleHandlers = new WeakMap();
216
219
 
220
+ /**
221
+ * Upload buffered SDK + console logs directly to S3 via the existing Log system.
222
+ * Extracts the replayId from the dashcam URL, calls POST /api/v1/logs to create
223
+ * a Log record and get a presigned PUT URL, then uploads the JSONL payload.
224
+ *
225
+ * @param {import('../../sdk.js').default} client - TestDriver SDK instance
226
+ * @param {string|null} dashcamUrl - Dashcam replay URL from dashcam.stop()
227
+ */
228
+ async function uploadLogsToReplay(client, dashcamUrl) {
229
+ if (!dashcamUrl) return;
230
+
231
+ // Extract replayId from the dashcam URL (e.g. https://app.dashcam.io/replay/6789abcdef012345...)
232
+ const replayMatch = dashcamUrl.match(/replay\/([a-f0-9]{24})/);
233
+ if (!replayMatch) {
234
+ if (debugConsoleSpy) {
235
+ console.log("[uploadLogsToReplay] Could not extract replayId from:", dashcamUrl);
236
+ }
237
+ return;
238
+ }
239
+
240
+ const replayId = replayMatch[1];
241
+ const logData = client.getLogs();
242
+
243
+ if (!logData || logData.trim().length === 0) {
244
+ if (debugConsoleSpy) {
245
+ console.log("[uploadLogsToReplay] No logs to upload");
246
+ }
247
+ return;
248
+ }
249
+
250
+ try {
251
+ // Get TD_API_KEY for auth (prefer SDK config which is always correctly resolved)
252
+ const apiKey = client.config?.TD_API_KEY || process.env.TD_API_KEY;
253
+ if (!apiKey) {
254
+ console.warn("[TestDriver] TD_API_KEY not set, skipping log upload");
255
+ return;
256
+ }
257
+
258
+ // Use the SDK's configured API root (matches what the SDK uses for all other API calls)
259
+ const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
260
+
261
+ console.log(`[TestDriver] Uploading logs for replay ${replayId} to ${apiRoot}...`);
262
+
263
+ const authRes = await fetch(`${apiRoot}/auth/exchange-api-key`, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify({ apiKey }),
267
+ });
268
+
269
+ if (!authRes.ok) {
270
+ console.warn("[TestDriver] Failed to exchange API key for log upload:", authRes.status);
271
+ return;
272
+ }
273
+
274
+ const authData = await authRes.json();
275
+ const token = authData.token;
276
+
277
+ // Create a Log record and get presigned upload URL
278
+ const logCreateRes = await fetch(`${apiRoot}/api/v1/logs`, {
279
+ method: "POST",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ Authorization: `Bearer ${token}`,
283
+ },
284
+ body: JSON.stringify({
285
+ replayId,
286
+ appId: "testdriver-sdk",
287
+ name: "TestDriver Log",
288
+ type: "cli",
289
+ }),
290
+ });
291
+
292
+ if (!logCreateRes.ok) {
293
+ console.warn("[TestDriver] Failed to create log record:", logCreateRes.status);
294
+ return;
295
+ }
296
+
297
+ const logCreateData = await logCreateRes.json();
298
+ const uploadUrl = logCreateData.presignedUploadUrl;
299
+
300
+ if (!uploadUrl) {
301
+ console.warn("[TestDriver] No presigned upload URL returned from log-create");
302
+ return;
303
+ }
304
+
305
+ // Upload the JSONL log data directly to S3
306
+ const uploadRes = await fetch(uploadUrl, {
307
+ method: "PUT",
308
+ headers: { "Content-Type": "application/x-ndjson" },
309
+ body: logData,
310
+ });
311
+
312
+ if (!uploadRes.ok) {
313
+ console.warn("[TestDriver] Failed to upload logs to S3:", uploadRes.status);
314
+ return;
315
+ }
316
+
317
+ console.log(`[TestDriver] ✅ Logs uploaded successfully for replay: ${replayId}`);
318
+ } catch (err) {
319
+ // Fire-and-forget — don't let log upload failure break the test
320
+ console.warn("[TestDriver] Log upload failed:", err.message);
321
+ }
322
+ }
323
+
217
324
  /**
218
325
  * Create a TestDriver client in a Vitest test with automatic lifecycle management
219
326
  *
@@ -340,31 +447,16 @@ export function TestDriver(context, options = {}) {
340
447
  );
341
448
  }
342
449
 
343
- // Only set up dashcam-related infrastructure if enabled (default: true)
344
- if (testdriver.dashcamEnabled) {
345
- // Set up console spy for dashcam visibility
346
- setupConsoleSpy(testdriver, context.task.id);
347
-
348
- // Create the log file on the remote machine
349
- const shell = testdriver.os === "windows" ? "pwsh" : "sh";
350
- const logPath =
351
- testdriver.os === "windows"
352
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
353
- : "/tmp/testdriver.log";
450
+ // Set up console spy for local log buffering (always, regardless of dashcam)
451
+ setupConsoleSpy(testdriver, context.task.id);
354
452
 
355
- const createLogCmd =
356
- testdriver.os === "windows"
357
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
358
- : `touch ${logPath}`;
359
-
360
- await testdriver.exec(shell, createLogCmd, 10000, true);
361
-
362
- // Add testdriver log to dashcam tracking
363
- await testdriver.dashcam.addFileLog(logPath, "TestDriver Log");
364
-
365
- // Web log tracking and dashcam.start() are handled by provision.chrome()
366
- // This ensures addWebLog is called with the domain pattern BEFORE dashcam.start()
367
- }
453
+ // Note: We no longer create a log file on the sandbox or call dashcam.addFileLog().
454
+ // TestDriver logs are buffered locally in _logBuffer and uploaded directly to S3
455
+ // from the vitest client at cleanup time. This avoids the expensive path of
456
+ // forwarding logs over websocket → sandbox file → dashcam upload.
457
+ //
458
+ // Web log tracking and dashcam.start() are still handled by provision.chrome()
459
+ // This ensures addWebLog is called with the domain pattern BEFORE dashcam.start()
368
460
  })();
369
461
 
370
462
  // Register cleanup handler with dashcam.stop()
@@ -496,6 +588,10 @@ export function TestDriver(context, options = {}) {
496
588
  );
497
589
  console.log("");
498
590
  }
591
+
592
+ // Upload buffered logs directly to S3 via the existing Log system.
593
+ // This replaces the old path of forwarding logs to the sandbox for dashcam capture.
594
+ await uploadLogsToReplay(currentInstance, dashcamUrl);
499
595
  } catch (error) {
500
596
  // Log more detailed error information for debugging
501
597
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.3.40",
3
+ "version": "7.3.42",
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.js CHANGED
@@ -615,11 +615,10 @@ class Element {
615
615
 
616
616
  // Track find interaction once at the end (fire-and-forget, don't block)
617
617
  const sessionId = this.sdk.getSessionId();
618
- if (sessionId && this.sdk.sandbox?.send) {
619
- await this.sdk.sandbox
620
- .send({
621
- type: "trackInteraction",
622
- interactionType: "find",
618
+ if (sessionId && this.sdk.apiClient) {
619
+ this.sdk.apiClient
620
+ .req("interaction/track", {
621
+ type: "find",
623
622
  session: sessionId,
624
623
  prompt: description,
625
624
  timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
@@ -1601,6 +1600,10 @@ class TestDriverSDK {
1601
1600
  // Set up logging if enabled (after emitter is exposed)
1602
1601
  this.loggingEnabled = options.logging !== false;
1603
1602
 
1603
+ // Log buffer: structured entries collected during test execution.
1604
+ // Uploaded to S3 at cleanup so they can be displayed alongside dashcam replays.
1605
+ this._logBuffer = [];
1606
+
1604
1607
  // Set up event listeners once (they live for the lifetime of the SDK instance)
1605
1608
  this._setupLogging();
1606
1609
 
@@ -3169,11 +3172,10 @@ CAPTCHA_SOLVER_EOF`,
3169
3172
 
3170
3173
  // Track successful findAll interaction (fire-and-forget, don't block)
3171
3174
  const sessionId = this.getSessionId();
3172
- if (sessionId && this.sandbox?.send) {
3173
- this.sandbox
3174
- .send({
3175
- type: "trackInteraction",
3176
- interactionType: "findAll",
3175
+ if (sessionId && this.apiClient) {
3176
+ this.apiClient
3177
+ .req("interaction/track", {
3178
+ type: "findAll",
3177
3179
  session: sessionId,
3178
3180
  prompt: description,
3179
3181
  timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
@@ -3225,11 +3227,10 @@ CAPTCHA_SOLVER_EOF`,
3225
3227
 
3226
3228
  // No elements found - track interaction (fire-and-forget, don't block)
3227
3229
  const sessionId = this.getSessionId();
3228
- if (sessionId && this.sandbox?.send) {
3229
- this.sandbox
3230
- .send({
3231
- type: "trackInteraction",
3232
- interactionType: "findAll",
3230
+ if (sessionId && this.apiClient) {
3231
+ this.apiClient
3232
+ .req("interaction/track", {
3233
+ type: "findAll",
3233
3234
  session: sessionId,
3234
3235
  prompt: description,
3235
3236
  timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
@@ -3270,11 +3271,10 @@ CAPTCHA_SOLVER_EOF`,
3270
3271
 
3271
3272
  // Track findAll error interaction (fire-and-forget, don't block)
3272
3273
  const sessionId = this.getSessionId();
3273
- if (sessionId && this.sandbox?.send) {
3274
- this.sandbox
3275
- .send({
3276
- type: "trackInteraction",
3277
- interactionType: "findAll",
3274
+ if (sessionId && this.apiClient) {
3275
+ this.apiClient
3276
+ .req("interaction/track", {
3277
+ type: "findAll",
3278
3278
  session: sessionId,
3279
3279
  prompt: description,
3280
3280
  timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
@@ -3797,7 +3797,7 @@ CAPTCHA_SOLVER_EOF`,
3797
3797
 
3798
3798
  // Set up basic event logging
3799
3799
  // Note: We only console.log here - the console spy in vitest/hooks.mjs
3800
- // handles forwarding to sandbox. This prevents duplicate output to server.
3800
+ // handles forwarding to the local log buffer.
3801
3801
  this.emitter.on("log:**", (message) => {
3802
3802
  const event = this.emitter.event;
3803
3803
 
@@ -3812,6 +3812,21 @@ CAPTCHA_SOLVER_EOF`,
3812
3812
  : message;
3813
3813
  console.log(prefixedMessage);
3814
3814
  }
3815
+
3816
+ // Buffer structured SDK log for later upload
3817
+ if (message) {
3818
+ const level = event === events.log.warn ? "warn"
3819
+ : event === events.log.debug ? "debug"
3820
+ : "info";
3821
+ this._logBuffer.push({
3822
+ time: Date.now(),
3823
+ line: String(message),
3824
+ level,
3825
+ source: "sdk",
3826
+ event,
3827
+ logFile: "sdk",
3828
+ });
3829
+ }
3815
3830
  });
3816
3831
 
3817
3832
  this.emitter.on("error:**", (data) => {
@@ -3824,12 +3839,34 @@ CAPTCHA_SOLVER_EOF`,
3824
3839
  lastFatalError = data;
3825
3840
  }
3826
3841
  }
3842
+
3843
+ // Buffer error events for later upload
3844
+ this._logBuffer.push({
3845
+ time: Date.now(),
3846
+ line: `${this.emitter.event}: ${data}`,
3847
+ level: "error",
3848
+ source: "sdk",
3849
+ event: this.emitter.event,
3850
+ logFile: "sdk",
3851
+ });
3827
3852
  });
3828
3853
 
3829
3854
  this.emitter.on("status", (message) => {
3830
3855
  if (this.loggingEnabled) {
3831
3856
  console.log(`- ${message}`);
3832
3857
  }
3858
+
3859
+ // Buffer status events
3860
+ if (message) {
3861
+ this._logBuffer.push({
3862
+ time: Date.now(),
3863
+ line: `- ${message}`,
3864
+ level: "info",
3865
+ source: "sdk",
3866
+ event: "status",
3867
+ logFile: "sdk",
3868
+ });
3869
+ }
3833
3870
  });
3834
3871
 
3835
3872
  // Handle exit events - throw error with meaningful message instead of calling process.exit
@@ -4206,6 +4243,26 @@ CAPTCHA_SOLVER_EOF`,
4206
4243
  async ai(task, options) {
4207
4244
  return await this.act(task, options);
4208
4245
  }
4246
+
4247
+ /**
4248
+ * Get buffered logs as a JSONL string for upload.
4249
+ * Each line is a JSON object with { time, line, level, source, event }.
4250
+ * @returns {string} JSONL-formatted log data
4251
+ */
4252
+ getLogs() {
4253
+ if (this._logBuffer.length === 0) return "";
4254
+ const startTime = this._logBuffer[0].time;
4255
+ return this._logBuffer
4256
+ .map((entry) => JSON.stringify({ ...entry, time: entry.time - startTime }))
4257
+ .join("\n");
4258
+ }
4259
+
4260
+ /**
4261
+ * Clear the internal log buffer.
4262
+ */
4263
+ clearLogs() {
4264
+ this._logBuffer = [];
4265
+ }
4209
4266
  }
4210
4267
 
4211
4268
  // Expose SDK version as a static property for use by vitest hooks/plugins