testdriverai 7.2.3 → 7.2.9
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/.github/workflows/publish.yaml +15 -7
- package/.github/workflows/testdriver.yml +36 -0
- package/agent/index.js +28 -109
- package/bin/testdriverai.js +8 -0
- package/debugger/index.html +37 -0
- package/docs/v7/_drafts/architecture.mdx +1 -26
- package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
- package/docs/v7/_drafts/test-recording.mdx +0 -6
- package/docs/v7/api/act.mdx +1 -0
- package/interfaces/cli/commands/init.js +33 -19
- package/interfaces/cli/lib/base.js +24 -0
- package/interfaces/cli.js +8 -1
- package/interfaces/logger.js +8 -3
- package/interfaces/vitest-plugin.mjs +16 -71
- package/lib/sentry.js +343 -0
- package/lib/vitest/hooks.mjs +12 -24
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.js +167 -56
- package/test/testdriver/act.test.mjs +30 -0
- package/test/testdriver/assert.test.mjs +1 -1
- package/test/testdriver/hover-text.test.mjs +1 -1
- package/test/testdriver/setup/testHelpers.mjs +8 -118
- package/tests/example.test.js +33 -0
- package/tests/login.js +28 -0
- package/vitest.config.js +18 -0
- package/vitest.config.mjs +2 -1
- package/agent/lib/cache.js +0 -142
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import crypto from "crypto";
|
|
3
|
-
import fs from "fs";
|
|
4
3
|
import { createRequire } from "module";
|
|
5
|
-
import os from "os";
|
|
6
4
|
import path from "path";
|
|
7
5
|
import { setTestRunInfo } from "./shared-test-state.mjs";
|
|
8
6
|
|
|
@@ -662,76 +660,19 @@ class TestDriverReporter {
|
|
|
662
660
|
|
|
663
661
|
logger.debug(`Calculated duration: ${duration}ms (startTime: ${testCase?.startTime}, now: ${Date.now()})`);
|
|
664
662
|
|
|
665
|
-
// Read test metadata from
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
let testFile = "unknown";
|
|
669
|
-
let testOrder = 0;
|
|
663
|
+
// Read test metadata from Vitest's task.meta (set in test hooks)
|
|
664
|
+
const meta = test.meta();
|
|
665
|
+
logger.debug(`Test meta for ${test.id}:`, meta);
|
|
670
666
|
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
logger.debug(`Looking for test result file with test.id: ${test.id}`);
|
|
678
|
-
logger.debug(`Test result file path: ${testResultFile}`);
|
|
679
|
-
|
|
680
|
-
try {
|
|
681
|
-
if (fs.existsSync(testResultFile)) {
|
|
682
|
-
const testResult = JSON.parse(fs.readFileSync(testResultFile, "utf-8"));
|
|
683
|
-
dashcamUrl = testResult.dashcamUrl || null;
|
|
684
|
-
const platform = testResult.platform || null;
|
|
685
|
-
sessionId = testResult.sessionId || null;
|
|
686
|
-
const absolutePath =
|
|
687
|
-
testResult.testFile ||
|
|
688
|
-
test.file?.filepath ||
|
|
689
|
-
test.file?.name ||
|
|
690
|
-
"unknown";
|
|
691
|
-
// Make path relative to project root
|
|
692
|
-
testFile = pluginState.projectRoot && absolutePath !== "unknown"
|
|
693
|
-
? path.relative(pluginState.projectRoot, absolutePath)
|
|
694
|
-
: absolutePath;
|
|
695
|
-
testOrder =
|
|
696
|
-
testResult.testOrder !== undefined ? testResult.testOrder : 0;
|
|
697
|
-
// Don't override duration from file - use Vitest's result.duration
|
|
698
|
-
// duration is already set above from result.duration
|
|
699
|
-
|
|
700
|
-
// Update test run platform from first test that reports it
|
|
701
|
-
if (platform && !pluginState.detectedPlatform) {
|
|
702
|
-
pluginState.detectedPlatform = platform;
|
|
703
|
-
}
|
|
667
|
+
const dashcamUrl = meta.dashcamUrl || null;
|
|
668
|
+
const sessionId = meta.sessionId || null;
|
|
669
|
+
const platform = meta.platform || null;
|
|
670
|
+
const sandboxId = meta.sandboxId || null;
|
|
671
|
+
let testFile = meta.testFile || "unknown";
|
|
672
|
+
const testOrder = meta.testOrder !== undefined ? meta.testOrder : 0;
|
|
704
673
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
fs.unlinkSync(testResultFile);
|
|
708
|
-
} catch {
|
|
709
|
-
// Ignore cleanup errors
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
logger.debug(`No result file found for test: ${test.id}`);
|
|
713
|
-
// Fallback to test object properties - try multiple sources
|
|
714
|
-
// In Vitest, the file path is on test.module.task.filepath
|
|
715
|
-
const absolutePath =
|
|
716
|
-
test.module?.task?.filepath ||
|
|
717
|
-
test.module?.file?.filepath ||
|
|
718
|
-
test.module?.file?.name ||
|
|
719
|
-
test.file?.filepath ||
|
|
720
|
-
test.file?.name ||
|
|
721
|
-
test.suite?.file?.filepath ||
|
|
722
|
-
test.suite?.file?.name ||
|
|
723
|
-
test.location?.file ||
|
|
724
|
-
"unknown";
|
|
725
|
-
// Make path relative to project root
|
|
726
|
-
testFile = pluginState.projectRoot && absolutePath !== "unknown"
|
|
727
|
-
? path.relative(pluginState.projectRoot, absolutePath)
|
|
728
|
-
: absolutePath;
|
|
729
|
-
logger.debug(`Resolved testFile: ${testFile}`);
|
|
730
|
-
}
|
|
731
|
-
} catch (error) {
|
|
732
|
-
logger.error("Failed to read test result file:", error.message);
|
|
733
|
-
// Fallback to test object properties - try multiple sources
|
|
734
|
-
// In Vitest, the file path is on test.module.task.filepath
|
|
674
|
+
// If testFile not in meta, fallback to test object properties
|
|
675
|
+
if (testFile === "unknown") {
|
|
735
676
|
const absolutePath =
|
|
736
677
|
test.module?.task?.filepath ||
|
|
737
678
|
test.module?.file?.filepath ||
|
|
@@ -742,13 +683,17 @@ class TestDriverReporter {
|
|
|
742
683
|
test.suite?.file?.name ||
|
|
743
684
|
test.location?.file ||
|
|
744
685
|
"unknown";
|
|
745
|
-
// Make path relative to project root
|
|
746
686
|
testFile = pluginState.projectRoot && absolutePath !== "unknown"
|
|
747
687
|
? path.relative(pluginState.projectRoot, absolutePath)
|
|
748
688
|
: absolutePath;
|
|
749
689
|
logger.debug(`Resolved testFile from fallback: ${testFile}`);
|
|
750
690
|
}
|
|
751
691
|
|
|
692
|
+
// Update test run platform from first test that reports it
|
|
693
|
+
if (platform && !pluginState.detectedPlatform) {
|
|
694
|
+
pluginState.detectedPlatform = platform;
|
|
695
|
+
}
|
|
696
|
+
|
|
752
697
|
// Get test run info from environment variables
|
|
753
698
|
const testRunId = process.env.TD_TEST_RUN_ID;
|
|
754
699
|
const token = process.env.TD_TEST_RUN_TOKEN;
|
package/lib/sentry.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry initialization for TestDriver CLI
|
|
3
|
+
*
|
|
4
|
+
* This module initializes Sentry for error tracking and performance monitoring.
|
|
5
|
+
* It should be required at the very beginning of the CLI entry point.
|
|
6
|
+
*
|
|
7
|
+
* Distributed Tracing:
|
|
8
|
+
* The CLI uses session-based trace IDs (MD5 hash of session ID) to link
|
|
9
|
+
* CLI traces with API traces. Call setSessionTraceContext() after establishing
|
|
10
|
+
* a session to ensure all CLI errors/logs are linked to the same trace.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const Sentry = require("@sentry/node");
|
|
14
|
+
const crypto = require("crypto");
|
|
15
|
+
const os = require("os");
|
|
16
|
+
const { version } = require("../package.json");
|
|
17
|
+
|
|
18
|
+
// Store the current session's trace context
|
|
19
|
+
let currentTraceId = null;
|
|
20
|
+
let currentSessionId = null;
|
|
21
|
+
|
|
22
|
+
// Track if we've attached listeners to avoid duplicates
|
|
23
|
+
let emitterAttached = false;
|
|
24
|
+
|
|
25
|
+
const isEnabled = () => {
|
|
26
|
+
|
|
27
|
+
// Disable if explicitly disabled
|
|
28
|
+
if (process.env.TD_TELEMETRY === "false") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (isEnabled()) {
|
|
35
|
+
Sentry.init({
|
|
36
|
+
dsn:
|
|
37
|
+
process.env.SENTRY_DSN ||
|
|
38
|
+
"https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
|
|
39
|
+
environment: process.env.NODE_ENV || "development",
|
|
40
|
+
release: `testdriverai@${version}`,
|
|
41
|
+
sampleRate: 1.0,
|
|
42
|
+
tracesSampleRate: 1.0, // Sample 20% of transactions for performance
|
|
43
|
+
enableLogs: true,
|
|
44
|
+
integrations: [
|
|
45
|
+
Sentry.httpIntegration(),
|
|
46
|
+
Sentry.nodeContextIntegration(),
|
|
47
|
+
],
|
|
48
|
+
// Set initial context
|
|
49
|
+
initialScope: {
|
|
50
|
+
tags: {
|
|
51
|
+
platform: os.platform(),
|
|
52
|
+
arch: os.arch(),
|
|
53
|
+
nodeVersion: process.version,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// Filter out common non-errors
|
|
57
|
+
beforeSend(event, hint) {
|
|
58
|
+
|
|
59
|
+
console.log('sending sentry event', event);
|
|
60
|
+
|
|
61
|
+
const error = hint.originalException;
|
|
62
|
+
|
|
63
|
+
// Don't send user-initiated exits
|
|
64
|
+
if (error && error.message && error.message.includes("User cancelled")) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return event;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set user context for Sentry
|
|
75
|
+
* @param {Object} user - User object with id, email, etc.
|
|
76
|
+
*/
|
|
77
|
+
function setUser(user) {
|
|
78
|
+
if (!isEnabled()) return;
|
|
79
|
+
Sentry.setUser(user);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set additional context
|
|
84
|
+
* @param {string} name - Context name
|
|
85
|
+
* @param {Object} context - Context data
|
|
86
|
+
*/
|
|
87
|
+
function setContext(name, context) {
|
|
88
|
+
if (!isEnabled()) return;
|
|
89
|
+
Sentry.setContext(name, context);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set a tag
|
|
94
|
+
* @param {string} key - Tag key
|
|
95
|
+
* @param {string} value - Tag value
|
|
96
|
+
*/
|
|
97
|
+
function setTag(key, value) {
|
|
98
|
+
if (!isEnabled()) return;
|
|
99
|
+
Sentry.setTag(key, value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Capture an exception
|
|
104
|
+
* @param {Error} error - The error to capture
|
|
105
|
+
* @param {Object} context - Additional context
|
|
106
|
+
*/
|
|
107
|
+
function captureException(error, context = {}) {
|
|
108
|
+
if (!isEnabled()) return;
|
|
109
|
+
|
|
110
|
+
Sentry.withScope((scope) => {
|
|
111
|
+
// Link to session trace if available
|
|
112
|
+
if (currentTraceId && currentSessionId) {
|
|
113
|
+
scope.setTag("session", currentSessionId);
|
|
114
|
+
scope.setContext("trace", {
|
|
115
|
+
trace_id: currentTraceId,
|
|
116
|
+
session_id: currentSessionId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (context.tags) {
|
|
121
|
+
Object.entries(context.tags).forEach(([key, value]) => {
|
|
122
|
+
scope.setTag(key, value);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (context.extra) {
|
|
126
|
+
Object.entries(context.extra).forEach(([key, value]) => {
|
|
127
|
+
scope.setExtra(key, value);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
Sentry.captureException(error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Capture a message
|
|
136
|
+
* @param {string} message - The message to capture
|
|
137
|
+
* @param {string} level - Severity level (info, warning, error)
|
|
138
|
+
*/
|
|
139
|
+
function captureMessage(message, level = "info") {
|
|
140
|
+
if (!isEnabled()) return;
|
|
141
|
+
|
|
142
|
+
Sentry.withScope((scope) => {
|
|
143
|
+
// Link to session trace if available
|
|
144
|
+
if (currentTraceId && currentSessionId) {
|
|
145
|
+
scope.setTag("session", currentSessionId);
|
|
146
|
+
scope.setContext("trace", {
|
|
147
|
+
trace_id: currentTraceId,
|
|
148
|
+
session_id: currentSessionId,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
Sentry.captureMessage(message, level);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set the session trace context for distributed tracing
|
|
157
|
+
* This links CLI errors/logs to the same trace as API calls
|
|
158
|
+
* @param {string} sessionId - The session ID
|
|
159
|
+
*/
|
|
160
|
+
function setSessionTraceContext(sessionId) {
|
|
161
|
+
if (!isEnabled() || !sessionId) return;
|
|
162
|
+
|
|
163
|
+
// Derive trace ID from session ID (same algorithm as API)
|
|
164
|
+
currentTraceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
165
|
+
currentSessionId = sessionId;
|
|
166
|
+
|
|
167
|
+
// Set as global tag so all events include it
|
|
168
|
+
Sentry.setTag("session", sessionId);
|
|
169
|
+
Sentry.setTag("trace_id", currentTraceId);
|
|
170
|
+
|
|
171
|
+
// Try to set propagation context for trace linking (may not be available in all versions)
|
|
172
|
+
try {
|
|
173
|
+
const scope = Sentry.getCurrentScope();
|
|
174
|
+
if (scope && typeof scope.setPropagationContext === 'function') {
|
|
175
|
+
scope.setPropagationContext({
|
|
176
|
+
traceId: currentTraceId,
|
|
177
|
+
spanId: currentTraceId.substring(0, 16),
|
|
178
|
+
sampled: true,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Ignore errors - propagation context may not be supported
|
|
183
|
+
console.log('Could not set propagation context:', e.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Clear the session trace context
|
|
189
|
+
*/
|
|
190
|
+
function clearSessionTraceContext() {
|
|
191
|
+
currentTraceId = null;
|
|
192
|
+
currentSessionId = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the current trace ID (for debugging)
|
|
197
|
+
* @returns {string|null} Current trace ID or null
|
|
198
|
+
*/
|
|
199
|
+
function getTraceId() {
|
|
200
|
+
return currentTraceId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Attach log listeners to an emitter to capture CLI logs as Sentry breadcrumbs
|
|
205
|
+
* @param {EventEmitter} emitter - The event emitter to listen to
|
|
206
|
+
*/
|
|
207
|
+
function attachLogListeners(emitter) {
|
|
208
|
+
|
|
209
|
+
if (!isEnabled() || !emitter || emitterAttached) return;
|
|
210
|
+
|
|
211
|
+
// Check if Sentry.logger is available
|
|
212
|
+
if (!Sentry.logger) {
|
|
213
|
+
console.log('Sentry.logger not available, skipping log listeners');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
emitterAttached = true;
|
|
218
|
+
|
|
219
|
+
// Helper to strip ANSI codes for cleaner logs
|
|
220
|
+
const stripAnsi = (str) => {
|
|
221
|
+
if (typeof str !== 'string') return String(str);
|
|
222
|
+
return str.replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '');
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Helper to get current log attributes with trace context
|
|
226
|
+
const getLogAttributes = (extra = {}) => {
|
|
227
|
+
const attrs = { ...extra };
|
|
228
|
+
if (currentSessionId) {
|
|
229
|
+
attrs['session.id'] = currentSessionId;
|
|
230
|
+
}
|
|
231
|
+
if (currentTraceId) {
|
|
232
|
+
attrs['sentry.trace.trace_id'] = currentTraceId;
|
|
233
|
+
}
|
|
234
|
+
// Get current user from Sentry scope
|
|
235
|
+
try {
|
|
236
|
+
const user = Sentry.getCurrentScope().getUser();
|
|
237
|
+
if (user) {
|
|
238
|
+
if (user.id) attrs['user.id'] = user.id;
|
|
239
|
+
if (user.email) attrs['user.email'] = user.email;
|
|
240
|
+
if (user.username) attrs['user.name'] = user.username;
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
// Ignore errors getting user
|
|
244
|
+
}
|
|
245
|
+
return attrs;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Capture log:log as info logs
|
|
249
|
+
emitter.on('log:log', (message) => {
|
|
250
|
+
Sentry.logger.info(stripAnsi(message), getLogAttributes({ category: 'cli.log' }));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Capture log:warn as warning logs
|
|
254
|
+
emitter.on('log:warn', (message) => {
|
|
255
|
+
Sentry.logger.warn(stripAnsi(message), getLogAttributes({ category: 'cli.warn' }));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Capture log:debug as debug logs (only in verbose mode)
|
|
259
|
+
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
260
|
+
emitter.on('log:debug', (message) => {
|
|
261
|
+
Sentry.logger.debug(stripAnsi(message), getLogAttributes({ category: 'cli.debug' }));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Capture command events
|
|
266
|
+
emitter.on('command:start', (data) => {
|
|
267
|
+
Sentry.logger.info(`Command started: ${data?.command || data?.name || 'unknown'}`, getLogAttributes({
|
|
268
|
+
category: 'cli.command',
|
|
269
|
+
...data,
|
|
270
|
+
}));
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
emitter.on('command:error', (data) => {
|
|
274
|
+
Sentry.logger.error(`Command error: ${data?.message || data?.error || 'unknown'}`, getLogAttributes({
|
|
275
|
+
category: 'cli.command',
|
|
276
|
+
...data,
|
|
277
|
+
}));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Capture step events
|
|
281
|
+
emitter.on('step:start', (data) => {
|
|
282
|
+
Sentry.logger.info(`Step started: ${data?.step || data?.name || 'unknown'}`, getLogAttributes({
|
|
283
|
+
category: 'cli.step',
|
|
284
|
+
}));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
emitter.on('step:error', (data) => {
|
|
288
|
+
Sentry.logger.error(`Step error: ${data?.message || data?.error || 'unknown'}`, getLogAttributes({
|
|
289
|
+
category: 'cli.step',
|
|
290
|
+
...data,
|
|
291
|
+
}));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Capture test events
|
|
295
|
+
emitter.on('test:start', (data) => {
|
|
296
|
+
Sentry.logger.info(`Test started: ${data?.name || 'unknown'}`, getLogAttributes({
|
|
297
|
+
category: 'cli.test',
|
|
298
|
+
}));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
emitter.on('test:error', (data) => {
|
|
302
|
+
Sentry.logger.error(`Test error: ${data?.message || data?.error || 'unknown'}`, getLogAttributes({
|
|
303
|
+
category: 'cli.test',
|
|
304
|
+
...data,
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Start a new transaction for performance monitoring
|
|
311
|
+
* @param {string} name - Transaction name
|
|
312
|
+
* @param {string} op - Operation type
|
|
313
|
+
* @returns {Object} Transaction object
|
|
314
|
+
*/
|
|
315
|
+
function startTransaction(name, op = "cli") {
|
|
316
|
+
if (!isEnabled()) return null;
|
|
317
|
+
return Sentry.startSpan({ name, op });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Flush pending events before process exit
|
|
322
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
323
|
+
*/
|
|
324
|
+
async function flush(timeout = 2000) {
|
|
325
|
+
if (!isEnabled()) return;
|
|
326
|
+
await Sentry.flush(timeout);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
Sentry,
|
|
331
|
+
isEnabled,
|
|
332
|
+
setUser,
|
|
333
|
+
setContext,
|
|
334
|
+
setTag,
|
|
335
|
+
captureException,
|
|
336
|
+
captureMessage,
|
|
337
|
+
setSessionTraceContext,
|
|
338
|
+
clearSessionTraceContext,
|
|
339
|
+
getTraceId,
|
|
340
|
+
attachLogListeners,
|
|
341
|
+
startTransaction,
|
|
342
|
+
flush,
|
|
343
|
+
};
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import chalk from 'chalk';
|
|
19
|
-
import fs from 'fs';
|
|
20
|
-
import os from 'os';
|
|
21
19
|
import path from 'path';
|
|
22
20
|
import { vi } from 'vitest';
|
|
23
21
|
import TestDriverSDK from '../../sdk.js';
|
|
@@ -137,6 +135,7 @@ const lifecycleHandlers = new WeakMap();
|
|
|
137
135
|
* @param {string} [options.apiKey] - TestDriver API key (defaults to process.env.TD_API_KEY)
|
|
138
136
|
* @param {boolean} [options.headless] - Run sandbox in headless mode
|
|
139
137
|
* @param {boolean} [options.newSandbox] - Create new sandbox
|
|
138
|
+
* @param {number} [options.timeout=0] - Sandbox timeout (TTL) in milliseconds. 0 = use provider default (5 min for E2B Linux)
|
|
140
139
|
* @param {boolean} [options.autoConnect=true] - Automatically connect to sandbox
|
|
141
140
|
* @returns {TestDriver} TestDriver client instance
|
|
142
141
|
*
|
|
@@ -242,9 +241,9 @@ export function TestDriver(context, options = {}) {
|
|
|
242
241
|
console.log('');
|
|
243
242
|
console.log('🎥' + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`);
|
|
244
243
|
console.log('');
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
244
|
+
|
|
245
|
+
// Set test metadata directly on the Vitest task context
|
|
246
|
+
// This is the proper way to pass data from test to reporter
|
|
248
247
|
const platform = testdriver.os || 'linux';
|
|
249
248
|
const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
|
|
250
249
|
const projectRoot = process.cwd();
|
|
@@ -252,27 +251,16 @@ export function TestDriver(context, options = {}) {
|
|
|
252
251
|
? path.relative(projectRoot, absolutePath)
|
|
253
252
|
: absolutePath;
|
|
254
253
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// Write test result file
|
|
262
|
-
const testResultFile = path.join(resultsDir, `${testId}.json`);
|
|
263
|
-
const testResult = {
|
|
264
|
-
dashcamUrl: dashcamUrl || null,
|
|
265
|
-
platform,
|
|
266
|
-
testFile,
|
|
267
|
-
testOrder: 0,
|
|
268
|
-
sessionId: testdriver.getSessionId(),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
|
|
254
|
+
// Set metadata on the task for the reporter to read
|
|
255
|
+
context.task.meta.dashcamUrl = dashcamUrl || null;
|
|
256
|
+
context.task.meta.platform = platform;
|
|
257
|
+
context.task.meta.testFile = testFile;
|
|
258
|
+
context.task.meta.testOrder = 0;
|
|
259
|
+
context.task.meta.sessionId = testdriver.getSessionId();
|
|
272
260
|
|
|
273
|
-
// Also register in memory if plugin is available
|
|
261
|
+
// Also register in memory if plugin is available (for cross-process scenarios)
|
|
274
262
|
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
275
|
-
globalThis.__testdriverPlugin.registerDashcamUrl(
|
|
263
|
+
globalThis.__testdriverPlugin.registerDashcamUrl(context.task.id, dashcamUrl, platform);
|
|
276
264
|
}
|
|
277
265
|
} catch (error) {
|
|
278
266
|
// Log more detailed error information for debugging
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.9",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "sdk.js",
|
|
6
6
|
"exports": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@oclif/plugin-help": "^6.2.30",
|
|
53
53
|
"@oclif/plugin-not-found": "^3.2.59",
|
|
54
54
|
"@oclif/plugin-warn-if-update-available": "^3.1.43",
|
|
55
|
+
"@sentry/node": "^9.47.1",
|
|
55
56
|
"@stoplight/yaml-ast-parser": "^0.0.50",
|
|
56
57
|
"ajv": "^8.17.1",
|
|
57
58
|
"arktype": "^2.1.19",
|
|
@@ -97,8 +98,8 @@
|
|
|
97
98
|
"mocha": "^10.8.2",
|
|
98
99
|
"node-addon-api": "^8.0.0",
|
|
99
100
|
"prettier": "3.3.3",
|
|
100
|
-
"testdriverai": "^
|
|
101
|
-
"vitest": "^4.0.
|
|
101
|
+
"testdriverai": "^7.2.3",
|
|
102
|
+
"vitest": "^4.0.16"
|
|
102
103
|
},
|
|
103
104
|
"optionalDependencies": {
|
|
104
105
|
"@esbuild/linux-x64": "^0.21.5"
|
package/sdk-log-formatter.js
CHANGED
|
@@ -878,6 +878,47 @@ class SDKLogFormatter {
|
|
|
878
878
|
|
|
879
879
|
return `\n${parts.join(" ")}\n`;
|
|
880
880
|
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Format act() start message - provides visual scope boundary
|
|
884
|
+
* @param {string} task - The task being executed
|
|
885
|
+
* @returns {string} Formatted act start message
|
|
886
|
+
*/
|
|
887
|
+
formatActStart(task) {
|
|
888
|
+
const parts = [];
|
|
889
|
+
this.addTimestamp(parts);
|
|
890
|
+
parts.push(this.getPrefix("action"));
|
|
891
|
+
parts.push(chalk.bold.cyan("Act"));
|
|
892
|
+
parts.push(chalk.cyan(`"${task}"`));
|
|
893
|
+
return parts.join(" ");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Format act() completion message - provides visual scope boundary
|
|
898
|
+
* @param {number} durationMs - Duration in milliseconds
|
|
899
|
+
* @param {boolean} success - Whether the act completed successfully
|
|
900
|
+
* @param {string} [error] - Error message if failed
|
|
901
|
+
* @returns {string} Formatted act complete message
|
|
902
|
+
*/
|
|
903
|
+
formatActComplete(durationMs, success, error = null) {
|
|
904
|
+
const parts = [];
|
|
905
|
+
this.addTimestamp(parts);
|
|
906
|
+
parts.push(this.getResultPrefix());
|
|
907
|
+
|
|
908
|
+
if (success) {
|
|
909
|
+
parts.push(chalk.green("complete"));
|
|
910
|
+
} else {
|
|
911
|
+
parts.push(chalk.red("failed"));
|
|
912
|
+
if (error) {
|
|
913
|
+
parts.push(chalk.dim("·"));
|
|
914
|
+
parts.push(chalk.red(error));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
parts.push(this.formatDurationColored(durationMs, "default"));
|
|
919
|
+
|
|
920
|
+
return parts.join(" ");
|
|
921
|
+
}
|
|
881
922
|
}
|
|
882
923
|
|
|
883
924
|
// Export singleton instance
|