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 +4 -0
- package/lib/vitest/hooks.mjs +137 -41
- package/package.json +1 -1
- package/sdk.js +62 -1
package/CHANGELOG.md
CHANGED
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
|
@@ -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
|
|
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
|