testdriverai 7.2.3 → 7.2.10
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 +163 -0
- package/.testdriver/last-sandbox +7 -0
- package/agent/events.js +1 -0
- package/agent/index.js +99 -163
- package/agent/lib/sandbox.js +11 -1
- package/agents.md +393 -0
- package/bin/testdriverai.js +8 -0
- package/debug/01-table-initial.png +0 -0
- package/debug/02-after-ai-explore.png +0 -0
- package/debug/02-after-scroll.png +0 -0
- package/debugger/index.html +37 -0
- package/docs/docs.json +93 -125
- package/docs/v7/_drafts/architecture.mdx +1 -26
- package/docs/v7/_drafts/caching.mdx +2 -2
- package/docs/v7/{getting-started → _drafts}/installation.mdx +0 -66
- package/docs/v7/{features/coverage.mdx → _drafts/powerful.mdx} +1 -90
- package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
- package/docs/v7/{features → _drafts}/scalable.mdx +126 -4
- package/docs/v7/_drafts/screenshot.mdx +155 -0
- package/docs/v7/_drafts/test-recording.mdx +0 -6
- package/docs/v7/_drafts/writing-tests.mdx +25 -0
- package/docs/v7/{api/act.mdx → ai.mdx} +28 -27
- package/docs/v7/{api/assert.mdx → assert.mdx} +3 -3
- package/docs/v7/aws-setup.mdx +338 -0
- package/docs/v7/caching.mdx +128 -0
- package/docs/v7/ci-cd.mdx +605 -0
- package/docs/v7/{api/click.mdx → click.mdx} +4 -4
- package/docs/v7/cloud.mdx +120 -0
- package/docs/v7/customizing-devices.mdx +129 -0
- package/docs/v7/{api/doubleClick.mdx → double-click.mdx} +5 -5
- package/docs/v7/enterprise.mdx +135 -0
- package/docs/v7/examples.mdx +5 -0
- package/docs/v7/{api/exec.mdx → exec.mdx} +3 -3
- package/docs/v7/{api/find.mdx → find.mdx} +17 -21
- package/docs/v7/{api/focusApplication.mdx → focus-application.mdx} +3 -3
- package/docs/v7/generating-tests.mdx +32 -0
- package/docs/v7/{api/hover.mdx → hover.mdx} +3 -3
- package/docs/v7/locating-elements.mdx +71 -0
- package/docs/v7/making-assertions.mdx +32 -0
- package/docs/v7/{api/mouseDown.mdx → mouse-down.mdx} +7 -7
- package/docs/v7/{api/mouseUp.mdx → mouse-up.mdx} +8 -8
- package/docs/v7/performing-actions.mdx +51 -0
- package/docs/v7/{api/pressKeys.mdx → press-keys.mdx} +3 -3
- package/docs/v7/quickstart.mdx +162 -0
- package/docs/v7/reusable-code.mdx +240 -0
- package/docs/v7/{api/rightClick.mdx → right-click.mdx} +5 -5
- package/docs/v7/running-tests.mdx +181 -0
- package/docs/v7/{api/scroll.mdx → scroll.mdx} +3 -3
- package/docs/v7/secrets.mdx +115 -0
- package/docs/v7/self-hosted.mdx +66 -0
- package/docs/v7/{api/type.mdx → type.mdx} +3 -3
- package/docs/v7/variables.mdx +111 -0
- package/docs/v7/waiting-for-elements.mdx +66 -0
- package/docs/v7/what-is-testdriver.mdx +54 -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 +81 -81
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.d.ts +22 -9
- package/sdk.js +344 -100
- package/test/manual/reconnect-provision.test.mjs +49 -0
- package/test/manual/reconnect-signin.test.mjs +41 -0
- package/test/testdriver/act.test.mjs +30 -0
- package/test/testdriver/ai.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 -119
- package/test/testdriver/windows-installer.test.mjs +61 -0
- package/tests/example.test.js +33 -0
- package/tests/login.js +28 -0
- package/tests/table-sort-enrollments.test.mjs +72 -0
- package/tests/table-sort-experiment.test.mjs +42 -0
- package/tests/table-sort-setup.test.mjs +59 -0
- package/vitest.config.mjs +3 -1
- package/agent/lib/cache.js +0 -142
- package/docs/v7/api/assertions.mdx +0 -403
- package/docs/v7/features/ai-native.mdx +0 -413
- package/docs/v7/features/application-logs.mdx +0 -353
- package/docs/v7/features/browser-logs.mdx +0 -414
- package/docs/v7/features/cache-management.mdx +0 -402
- package/docs/v7/features/continuous-testing.mdx +0 -346
- package/docs/v7/features/data-driven-testing.mdx +0 -441
- package/docs/v7/features/easy-to-write.mdx +0 -280
- package/docs/v7/features/enterprise.mdx +0 -656
- package/docs/v7/features/fast.mdx +0 -406
- package/docs/v7/features/managed-sandboxes.mdx +0 -384
- package/docs/v7/features/network-monitoring.mdx +0 -568
- package/docs/v7/features/parallel-execution.mdx +0 -381
- package/docs/v7/features/powerful.mdx +0 -531
- package/docs/v7/features/sandbox-customization.mdx +0 -229
- package/docs/v7/features/stable.mdx +0 -473
- package/docs/v7/features/system-performance.mdx +0 -616
- package/docs/v7/features/test-analytics.mdx +0 -373
- package/docs/v7/features/test-cases.mdx +0 -393
- package/docs/v7/features/test-replays.mdx +0 -408
- package/docs/v7/features/test-reports.mdx +0 -308
- package/docs/v7/getting-started/debugging-tests.mdx +0 -382
- package/docs/v7/getting-started/quickstart.mdx +0 -90
- package/docs/v7/getting-started/running-tests.mdx +0 -173
- package/docs/v7/getting-started/setting-up-in-ci.mdx +0 -612
- package/docs/v7/getting-started/writing-tests.mdx +0 -534
- package/docs/v7/overview/what-is-testdriver.mdx +0 -386
- package/docs/v7/presets/chrome-extension.mdx +0 -248
- package/docs/v7/presets/chrome.mdx +0 -300
- package/docs/v7/presets/electron.mdx +0 -460
- package/docs/v7/presets/vscode.mdx +0 -417
- package/docs/v7/presets/webapp.mdx +0 -393
- /package/docs/v7/{commands → _drafts/commands}/assert.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/exec.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/focus-application.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/hover-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/hover-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/if.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/match-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/press-keys.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/remember.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/run.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll-until-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll-until-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/type.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait-for-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait-for-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait.mdx +0 -0
- /package/docs/v7/{getting-started → _drafts}/configuration.mdx +0 -0
- /package/docs/v7/{features → _drafts}/observable.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/linux.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/macos.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/windows.mdx +0 -0
- /package/docs/v7/{playwright.mdx → _drafts/playwright.mdx} +0 -0
- /package/docs/v7/{overview → _drafts}/readme.mdx +0 -0
- /package/docs/v7/{features → _drafts}/reports.mdx +0 -0
- /package/docs/v7/{api/client.mdx → client.mdx} +0 -0
- /package/docs/v7/{api/dashcam.mdx → dashcam.mdx} +0 -0
- /package/docs/v7/{api/elements.mdx → elements.mdx} +0 -0
- /package/docs/v7/{api/sandbox.mdx → sandbox.mdx} +0 -0
|
@@ -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
|
*
|
|
@@ -155,9 +154,16 @@ export function TestDriver(context, options = {}) {
|
|
|
155
154
|
throw new Error('TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })');
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
// Return existing instance if already created for this test
|
|
157
|
+
// Return existing instance if already created for this test AND it's still connected
|
|
158
|
+
// On retry, the previous instance will be disconnected, so we need to create a new one
|
|
159
159
|
if (testDriverInstances.has(context.task)) {
|
|
160
|
-
|
|
160
|
+
const existingInstance = testDriverInstances.get(context.task);
|
|
161
|
+
if (existingInstance.connected) {
|
|
162
|
+
return existingInstance;
|
|
163
|
+
}
|
|
164
|
+
// Instance exists but is disconnected (likely a retry) - remove it and create fresh
|
|
165
|
+
testDriverInstances.delete(context.task);
|
|
166
|
+
lifecycleHandlers.delete(context.task);
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
// Get global plugin options if available
|
|
@@ -195,7 +201,6 @@ export function TestDriver(context, options = {}) {
|
|
|
195
201
|
|
|
196
202
|
if (autoConnect) {
|
|
197
203
|
testdriver.__connectionPromise = (async () => {
|
|
198
|
-
console.log('[testdriver] Connecting to sandbox...');
|
|
199
204
|
if (debugConsoleSpy) {
|
|
200
205
|
console.log('[DEBUG] Before auth - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
|
|
201
206
|
}
|
|
@@ -232,85 +237,80 @@ export function TestDriver(context, options = {}) {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
// Register cleanup handler with dashcam.stop()
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
// Mark as not recording to prevent retries
|
|
286
|
-
if (testdriver._dashcam) {
|
|
287
|
-
testdriver._dashcam.recording = false;
|
|
288
|
-
}
|
|
240
|
+
// We always register a new cleanup handler because on retry we need to clean up the new instance
|
|
241
|
+
const cleanup = async () => {
|
|
242
|
+
// Get the current instance from the WeakMap (not from closure)
|
|
243
|
+
// This ensures we clean up the correct instance on retries
|
|
244
|
+
const currentInstance = testDriverInstances.get(context.task);
|
|
245
|
+
if (!currentInstance) {
|
|
246
|
+
return; // Already cleaned up
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Stop dashcam if it was started - with timeout to prevent hanging
|
|
251
|
+
if (currentInstance._dashcam && currentInstance._dashcam.recording) {
|
|
252
|
+
try {
|
|
253
|
+
const dashcamUrl = await currentInstance.dashcam.stop();
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log('🎥' + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`);
|
|
256
|
+
console.log('');
|
|
257
|
+
|
|
258
|
+
// Set test metadata directly on the Vitest task context
|
|
259
|
+
// This is the proper way to pass data from test to reporter
|
|
260
|
+
const platform = currentInstance.os || 'linux';
|
|
261
|
+
const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
|
|
262
|
+
const projectRoot = process.cwd();
|
|
263
|
+
const testFile = absolutePath !== 'unknown'
|
|
264
|
+
? path.relative(projectRoot, absolutePath)
|
|
265
|
+
: absolutePath;
|
|
266
|
+
|
|
267
|
+
// Set metadata on the task for the reporter to read
|
|
268
|
+
context.task.meta.dashcamUrl = dashcamUrl || null;
|
|
269
|
+
context.task.meta.platform = platform;
|
|
270
|
+
context.task.meta.testFile = testFile;
|
|
271
|
+
context.task.meta.testOrder = 0;
|
|
272
|
+
context.task.meta.sessionId = currentInstance.getSessionId();
|
|
273
|
+
|
|
274
|
+
// Also register in memory if plugin is available (for cross-process scenarios)
|
|
275
|
+
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
276
|
+
globalThis.__testdriverPlugin.registerDashcamUrl(context.task.id, dashcamUrl, platform);
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Log more detailed error information for debugging
|
|
280
|
+
console.error('❌ Failed to stop dashcam:', error.name || error.constructor?.name || 'Error');
|
|
281
|
+
if (error.message) console.error(' Message:', error.message);
|
|
282
|
+
// NotFoundError during cleanup is expected if sandbox already terminated
|
|
283
|
+
if (error.name === 'NotFoundError' || error.responseData?.error === 'NotFoundError') {
|
|
284
|
+
console.log(' ℹ️ Sandbox session already terminated - dashcam stop skipped');
|
|
285
|
+
}
|
|
286
|
+
// Mark as not recording to prevent retries
|
|
287
|
+
if (currentInstance._dashcam) {
|
|
288
|
+
currentInstance._dashcam.recording = false;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
// Clean up console spies
|
|
293
|
-
cleanupConsoleSpy(testdriver);
|
|
294
|
-
|
|
295
|
-
// Wait for connection to finish if it was initiated
|
|
296
|
-
if (testdriver.__connectionPromise) {
|
|
297
|
-
await testdriver.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Disconnect with timeout
|
|
301
|
-
await Promise.race([
|
|
302
|
-
testdriver.disconnect(),
|
|
303
|
-
new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout for disconnect
|
|
304
|
-
]);
|
|
305
|
-
} catch (error) {
|
|
306
|
-
console.error('Error disconnecting client:', error);
|
|
307
291
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
292
|
+
|
|
293
|
+
// Clean up console spies
|
|
294
|
+
cleanupConsoleSpy(currentInstance);
|
|
295
|
+
|
|
296
|
+
// Wait for connection to finish if it was initiated
|
|
297
|
+
if (currentInstance.__connectionPromise) {
|
|
298
|
+
await currentInstance.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Disconnect with timeout
|
|
302
|
+
await Promise.race([
|
|
303
|
+
currentInstance.disconnect(),
|
|
304
|
+
new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout for disconnect
|
|
305
|
+
]);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('Error disconnecting client:', error);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
lifecycleHandlers.set(context.task, cleanup);
|
|
311
|
+
|
|
312
|
+
// Vitest will call this automatically after the test (each retry attempt)
|
|
313
|
+
context.onTestFinished?.(cleanup);
|
|
314
314
|
|
|
315
315
|
return testdriver;
|
|
316
316
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.10",
|
|
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"
|