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 +8 -0
- package/lib/core/Dashcam.js +5 -3
- package/lib/vitest/hooks.mjs +137 -41
- package/package.json +1 -1
- package/sdk.js +78 -21
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
|
|
package/lib/core/Dashcam.js
CHANGED
|
@@ -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
|
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -104,22 +104,25 @@ function serialiseConsoleArgs(args) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
151
|
-
|
|
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
|
-
//
|
|
344
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
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.
|
|
619
|
-
|
|
620
|
-
.
|
|
621
|
-
type: "
|
|
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.
|
|
3173
|
-
this.
|
|
3174
|
-
.
|
|
3175
|
-
type: "
|
|
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.
|
|
3229
|
-
this.
|
|
3230
|
-
.
|
|
3231
|
-
type: "
|
|
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.
|
|
3274
|
-
this.
|
|
3275
|
-
.
|
|
3276
|
-
type: "
|
|
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
|
|
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
|