testdriverai 7.2.62 → 7.2.64
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/agent/index.js +75 -57
- package/agent/lib/sandbox.js +77 -26
- package/docs/v7/_drafts/provision.mdx +61 -0
- package/lib/core/Dashcam.js +22 -15
- package/lib/vitest/setup-aws.mjs +0 -10
- package/package.json +1 -1
- package/sdk.d.ts +21 -3
- package/sdk.js +61 -126
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [7.2.64](https://github.com/testdriverai/testdriverai/compare/v7.2.63...v7.2.64) (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## [7.2.63](https://github.com/testdriverai/testdriverai/compare/v7.2.62...v7.2.63) (2026-01-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
## [7.2.62](https://github.com/testdriverai/testdriverai/compare/v7.2.61...v7.2.62) (2026-01-30)
|
|
2
10
|
|
|
3
11
|
|
package/agent/index.js
CHANGED
|
@@ -110,8 +110,8 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
110
110
|
this.sandbox = createSandbox(this.emitter, this.analytics, this.session);
|
|
111
111
|
|
|
112
112
|
// Attach Sentry log listeners to capture CLI logs as breadcrumbs
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const sentry = require("../lib/sentry");
|
|
114
|
+
sentry.attachLogListeners(this.emitter);
|
|
115
115
|
|
|
116
116
|
// Set the OS for the sandbox to use
|
|
117
117
|
this.sandbox.os = this.sandboxOs;
|
|
@@ -191,7 +191,14 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
191
191
|
// allows us to save the current state, run lifecycle hooks, and track analytics
|
|
192
192
|
async exit(failed = true, shouldSave = false, shouldRunPostrun = false) {
|
|
193
193
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
194
|
-
this.emitter.emit(
|
|
194
|
+
this.emitter.emit(
|
|
195
|
+
events.log.narration,
|
|
196
|
+
formatter.getPrefix("disconnect") +
|
|
197
|
+
" " +
|
|
198
|
+
theme.yellow.bold("Exiting") +
|
|
199
|
+
theme.dim("..."),
|
|
200
|
+
true,
|
|
201
|
+
);
|
|
195
202
|
|
|
196
203
|
// Clean up redraw interval
|
|
197
204
|
if (this.redraw && this.redraw.cleanup) {
|
|
@@ -240,9 +247,7 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
240
247
|
if (errorContext) {
|
|
241
248
|
this.emitter.emit(events.error.fatal, errorContext);
|
|
242
249
|
} else {
|
|
243
|
-
this.emitter.emit(
|
|
244
|
-
events.error.fatal,error,
|
|
245
|
-
);
|
|
250
|
+
this.emitter.emit(events.error.fatal, error);
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
if (skipPostrun) {
|
|
@@ -436,17 +441,15 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
436
441
|
let mousePosition = await this.system.getMousePosition();
|
|
437
442
|
let activeWindow = await this.system.activeWin();
|
|
438
443
|
|
|
439
|
-
let response = await this.sdk.req(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
activeWindow,
|
|
446
|
-
}
|
|
447
|
-
);
|
|
444
|
+
let response = await this.sdk.req("check", {
|
|
445
|
+
tasks: this.tasks,
|
|
446
|
+
images,
|
|
447
|
+
mousePosition,
|
|
448
|
+
activeWindow,
|
|
449
|
+
});
|
|
448
450
|
|
|
449
|
-
|
|
451
|
+
// Use log.log (not markdown.static) so output goes through console spy to sandbox
|
|
452
|
+
this.emitter.emit(events.log.log, response.data);
|
|
450
453
|
|
|
451
454
|
this.lastScreenshot = thisScreenshot;
|
|
452
455
|
|
|
@@ -877,7 +880,7 @@ commands:
|
|
|
877
880
|
currentTask,
|
|
878
881
|
dry = false,
|
|
879
882
|
validateAndLoop = false,
|
|
880
|
-
shouldSave = true
|
|
883
|
+
shouldSave = true,
|
|
881
884
|
) {
|
|
882
885
|
// Check if execution has been stopped
|
|
883
886
|
if (this.stopped) {
|
|
@@ -900,15 +903,12 @@ commands:
|
|
|
900
903
|
|
|
901
904
|
this.lastScreenshot = await this.system.captureScreenBase64();
|
|
902
905
|
|
|
903
|
-
let message = await this.sdk.req(
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
image: this.lastScreenshot,
|
|
910
|
-
}
|
|
911
|
-
);
|
|
906
|
+
let message = await this.sdk.req("input", {
|
|
907
|
+
input: currentTask,
|
|
908
|
+
mousePosition: await this.system.getMousePosition(),
|
|
909
|
+
activeWindow: await this.system.activeWin(),
|
|
910
|
+
image: this.lastScreenshot,
|
|
911
|
+
});
|
|
912
912
|
|
|
913
913
|
this.emitter.emit(events.log.log, message.data);
|
|
914
914
|
|
|
@@ -1625,8 +1625,8 @@ ${regression}
|
|
|
1625
1625
|
|
|
1626
1626
|
// Returns the path to the last sandbox file
|
|
1627
1627
|
getLastSandboxFilePath() {
|
|
1628
|
-
const testdriverDir = path.join(process.cwd(),
|
|
1629
|
-
return path.join(testdriverDir,
|
|
1628
|
+
const testdriverDir = path.join(process.cwd(), ".testdriver");
|
|
1629
|
+
return path.join(testdriverDir, "last-sandbox");
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
1632
|
// Returns full sandbox info from last-sandbox file (no timeout - let API validate)
|
|
@@ -1647,7 +1647,7 @@ ${regression}
|
|
|
1647
1647
|
|
|
1648
1648
|
return {
|
|
1649
1649
|
sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
|
|
1650
|
-
os: sandboxInfo.os ||
|
|
1650
|
+
os: sandboxInfo.os || "linux",
|
|
1651
1651
|
ami: sandboxInfo.ami || null,
|
|
1652
1652
|
instanceType: sandboxInfo.instanceType || null,
|
|
1653
1653
|
timestamp: sandboxInfo.timestamp || null,
|
|
@@ -1662,7 +1662,7 @@ ${regression}
|
|
|
1662
1662
|
// Returns sandboxId to use if AMI/instance type match current requirements
|
|
1663
1663
|
getRecentSandboxId() {
|
|
1664
1664
|
const sandboxInfo = this.getLastSandboxId();
|
|
1665
|
-
|
|
1665
|
+
|
|
1666
1666
|
if (!sandboxInfo || !sandboxInfo.sandboxId) {
|
|
1667
1667
|
return null;
|
|
1668
1668
|
}
|
|
@@ -1689,13 +1689,13 @@ ${regression}
|
|
|
1689
1689
|
saveLastSandboxId(sandboxId, osType = "linux") {
|
|
1690
1690
|
const lastSandboxFile = this.getLastSandboxFilePath();
|
|
1691
1691
|
const testdriverDir = path.dirname(lastSandboxFile);
|
|
1692
|
-
|
|
1692
|
+
|
|
1693
1693
|
try {
|
|
1694
1694
|
// Ensure .testdriver directory exists
|
|
1695
1695
|
if (!fs.existsSync(testdriverDir)) {
|
|
1696
1696
|
fs.mkdirSync(testdriverDir, { recursive: true });
|
|
1697
1697
|
}
|
|
1698
|
-
|
|
1698
|
+
|
|
1699
1699
|
const sandboxInfo = {
|
|
1700
1700
|
sandboxId: sandboxId,
|
|
1701
1701
|
os: osType,
|
|
@@ -1756,15 +1756,9 @@ ${regression}
|
|
|
1756
1756
|
// Also clear this.sandboxId to prevent reconnection attempts
|
|
1757
1757
|
this.sandboxId = null;
|
|
1758
1758
|
if (!this.config.CI && !this.newSandbox) {
|
|
1759
|
-
this.emitter.emit(
|
|
1760
|
-
events.log.log,
|
|
1761
|
-
theme.dim("--`new` flag detected, will create a new sandbox"),
|
|
1762
|
-
);
|
|
1759
|
+
this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
|
|
1763
1760
|
} else if (this.newSandbox) {
|
|
1764
|
-
this.emitter.emit(
|
|
1765
|
-
events.log.log,
|
|
1766
|
-
theme.dim("--new-sandbox flag detected, will create a new sandbox"),
|
|
1767
|
-
);
|
|
1761
|
+
this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
|
|
1768
1762
|
}
|
|
1769
1763
|
}
|
|
1770
1764
|
|
|
@@ -1801,7 +1795,7 @@ ${regression}
|
|
|
1801
1795
|
theme.dim(`using recent sandbox: ${recentId}`),
|
|
1802
1796
|
);
|
|
1803
1797
|
this.sandboxId = recentId;
|
|
1804
|
-
|
|
1798
|
+
|
|
1805
1799
|
try {
|
|
1806
1800
|
let instance = await this.connectToSandboxDirect(
|
|
1807
1801
|
this.sandboxId,
|
|
@@ -1849,13 +1843,17 @@ ${regression}
|
|
|
1849
1843
|
console.error("Failed to reconnect to sandbox:", error);
|
|
1850
1844
|
}
|
|
1851
1845
|
}
|
|
1852
|
-
|
|
1846
|
+
|
|
1853
1847
|
// Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
|
|
1854
1848
|
if (!this.instance) {
|
|
1855
1849
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
1856
1850
|
this.emitter.emit(
|
|
1857
1851
|
events.log.narration,
|
|
1858
|
-
formatter.getPrefix("connect") +
|
|
1852
|
+
formatter.getPrefix("connect") +
|
|
1853
|
+
" " +
|
|
1854
|
+
theme.green.bold("Creating") +
|
|
1855
|
+
" " +
|
|
1856
|
+
theme.cyan(`new sandbox...`),
|
|
1859
1857
|
);
|
|
1860
1858
|
// We don't have resiliency/retries baked in, so let's at least give it 1 attempt
|
|
1861
1859
|
// to see if that fixes the issue.
|
|
@@ -1868,11 +1866,12 @@ ${regression}
|
|
|
1868
1866
|
});
|
|
1869
1867
|
|
|
1870
1868
|
// Extract the sandbox ID from the newly created sandbox
|
|
1871
|
-
this.sandboxId =
|
|
1872
|
-
|
|
1869
|
+
this.sandboxId =
|
|
1870
|
+
newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
|
|
1871
|
+
|
|
1873
1872
|
// Use the configured sandbox OS type
|
|
1874
1873
|
this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
|
|
1875
|
-
|
|
1874
|
+
|
|
1876
1875
|
let instance = await this.connectToSandboxDirect(
|
|
1877
1876
|
this.sandboxId,
|
|
1878
1877
|
true, // always persist by default
|
|
@@ -2001,7 +2000,6 @@ ${regression}
|
|
|
2001
2000
|
}
|
|
2002
2001
|
|
|
2003
2002
|
async renderSandbox(instance, headless = false) {
|
|
2004
|
-
|
|
2005
2003
|
if (!headless) {
|
|
2006
2004
|
let url;
|
|
2007
2005
|
|
|
@@ -2055,7 +2053,13 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2055
2053
|
}
|
|
2056
2054
|
|
|
2057
2055
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
2058
|
-
this.emitter.emit(
|
|
2056
|
+
this.emitter.emit(
|
|
2057
|
+
events.log.narration,
|
|
2058
|
+
formatter.getPrefix("connect") +
|
|
2059
|
+
" " +
|
|
2060
|
+
theme.green.bold("Authenticating") +
|
|
2061
|
+
theme.dim("..."),
|
|
2062
|
+
);
|
|
2059
2063
|
let ableToAuth = await this.sandbox.auth(this.config.TD_API_KEY);
|
|
2060
2064
|
|
|
2061
2065
|
if (!ableToAuth) {
|
|
@@ -2069,7 +2073,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2069
2073
|
|
|
2070
2074
|
async connectToSandboxDirect(sandboxId, persist = false, keepAlive = null) {
|
|
2071
2075
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
2072
|
-
this.emitter.emit(
|
|
2076
|
+
this.emitter.emit(
|
|
2077
|
+
events.log.narration,
|
|
2078
|
+
formatter.getPrefix("connect") +
|
|
2079
|
+
" " +
|
|
2080
|
+
theme.green.bold("Connecting") +
|
|
2081
|
+
" " +
|
|
2082
|
+
theme.cyan(`to sandbox...`),
|
|
2083
|
+
);
|
|
2073
2084
|
let reply = await this.sandbox.connect(sandboxId, persist, keepAlive);
|
|
2074
2085
|
|
|
2075
2086
|
// reply includes { success, url, sandbox: {...} }
|
|
@@ -2111,15 +2122,18 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2111
2122
|
let response = await this.sandbox.send(sandboxConfig, 60000 * 8);
|
|
2112
2123
|
|
|
2113
2124
|
// Check if queued (all slots in use)
|
|
2114
|
-
if (response.type ===
|
|
2125
|
+
if (response.type === "create.queued") {
|
|
2115
2126
|
this.emitter.emit(
|
|
2116
2127
|
events.log.narration,
|
|
2117
|
-
formatter.getPrefix("queue") +
|
|
2118
|
-
|
|
2128
|
+
formatter.getPrefix("queue") +
|
|
2129
|
+
" " +
|
|
2130
|
+
theme.yellow.bold("Waiting") +
|
|
2131
|
+
" " +
|
|
2132
|
+
theme.dim(response.message),
|
|
2119
2133
|
);
|
|
2120
2134
|
|
|
2121
2135
|
// Wait then retry
|
|
2122
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
2136
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
2123
2137
|
continue;
|
|
2124
2138
|
}
|
|
2125
2139
|
|
|
@@ -2138,10 +2152,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2138
2152
|
// should be start of new session
|
|
2139
2153
|
// If sandbox is connected, get system info; otherwise pass empty objects
|
|
2140
2154
|
const isSandboxConnected = this.sandbox.apiSocketConnected;
|
|
2141
|
-
|
|
2155
|
+
|
|
2142
2156
|
const sessionRes = await this.sdk.req("session/start", {
|
|
2143
|
-
systemInformationOsInfo: isSandboxConnected
|
|
2144
|
-
|
|
2157
|
+
systemInformationOsInfo: isSandboxConnected
|
|
2158
|
+
? await this.system.getSystemInformationOsInfo()
|
|
2159
|
+
: {},
|
|
2160
|
+
mousePosition: isSandboxConnected
|
|
2161
|
+
? await this.system.getMousePosition()
|
|
2162
|
+
: {},
|
|
2145
2163
|
activeWindow: isSandboxConnected ? await this.system.activeWin() : {},
|
|
2146
2164
|
});
|
|
2147
2165
|
|
|
@@ -2152,7 +2170,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2152
2170
|
}
|
|
2153
2171
|
|
|
2154
2172
|
this.session.set(sessionRes.data.id);
|
|
2155
|
-
|
|
2173
|
+
|
|
2156
2174
|
// Set Sentry session trace context for distributed tracing
|
|
2157
2175
|
// This links CLI errors/logs to the same trace as API calls
|
|
2158
2176
|
try {
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -10,14 +10,14 @@ const { events } = require("../events");
|
|
|
10
10
|
*/
|
|
11
11
|
function getSentryTraceHeaders(sessionId) {
|
|
12
12
|
if (!sessionId) return {};
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
// Same logic as API: derive trace ID from session ID
|
|
15
|
-
const traceId = crypto.createHash(
|
|
16
|
-
const spanId = crypto.randomBytes(8).toString(
|
|
17
|
-
|
|
15
|
+
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
16
|
+
const spanId = crypto.randomBytes(8).toString("hex");
|
|
17
|
+
|
|
18
18
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
"sentry-trace": `${traceId}-${spanId}-1`,
|
|
20
|
+
baggage: `sentry-trace_id=${traceId},sentry-sample_rate=1.0,sentry-sampled=true`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -36,6 +36,11 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
36
36
|
this.os = null; // Store OS value to send with every message
|
|
37
37
|
this.sessionInstance = sessionInstance; // Store session instance to include in messages
|
|
38
38
|
this.traceId = null; // Sentry trace ID for debugging
|
|
39
|
+
this.reconnectAttempts = 0;
|
|
40
|
+
this.maxReconnectAttempts = 5;
|
|
41
|
+
this.intentionalDisconnect = false;
|
|
42
|
+
this.apiRoot = null;
|
|
43
|
+
this.apiKey = null;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -90,12 +95,16 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
90
95
|
});
|
|
91
96
|
|
|
92
97
|
const requestId = message.requestId;
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
// Set up timeout to prevent hanging requests
|
|
95
100
|
const timeoutId = setTimeout(() => {
|
|
96
101
|
if (this.ps[requestId]) {
|
|
97
102
|
delete this.ps[requestId];
|
|
98
|
-
rejectPromise(
|
|
103
|
+
rejectPromise(
|
|
104
|
+
new Error(
|
|
105
|
+
`Sandbox message '${message.type}' timed out after ${timeout}ms`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
99
108
|
}
|
|
100
109
|
}, timeout);
|
|
101
110
|
|
|
@@ -115,12 +124,13 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
115
124
|
|
|
116
125
|
return p;
|
|
117
126
|
}
|
|
118
|
-
|
|
127
|
+
|
|
119
128
|
// Return a rejected promise if socket is not available
|
|
120
|
-
return Promise.reject(new Error(
|
|
129
|
+
return Promise.reject(new Error("Sandbox socket not connected"));
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
async auth(apiKey) {
|
|
133
|
+
this.apiKey = apiKey;
|
|
124
134
|
let reply = await this.send({
|
|
125
135
|
type: "authenticate",
|
|
126
136
|
apiKey,
|
|
@@ -128,15 +138,17 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
128
138
|
|
|
129
139
|
if (reply.success) {
|
|
130
140
|
this.authenticated = true;
|
|
131
|
-
|
|
141
|
+
|
|
132
142
|
// Log and store the Sentry trace ID for debugging
|
|
133
143
|
if (reply.traceId) {
|
|
134
144
|
this.traceId = reply.traceId;
|
|
135
|
-
console.log(
|
|
145
|
+
console.log("");
|
|
136
146
|
console.log(`🔗 View Trace:`);
|
|
137
|
-
console.log(
|
|
147
|
+
console.log(
|
|
148
|
+
`https://testdriver.sentry.io/explore/traces/trace/${reply.traceId}`,
|
|
149
|
+
);
|
|
138
150
|
}
|
|
139
|
-
|
|
151
|
+
|
|
140
152
|
emitter.emit(events.sandbox.authenticated, { traceId: reply.traceId });
|
|
141
153
|
return true;
|
|
142
154
|
}
|
|
@@ -161,24 +173,58 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
161
173
|
}
|
|
162
174
|
}
|
|
163
175
|
|
|
176
|
+
async handleConnectionLoss() {
|
|
177
|
+
if (this.intentionalDisconnect) return;
|
|
178
|
+
|
|
179
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
180
|
+
const errorMsg =
|
|
181
|
+
"Unable to reconnect to TestDriver sandbox after multiple attempts. Please check your internet connection.";
|
|
182
|
+
emitter.emit(events.error.sandbox, errorMsg);
|
|
183
|
+
console.error(errorMsg);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.reconnectAttempts++;
|
|
188
|
+
const delay = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 30000);
|
|
189
|
+
|
|
190
|
+
console.log(
|
|
191
|
+
`[Sandbox] Connection lost. Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
setTimeout(async () => {
|
|
195
|
+
try {
|
|
196
|
+
await this.boot(this.apiRoot);
|
|
197
|
+
if (this.apiKey) {
|
|
198
|
+
await this.auth(this.apiKey);
|
|
199
|
+
}
|
|
200
|
+
console.log("[Sandbox] Reconnected successfully.");
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Ignore error here as the boot's error handler will trigger handleConnectionLoss again
|
|
203
|
+
}
|
|
204
|
+
}, delay);
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
async boot(apiRoot) {
|
|
208
|
+
if (apiRoot) this.apiRoot = apiRoot;
|
|
165
209
|
return new Promise((resolve, reject) => {
|
|
166
210
|
// Get session ID for Sentry trace headers
|
|
167
211
|
const sessionId = this.sessionInstance?.get();
|
|
168
|
-
|
|
212
|
+
|
|
169
213
|
if (!sessionId) {
|
|
170
|
-
console.warn(
|
|
214
|
+
console.warn(
|
|
215
|
+
"[Sandbox] No session ID available at boot time - Sentry tracing will not be available",
|
|
216
|
+
);
|
|
171
217
|
}
|
|
172
|
-
|
|
218
|
+
|
|
173
219
|
const sentryHeaders = getSentryTraceHeaders(sessionId);
|
|
174
220
|
|
|
175
221
|
// Build WebSocket URL with Sentry trace headers as query params
|
|
176
222
|
const wsUrl = new URL(apiRoot.replace("https://", "wss://"));
|
|
177
|
-
if (sentryHeaders[
|
|
178
|
-
wsUrl.searchParams.set(
|
|
223
|
+
if (sentryHeaders["sentry-trace"]) {
|
|
224
|
+
wsUrl.searchParams.set("sentry-trace", sentryHeaders["sentry-trace"]);
|
|
179
225
|
}
|
|
180
|
-
if (sentryHeaders[
|
|
181
|
-
wsUrl.searchParams.set(
|
|
226
|
+
if (sentryHeaders["baggage"]) {
|
|
227
|
+
wsUrl.searchParams.set("baggage", sentryHeaders["baggage"]);
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
this.socket = new WebSocket(wsUrl.toString());
|
|
@@ -189,6 +235,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
189
235
|
// Emit a clear error event for API key issues
|
|
190
236
|
reject();
|
|
191
237
|
this.apiSocketConnected = false;
|
|
238
|
+
this.handleConnectionLoss();
|
|
192
239
|
});
|
|
193
240
|
|
|
194
241
|
this.socket.on("error", (err) => {
|
|
@@ -197,10 +244,13 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
197
244
|
clearInterval(this.heartbeat);
|
|
198
245
|
emitter.emit(events.error.sandbox, err);
|
|
199
246
|
this.apiSocketConnected = false;
|
|
200
|
-
|
|
247
|
+
this.handleConnectionLoss();
|
|
248
|
+
// We don't throw here to avoid crashing the process, let reconnection handle it
|
|
249
|
+
reject(err);
|
|
201
250
|
});
|
|
202
251
|
|
|
203
252
|
this.socket.on("open", async () => {
|
|
253
|
+
this.reconnectAttempts = 0;
|
|
204
254
|
this.apiSocketConnected = true;
|
|
205
255
|
|
|
206
256
|
this.heartbeat = setInterval(() => {
|
|
@@ -216,7 +266,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
216
266
|
let message = JSON.parse(raw);
|
|
217
267
|
|
|
218
268
|
// Handle progress messages (no requestId needed)
|
|
219
|
-
if (message.type ===
|
|
269
|
+
if (message.type === "sandbox.progress") {
|
|
220
270
|
emitter.emit(events.sandbox.progress, {
|
|
221
271
|
step: message.step,
|
|
222
272
|
message: message.message,
|
|
@@ -250,11 +300,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
250
300
|
* Close the WebSocket connection and clean up resources
|
|
251
301
|
*/
|
|
252
302
|
close() {
|
|
303
|
+
this.intentionalDisconnect = true;
|
|
253
304
|
if (this.heartbeat) {
|
|
254
305
|
clearInterval(this.heartbeat);
|
|
255
306
|
this.heartbeat = null;
|
|
256
307
|
}
|
|
257
|
-
|
|
308
|
+
|
|
258
309
|
if (this.socket) {
|
|
259
310
|
try {
|
|
260
311
|
this.socket.close();
|
|
@@ -263,12 +314,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
263
314
|
}
|
|
264
315
|
this.socket = null;
|
|
265
316
|
}
|
|
266
|
-
|
|
317
|
+
|
|
267
318
|
this.apiSocketConnected = false;
|
|
268
319
|
this.instanceSocketConnected = false;
|
|
269
320
|
this.authenticated = false;
|
|
270
321
|
this.instance = null;
|
|
271
|
-
|
|
322
|
+
|
|
272
323
|
// Silently clear pending promises without rejecting
|
|
273
324
|
// (rejecting causes unhandled promise rejections during cleanup)
|
|
274
325
|
this.ps = {};
|
|
@@ -27,6 +27,7 @@ describe('My Test Suite', () => {
|
|
|
27
27
|
- `provision.vscode()` - Launch VS Code with optional extensions
|
|
28
28
|
- `provision.installer()` - Download and install applications
|
|
29
29
|
- `provision.electron()` - Launch Electron applications
|
|
30
|
+
- `provision.dashcam()` - Initialize Dashcam recording with logging
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -248,6 +249,66 @@ it('should launch Electron app', async (context) => {
|
|
|
248
249
|
|
|
249
250
|
---
|
|
250
251
|
|
|
252
|
+
## provision.dashcam()
|
|
253
|
+
|
|
254
|
+
Initialize Dashcam recording with logging. Use this when you want to start Dashcam recording without launching a specific application (e.g., for custom application launches or testing scenarios).
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
await testdriver.provision.dashcam({
|
|
258
|
+
logPath: '/tmp/myapp.log',
|
|
259
|
+
logName: 'My Application Log',
|
|
260
|
+
title: 'Custom Test Recording'
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Options:**
|
|
265
|
+
| Option | Type | Default | Description |
|
|
266
|
+
|--------|------|---------|-------------|
|
|
267
|
+
| `logPath` | string | auto-generated | Path to log file to track |
|
|
268
|
+
| `logName` | string | `'TestDriver Log'` | Display name for the log |
|
|
269
|
+
| `webLogs` | boolean | `true` | Enable web log tracking |
|
|
270
|
+
| `title` | string | - | Custom title for the recording |
|
|
271
|
+
|
|
272
|
+
**Example - Basic Recording:**
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
it('should record a custom application test', async (context) => {
|
|
276
|
+
const testdriver = TestDriver(context, ());
|
|
277
|
+
|
|
278
|
+
// Start Dashcam recording
|
|
279
|
+
await testdriver.provision.dashcam();
|
|
280
|
+
|
|
281
|
+
// Launch your custom application
|
|
282
|
+
await testdriver.exec('sh', './my-custom-app.sh &', 10000);
|
|
283
|
+
|
|
284
|
+
// Interact with the application
|
|
285
|
+
await testdriver.find('main window').click();
|
|
286
|
+
|
|
287
|
+
const result = await testdriver.assert('Application is running');
|
|
288
|
+
expect(result).toBeTruthy();
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Example - Custom Log File:**
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
it('should record with custom log file', async (context) => {
|
|
296
|
+
const testdriver = TestDriver(context, ());
|
|
297
|
+
|
|
298
|
+
// Start Dashcam with custom log tracking
|
|
299
|
+
await testdriver.provision.dashcam({
|
|
300
|
+
logPath: '/var/log/myapp/application.log',
|
|
301
|
+
logName: 'My App Logs',
|
|
302
|
+
title: 'Custom App Test'
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Your test code here
|
|
306
|
+
await testdriver.exec('sh', 'myapp --start', 30000);
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
251
312
|
## How Provision Methods Work
|
|
252
313
|
|
|
253
314
|
When you call a provision method:
|
package/lib/core/Dashcam.js
CHANGED
|
@@ -112,7 +112,7 @@ class Dashcam {
|
|
|
112
112
|
shell,
|
|
113
113
|
"npm prefix -g",
|
|
114
114
|
40000,
|
|
115
|
-
false,
|
|
115
|
+
process.env.DEBUG == "true" ? false : true,
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
if (this.client.os === "windows") {
|
|
@@ -140,7 +140,7 @@ class Dashcam {
|
|
|
140
140
|
shell,
|
|
141
141
|
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" auth ${key}`,
|
|
142
142
|
120000,
|
|
143
|
-
false,
|
|
143
|
+
process.env.DEBUG == "true" ? false : true,
|
|
144
144
|
);
|
|
145
145
|
this._log("debug", "Auth output:", authOutput);
|
|
146
146
|
} else {
|
|
@@ -149,7 +149,7 @@ class Dashcam {
|
|
|
149
149
|
shell,
|
|
150
150
|
`TD_API_ROOT="${apiRoot}" dashcam auth ${key}`,
|
|
151
151
|
120000,
|
|
152
|
-
false,
|
|
152
|
+
process.env.DEBUG == "true" ? false : true,
|
|
153
153
|
);
|
|
154
154
|
this._log("debug", "Auth output:", authOutput);
|
|
155
155
|
}
|
|
@@ -173,7 +173,7 @@ class Dashcam {
|
|
|
173
173
|
shell,
|
|
174
174
|
`New-Item -ItemType File -Path "${path}" -Force`,
|
|
175
175
|
10000,
|
|
176
|
-
false,
|
|
176
|
+
process.env.DEBUG == "true" ? false : true,
|
|
177
177
|
);
|
|
178
178
|
this._log("debug", "Create log file output:", createFileOutput);
|
|
179
179
|
|
|
@@ -182,19 +182,24 @@ class Dashcam {
|
|
|
182
182
|
shell,
|
|
183
183
|
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=file --file="${path}" --name="${name}"`,
|
|
184
184
|
120000,
|
|
185
|
-
false,
|
|
185
|
+
process.env.DEBUG == "true" ? false : true,
|
|
186
186
|
);
|
|
187
187
|
this._log("debug", "Add log tracking output:", addLogOutput);
|
|
188
188
|
} else {
|
|
189
189
|
// Create log file
|
|
190
|
-
await this.client.exec(
|
|
190
|
+
await this.client.exec(
|
|
191
|
+
shell,
|
|
192
|
+
`touch ${path}`,
|
|
193
|
+
10000,
|
|
194
|
+
process.env.DEBUG == "true" ? false : true,
|
|
195
|
+
);
|
|
191
196
|
|
|
192
197
|
// Add log tracking with TD_API_ROOT
|
|
193
198
|
const addLogOutput = await this.client.exec(
|
|
194
199
|
shell,
|
|
195
200
|
`TD_API_ROOT="${apiRoot}" dashcam logs --add --type=file --file="${path}" --name="${name}"`,
|
|
196
201
|
10000,
|
|
197
|
-
false,
|
|
202
|
+
process.env.DEBUG == "true" ? false : true,
|
|
198
203
|
);
|
|
199
204
|
this._log("debug", "Add log tracking output:", addLogOutput);
|
|
200
205
|
}
|
|
@@ -216,7 +221,7 @@ class Dashcam {
|
|
|
216
221
|
shell,
|
|
217
222
|
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=application --application="${application}" --name="${name}"`,
|
|
218
223
|
120000,
|
|
219
|
-
false,
|
|
224
|
+
process.env.DEBUG == "true" ? false : true,
|
|
220
225
|
);
|
|
221
226
|
this._log("debug", "Add application log tracking output:", addLogOutput);
|
|
222
227
|
} else {
|
|
@@ -224,7 +229,7 @@ class Dashcam {
|
|
|
224
229
|
shell,
|
|
225
230
|
`TD_API_ROOT="${apiRoot}" dashcam logs --add --type=application --application="${application}" --name="${name}"`,
|
|
226
231
|
10000,
|
|
227
|
-
false,
|
|
232
|
+
process.env.DEBUG == "true" ? false : true,
|
|
228
233
|
);
|
|
229
234
|
this._log("debug", "Add application log tracking output:", addLogOutput);
|
|
230
235
|
}
|
|
@@ -246,7 +251,7 @@ class Dashcam {
|
|
|
246
251
|
shell,
|
|
247
252
|
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=web --pattern="${pattern}" --name="${name}"`,
|
|
248
253
|
120000,
|
|
249
|
-
false,
|
|
254
|
+
process.env.DEBUG == "true" ? false : true,
|
|
250
255
|
);
|
|
251
256
|
this._log("debug", "Add web log tracking output:", addLogOutput);
|
|
252
257
|
} else {
|
|
@@ -254,7 +259,7 @@ class Dashcam {
|
|
|
254
259
|
shell,
|
|
255
260
|
`TD_API_ROOT="${apiRoot}" dashcam logs --add --type=web --pattern="${pattern}" --name="${name}"`,
|
|
256
261
|
10000,
|
|
257
|
-
false,
|
|
262
|
+
process.env.DEBUG == "true" ? false : true,
|
|
258
263
|
);
|
|
259
264
|
this._log("debug", "Add web log tracking output:", addLogOutput);
|
|
260
265
|
}
|
|
@@ -310,7 +315,7 @@ class Dashcam {
|
|
|
310
315
|
shell,
|
|
311
316
|
startScript,
|
|
312
317
|
10000,
|
|
313
|
-
false,
|
|
318
|
+
process.env.DEBUG == "true" ? false : true,
|
|
314
319
|
);
|
|
315
320
|
this._log("debug", "Start-Process output:", startOutput);
|
|
316
321
|
|
|
@@ -320,7 +325,7 @@ class Dashcam {
|
|
|
320
325
|
shell,
|
|
321
326
|
`Get-Content "${outputFile}" -ErrorAction SilentlyContinue`,
|
|
322
327
|
10000,
|
|
323
|
-
false,
|
|
328
|
+
process.env.DEBUG == "true" ? false : true,
|
|
324
329
|
);
|
|
325
330
|
this._log("debug", "Dashcam record output:", dashcamOutput);
|
|
326
331
|
|
|
@@ -339,6 +344,8 @@ class Dashcam {
|
|
|
339
344
|
await this.client.exec(
|
|
340
345
|
shell,
|
|
341
346
|
`TD_API_ROOT="${apiRoot}" dashcam record${titleArg} >/dev/null 2>&1 &`,
|
|
347
|
+
10000,
|
|
348
|
+
process.env.DEBUG == "true" ? false : true,
|
|
342
349
|
);
|
|
343
350
|
this._log("debug", "Dashcam recording started");
|
|
344
351
|
}
|
|
@@ -382,7 +389,7 @@ class Dashcam {
|
|
|
382
389
|
shell,
|
|
383
390
|
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`,
|
|
384
391
|
300000,
|
|
385
|
-
process.env.DEBUG == "true" ?
|
|
392
|
+
process.env.DEBUG == "true" ? false : true,
|
|
386
393
|
);
|
|
387
394
|
this._log("debug", "Dashcam stop command output:", output);
|
|
388
395
|
} else {
|
|
@@ -392,7 +399,7 @@ class Dashcam {
|
|
|
392
399
|
shell,
|
|
393
400
|
`TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`,
|
|
394
401
|
300000,
|
|
395
|
-
process.env.DEBUG == "true" ?
|
|
402
|
+
process.env.DEBUG == "true" ? false : true,
|
|
396
403
|
);
|
|
397
404
|
this._log("debug", "Dashcam command output:", output);
|
|
398
405
|
}
|
package/lib/vitest/setup-aws.mjs
CHANGED
|
@@ -114,12 +114,10 @@ function cleanupAllInstances() {
|
|
|
114
114
|
// Register cleanup handlers for various exit scenarios
|
|
115
115
|
process.on("exit", cleanupAllInstances);
|
|
116
116
|
process.on("SIGINT", () => {
|
|
117
|
-
console.log("\n[TestDriver] Received SIGINT, cleaning up instances...");
|
|
118
117
|
cleanupAllInstances();
|
|
119
118
|
// Don't call process.exit here - let the signal handler do its job
|
|
120
119
|
});
|
|
121
120
|
process.on("SIGTERM", () => {
|
|
122
|
-
console.log("\n[TestDriver] Received SIGTERM, cleaning up instances...");
|
|
123
121
|
cleanupAllInstances();
|
|
124
122
|
// Don't call process.exit here - let the signal handler do its job
|
|
125
123
|
});
|
|
@@ -142,14 +140,6 @@ beforeEach(async (context) => {
|
|
|
142
140
|
return;
|
|
143
141
|
}
|
|
144
142
|
|
|
145
|
-
// Verify required parameters are available
|
|
146
|
-
if (process.env.TD_OS === "windows") {
|
|
147
|
-
console.log(
|
|
148
|
-
"[TestDriver] startup check: TWOCAPTCHA_API_KEY is " +
|
|
149
|
-
(process.env.TWOCAPTCHA_API_KEY ? "REQUIRED" : "MISSING"),
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
143
|
if (!process.env.AWS_LAUNCH_TEMPLATE_ID || !process.env.AMI_ID) {
|
|
154
144
|
throw new Error(
|
|
155
145
|
"[TestDriver] TD_OS=windows requires AWS_LAUNCH_TEMPLATE_ID and AMI_ID environment variables",
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -769,6 +769,18 @@ export interface ProvisionElectronOptions {
|
|
|
769
769
|
args?: string[];
|
|
770
770
|
}
|
|
771
771
|
|
|
772
|
+
/** Options for provision.dashcam */
|
|
773
|
+
export interface ProvisionDashcamOptions {
|
|
774
|
+
/** Path to log file (auto-generated if not provided) */
|
|
775
|
+
logPath?: string;
|
|
776
|
+
/** Display name for the log (default: 'TestDriver Log') */
|
|
777
|
+
logName?: string;
|
|
778
|
+
/** Enable web log tracking (default: true) */
|
|
779
|
+
webLogs?: boolean;
|
|
780
|
+
/** Custom title for the recording */
|
|
781
|
+
title?: string;
|
|
782
|
+
}
|
|
783
|
+
|
|
772
784
|
/** Provision API for launching applications */
|
|
773
785
|
export interface ProvisionAPI {
|
|
774
786
|
/**
|
|
@@ -801,6 +813,12 @@ export interface ProvisionAPI {
|
|
|
801
813
|
* @param options - Electron launch options
|
|
802
814
|
*/
|
|
803
815
|
electron(options: ProvisionElectronOptions): Promise<void>;
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Initialize Dashcam recording with logging
|
|
819
|
+
* @param options - Dashcam options
|
|
820
|
+
*/
|
|
821
|
+
dashcam(options?: ProvisionDashcamOptions): Promise<void>;
|
|
804
822
|
}
|
|
805
823
|
|
|
806
824
|
/** Dashcam API for screen recording */
|
|
@@ -1229,6 +1247,7 @@ export default class TestDriverSDK {
|
|
|
1229
1247
|
// AI Methods (Exploratory Loop)
|
|
1230
1248
|
|
|
1231
1249
|
/**
|
|
1250
|
+
* @deprecated Use ai() instead
|
|
1232
1251
|
* Execute a natural language task using AI
|
|
1233
1252
|
* This is the SDK equivalent of the CLI's exploratory loop
|
|
1234
1253
|
*
|
|
@@ -1238,11 +1257,11 @@ export default class TestDriverSDK {
|
|
|
1238
1257
|
*
|
|
1239
1258
|
* @example
|
|
1240
1259
|
* // Simple execution
|
|
1241
|
-
* await client.
|
|
1260
|
+
* await client.ai('Click the submit button');
|
|
1242
1261
|
*
|
|
1243
1262
|
* @example
|
|
1244
1263
|
* // With validation loop
|
|
1245
|
-
* const result = await client.
|
|
1264
|
+
* const result = await client.ai('Fill out the contact form', { validateAndLoop: true });
|
|
1246
1265
|
* console.log(result); // AI's final assessment
|
|
1247
1266
|
*/
|
|
1248
1267
|
act(
|
|
@@ -1251,7 +1270,6 @@ export default class TestDriverSDK {
|
|
|
1251
1270
|
): Promise<string | void>;
|
|
1252
1271
|
|
|
1253
1272
|
/**
|
|
1254
|
-
* @deprecated Use act() instead
|
|
1255
1273
|
* Execute a natural language task using AI
|
|
1256
1274
|
*
|
|
1257
1275
|
* @param task - Natural language description of what to do
|
package/sdk.js
CHANGED
|
@@ -1506,64 +1506,7 @@ class TestDriverSDK {
|
|
|
1506
1506
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1507
1507
|
}
|
|
1508
1508
|
|
|
1509
|
-
// Set up Chrome profile with preferences
|
|
1510
1509
|
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1511
|
-
const userDataDir =
|
|
1512
|
-
this.os === "windows"
|
|
1513
|
-
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
1514
|
-
: "/tmp/testdriver-chrome-profile";
|
|
1515
|
-
|
|
1516
|
-
// Create user data directory and Default profile directory
|
|
1517
|
-
const defaultProfileDir =
|
|
1518
|
-
this.os === "windows"
|
|
1519
|
-
? `${userDataDir}\\Default`
|
|
1520
|
-
: `${userDataDir}/Default`;
|
|
1521
|
-
|
|
1522
|
-
const createDirCmd =
|
|
1523
|
-
this.os === "windows"
|
|
1524
|
-
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
1525
|
-
: `mkdir -p "${defaultProfileDir}"`;
|
|
1526
|
-
|
|
1527
|
-
await this.exec(shell, createDirCmd, 60000, true);
|
|
1528
|
-
|
|
1529
|
-
// Write Chrome preferences
|
|
1530
|
-
const chromePrefs = {
|
|
1531
|
-
credentials_enable_service: false,
|
|
1532
|
-
profile: {
|
|
1533
|
-
password_manager_enabled: false,
|
|
1534
|
-
default_content_setting_values: {},
|
|
1535
|
-
},
|
|
1536
|
-
signin: {
|
|
1537
|
-
allowed: false,
|
|
1538
|
-
},
|
|
1539
|
-
sync: {
|
|
1540
|
-
requested: false,
|
|
1541
|
-
first_setup_complete: true,
|
|
1542
|
-
sync_all_os_types: false,
|
|
1543
|
-
},
|
|
1544
|
-
autofill: {
|
|
1545
|
-
enabled: false,
|
|
1546
|
-
},
|
|
1547
|
-
local_state: {
|
|
1548
|
-
browser: {
|
|
1549
|
-
has_seen_welcome_page: true,
|
|
1550
|
-
},
|
|
1551
|
-
},
|
|
1552
|
-
};
|
|
1553
|
-
|
|
1554
|
-
const prefsPath =
|
|
1555
|
-
this.os === "windows"
|
|
1556
|
-
? `${defaultProfileDir}\\Preferences`
|
|
1557
|
-
: `${defaultProfileDir}/Preferences`;
|
|
1558
|
-
|
|
1559
|
-
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
1560
|
-
const writePrefCmd =
|
|
1561
|
-
this.os === "windows"
|
|
1562
|
-
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
1563
|
-
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
1564
|
-
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
1565
|
-
|
|
1566
|
-
await this.exec(shell, writePrefCmd, 60000, true);
|
|
1567
1510
|
|
|
1568
1511
|
// Build Chrome launch command
|
|
1569
1512
|
const chromeArgs = [];
|
|
@@ -1575,7 +1518,6 @@ class TestDriverSDK {
|
|
|
1575
1518
|
"--no-first-run",
|
|
1576
1519
|
"--no-experiments",
|
|
1577
1520
|
"--disable-infobars",
|
|
1578
|
-
`--user-data-dir=${userDataDir}`,
|
|
1579
1521
|
);
|
|
1580
1522
|
|
|
1581
1523
|
// Add remote debugging port for captcha solving support
|
|
@@ -1788,64 +1730,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1788
1730
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1789
1731
|
}
|
|
1790
1732
|
|
|
1791
|
-
// Set up Chrome profile with preferences
|
|
1792
|
-
const userDataDir =
|
|
1793
|
-
this.os === "windows"
|
|
1794
|
-
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
1795
|
-
: "/tmp/testdriver-chrome-profile";
|
|
1796
|
-
|
|
1797
|
-
// Create user data directory and Default profile directory
|
|
1798
|
-
const defaultProfileDir =
|
|
1799
|
-
this.os === "windows"
|
|
1800
|
-
? `${userDataDir}\\Default`
|
|
1801
|
-
: `${userDataDir}/Default`;
|
|
1802
|
-
|
|
1803
|
-
const createDirCmd =
|
|
1804
|
-
this.os === "windows"
|
|
1805
|
-
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
1806
|
-
: `mkdir -p "${defaultProfileDir}"`;
|
|
1807
|
-
|
|
1808
|
-
await this.exec(shell, createDirCmd, 60000, true);
|
|
1809
|
-
|
|
1810
|
-
// Write Chrome preferences
|
|
1811
|
-
const chromePrefs = {
|
|
1812
|
-
credentials_enable_service: false,
|
|
1813
|
-
profile: {
|
|
1814
|
-
password_manager_enabled: false,
|
|
1815
|
-
default_content_setting_values: {},
|
|
1816
|
-
},
|
|
1817
|
-
signin: {
|
|
1818
|
-
allowed: false,
|
|
1819
|
-
},
|
|
1820
|
-
sync: {
|
|
1821
|
-
requested: false,
|
|
1822
|
-
first_setup_complete: true,
|
|
1823
|
-
sync_all_os_types: false,
|
|
1824
|
-
},
|
|
1825
|
-
autofill: {
|
|
1826
|
-
enabled: false,
|
|
1827
|
-
},
|
|
1828
|
-
local_state: {
|
|
1829
|
-
browser: {
|
|
1830
|
-
has_seen_welcome_page: true,
|
|
1831
|
-
},
|
|
1832
|
-
},
|
|
1833
|
-
};
|
|
1834
|
-
|
|
1835
|
-
const prefsPath =
|
|
1836
|
-
this.os === "windows"
|
|
1837
|
-
? `${defaultProfileDir}\\Preferences`
|
|
1838
|
-
: `${defaultProfileDir}/Preferences`;
|
|
1839
|
-
|
|
1840
|
-
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
1841
|
-
const writePrefCmd =
|
|
1842
|
-
this.os === "windows"
|
|
1843
|
-
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
1844
|
-
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
1845
|
-
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
1846
|
-
|
|
1847
|
-
await this.exec(shell, writePrefCmd, 60000, true);
|
|
1848
|
-
|
|
1849
1733
|
// Build Chrome launch command
|
|
1850
1734
|
const chromeArgs = [];
|
|
1851
1735
|
if (maximized) chromeArgs.push("--start-maximized");
|
|
@@ -1856,7 +1740,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1856
1740
|
"--no-experiments",
|
|
1857
1741
|
"--disable-infobars",
|
|
1858
1742
|
"--disable-features=ChromeLabs",
|
|
1859
|
-
`--user-data-dir=${userDataDir}`,
|
|
1860
1743
|
);
|
|
1861
1744
|
|
|
1862
1745
|
// Add remote debugging port for captcha solving support
|
|
@@ -2160,6 +2043,58 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2160
2043
|
|
|
2161
2044
|
await this.focusApplication("Electron");
|
|
2162
2045
|
},
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* Initialize Dashcam recording with logging
|
|
2049
|
+
* @param {Object} options - Dashcam options
|
|
2050
|
+
* @param {string} [options.logPath] - Path to log file (auto-generated if not provided)
|
|
2051
|
+
* @param {string} [options.logName='TestDriver Log'] - Display name for the log
|
|
2052
|
+
* @param {boolean} [options.webLogs=true] - Enable web log tracking
|
|
2053
|
+
* @param {string} [options.title] - Custom title for the recording
|
|
2054
|
+
* @returns {Promise<void>}
|
|
2055
|
+
*/
|
|
2056
|
+
dashcam: async (options = {}) => {
|
|
2057
|
+
const {
|
|
2058
|
+
logPath,
|
|
2059
|
+
logName = "TestDriver Log",
|
|
2060
|
+
webLogs = true,
|
|
2061
|
+
title,
|
|
2062
|
+
} = options;
|
|
2063
|
+
|
|
2064
|
+
// Ensure dashcam is available
|
|
2065
|
+
if (!this._dashcam) {
|
|
2066
|
+
console.warn(
|
|
2067
|
+
"[provision.dashcam] Dashcam is not available. Skipping.",
|
|
2068
|
+
);
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// Set custom title if provided
|
|
2073
|
+
if (title) {
|
|
2074
|
+
this._dashcam.setTitle(title);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// Add file log tracking
|
|
2078
|
+
const actualLogPath =
|
|
2079
|
+
logPath ||
|
|
2080
|
+
(this.os === "windows"
|
|
2081
|
+
? "C:\\Users\\testdriver\\testdriver.log"
|
|
2082
|
+
: "/tmp/testdriver.log");
|
|
2083
|
+
|
|
2084
|
+
await this._dashcam.addFileLog(actualLogPath, logName);
|
|
2085
|
+
|
|
2086
|
+
// Add web log tracking if enabled
|
|
2087
|
+
if (webLogs) {
|
|
2088
|
+
await this._dashcam.addWebLog("**", "Web Logs");
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Start recording if not already recording
|
|
2092
|
+
if (!(await this._dashcam.isRecording())) {
|
|
2093
|
+
await this._dashcam.start();
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
console.log("[provision.dashcam] ✅ Dashcam recording started");
|
|
2097
|
+
},
|
|
2163
2098
|
};
|
|
2164
2099
|
|
|
2165
2100
|
// Wrap all provision methods with reconnect check using Proxy
|
|
@@ -3499,28 +3434,28 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3499
3434
|
*
|
|
3500
3435
|
* @example
|
|
3501
3436
|
* // Simple execution
|
|
3502
|
-
* const result = await client.
|
|
3437
|
+
* const result = await client.ai('Click the submit button');
|
|
3503
3438
|
* console.log(result.success); // true
|
|
3504
3439
|
*
|
|
3505
3440
|
* @example
|
|
3506
3441
|
* // With custom retry limit
|
|
3507
|
-
* const result = await client.
|
|
3442
|
+
* const result = await client.ai('Fill out the contact form', { tries: 10 });
|
|
3508
3443
|
* console.log(`Completed in ${result.tries} tries`);
|
|
3509
3444
|
*
|
|
3510
3445
|
* @example
|
|
3511
3446
|
* // Handle failures
|
|
3512
3447
|
* try {
|
|
3513
|
-
* await client.
|
|
3448
|
+
* await client.ai('Complete the checkout process', { tries: 3 });
|
|
3514
3449
|
* } catch (error) {
|
|
3515
3450
|
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3516
3451
|
* }
|
|
3517
3452
|
*/
|
|
3518
|
-
async
|
|
3453
|
+
async ai(task, options = {}) {
|
|
3519
3454
|
this._ensureConnected();
|
|
3520
3455
|
|
|
3521
3456
|
const { tries = 7 } = options;
|
|
3522
3457
|
|
|
3523
|
-
this.analytics.track("sdk.
|
|
3458
|
+
this.analytics.track("sdk.ai", { task, tries });
|
|
3524
3459
|
|
|
3525
3460
|
const { events } = require("./agent/events.js");
|
|
3526
3461
|
const startTime = Date.now();
|
|
@@ -3529,7 +3464,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3529
3464
|
const originalCheckLimit = this.agent.checkLimit;
|
|
3530
3465
|
this.agent.checkLimit = tries;
|
|
3531
3466
|
|
|
3532
|
-
// Reset check count for this
|
|
3467
|
+
// Reset check count for this ai() call
|
|
3533
3468
|
const originalCheckCount = this.agent.checkCount;
|
|
3534
3469
|
this.agent.checkCount = 0;
|
|
3535
3470
|
|
|
@@ -3596,7 +3531,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3596
3531
|
}
|
|
3597
3532
|
|
|
3598
3533
|
/**
|
|
3599
|
-
* @deprecated Use
|
|
3534
|
+
* @deprecated Use ai() instead
|
|
3600
3535
|
* Execute a natural language task using AI
|
|
3601
3536
|
*
|
|
3602
3537
|
* @param {string} task - Natural language description of what to do
|
|
@@ -3604,8 +3539,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3604
3539
|
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3605
3540
|
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3606
3541
|
*/
|
|
3607
|
-
async
|
|
3608
|
-
return await this.
|
|
3542
|
+
async act(task, options) {
|
|
3543
|
+
return await this.ai(task, options);
|
|
3609
3544
|
}
|
|
3610
3545
|
}
|
|
3611
3546
|
|