testdriverai 7.3.40 → 7.3.41

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,7 @@
1
+ ## [7.3.41](https://github.com/testdriverai/testdriverai/compare/v7.3.40...v7.3.41) (2026-02-25)
2
+
3
+
4
+
1
5
  ## [7.3.40](https://github.com/testdriverai/testdriverai/compare/v7.3.39...v7.3.40) (2026-02-25)
2
6
 
3
7
 
@@ -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.41",
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
@@ -1601,6 +1601,10 @@ class TestDriverSDK {
1601
1601
  // Set up logging if enabled (after emitter is exposed)
1602
1602
  this.loggingEnabled = options.logging !== false;
1603
1603
 
1604
+ // Log buffer: structured entries collected during test execution.
1605
+ // Uploaded to S3 at cleanup so they can be displayed alongside dashcam replays.
1606
+ this._logBuffer = [];
1607
+
1604
1608
  // Set up event listeners once (they live for the lifetime of the SDK instance)
1605
1609
  this._setupLogging();
1606
1610
 
@@ -3797,7 +3801,7 @@ CAPTCHA_SOLVER_EOF`,
3797
3801
 
3798
3802
  // Set up basic event logging
3799
3803
  // Note: We only console.log here - the console spy in vitest/hooks.mjs
3800
- // handles forwarding to sandbox. This prevents duplicate output to server.
3804
+ // handles forwarding to the local log buffer.
3801
3805
  this.emitter.on("log:**", (message) => {
3802
3806
  const event = this.emitter.event;
3803
3807
 
@@ -3812,6 +3816,21 @@ CAPTCHA_SOLVER_EOF`,
3812
3816
  : message;
3813
3817
  console.log(prefixedMessage);
3814
3818
  }
3819
+
3820
+ // Buffer structured SDK log for later upload
3821
+ if (message) {
3822
+ const level = event === events.log.warn ? "warn"
3823
+ : event === events.log.debug ? "debug"
3824
+ : "info";
3825
+ this._logBuffer.push({
3826
+ time: Date.now(),
3827
+ line: String(message),
3828
+ level,
3829
+ source: "sdk",
3830
+ event,
3831
+ logFile: "sdk",
3832
+ });
3833
+ }
3815
3834
  });
3816
3835
 
3817
3836
  this.emitter.on("error:**", (data) => {
@@ -3824,12 +3843,34 @@ CAPTCHA_SOLVER_EOF`,
3824
3843
  lastFatalError = data;
3825
3844
  }
3826
3845
  }
3846
+
3847
+ // Buffer error events for later upload
3848
+ this._logBuffer.push({
3849
+ time: Date.now(),
3850
+ line: `${this.emitter.event}: ${data}`,
3851
+ level: "error",
3852
+ source: "sdk",
3853
+ event: this.emitter.event,
3854
+ logFile: "sdk",
3855
+ });
3827
3856
  });
3828
3857
 
3829
3858
  this.emitter.on("status", (message) => {
3830
3859
  if (this.loggingEnabled) {
3831
3860
  console.log(`- ${message}`);
3832
3861
  }
3862
+
3863
+ // Buffer status events
3864
+ if (message) {
3865
+ this._logBuffer.push({
3866
+ time: Date.now(),
3867
+ line: `- ${message}`,
3868
+ level: "info",
3869
+ source: "sdk",
3870
+ event: "status",
3871
+ logFile: "sdk",
3872
+ });
3873
+ }
3833
3874
  });
3834
3875
 
3835
3876
  // Handle exit events - throw error with meaningful message instead of calling process.exit
@@ -4206,6 +4247,26 @@ CAPTCHA_SOLVER_EOF`,
4206
4247
  async ai(task, options) {
4207
4248
  return await this.act(task, options);
4208
4249
  }
4250
+
4251
+ /**
4252
+ * Get buffered logs as a JSONL string for upload.
4253
+ * Each line is a JSON object with { time, line, level, source, event }.
4254
+ * @returns {string} JSONL-formatted log data
4255
+ */
4256
+ getLogs() {
4257
+ if (this._logBuffer.length === 0) return "";
4258
+ const startTime = this._logBuffer[0].time;
4259
+ return this._logBuffer
4260
+ .map((entry) => JSON.stringify({ ...entry, time: entry.time - startTime }))
4261
+ .join("\n");
4262
+ }
4263
+
4264
+ /**
4265
+ * Clear the internal log buffer.
4266
+ */
4267
+ clearLogs() {
4268
+ this._logBuffer = [];
4269
+ }
4209
4270
  }
4210
4271
 
4211
4272
  // Expose SDK version as a static property for use by vitest hooks/plugins