testdriverai 7.2.63 → 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 +4 -0
- package/agent/index.js +73 -56
- package/agent/lib/sandbox.js +77 -26
- package/lib/core/Dashcam.js +22 -15
- package/lib/vitest/setup-aws.mjs +0 -10
- package/package.json +1 -1
- package/sdk.d.ts +3 -3
- package/sdk.js +9 -9
package/CHANGELOG.md
CHANGED
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,15 +441,12 @@ 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
|
|
450
452
|
this.emitter.emit(events.log.log, response.data);
|
|
@@ -878,7 +880,7 @@ commands:
|
|
|
878
880
|
currentTask,
|
|
879
881
|
dry = false,
|
|
880
882
|
validateAndLoop = false,
|
|
881
|
-
shouldSave = true
|
|
883
|
+
shouldSave = true,
|
|
882
884
|
) {
|
|
883
885
|
// Check if execution has been stopped
|
|
884
886
|
if (this.stopped) {
|
|
@@ -901,15 +903,12 @@ commands:
|
|
|
901
903
|
|
|
902
904
|
this.lastScreenshot = await this.system.captureScreenBase64();
|
|
903
905
|
|
|
904
|
-
let message = await this.sdk.req(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
image: this.lastScreenshot,
|
|
911
|
-
}
|
|
912
|
-
);
|
|
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
|
+
});
|
|
913
912
|
|
|
914
913
|
this.emitter.emit(events.log.log, message.data);
|
|
915
914
|
|
|
@@ -1626,8 +1625,8 @@ ${regression}
|
|
|
1626
1625
|
|
|
1627
1626
|
// Returns the path to the last sandbox file
|
|
1628
1627
|
getLastSandboxFilePath() {
|
|
1629
|
-
const testdriverDir = path.join(process.cwd(),
|
|
1630
|
-
return path.join(testdriverDir,
|
|
1628
|
+
const testdriverDir = path.join(process.cwd(), ".testdriver");
|
|
1629
|
+
return path.join(testdriverDir, "last-sandbox");
|
|
1631
1630
|
}
|
|
1632
1631
|
|
|
1633
1632
|
// Returns full sandbox info from last-sandbox file (no timeout - let API validate)
|
|
@@ -1648,7 +1647,7 @@ ${regression}
|
|
|
1648
1647
|
|
|
1649
1648
|
return {
|
|
1650
1649
|
sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
|
|
1651
|
-
os: sandboxInfo.os ||
|
|
1650
|
+
os: sandboxInfo.os || "linux",
|
|
1652
1651
|
ami: sandboxInfo.ami || null,
|
|
1653
1652
|
instanceType: sandboxInfo.instanceType || null,
|
|
1654
1653
|
timestamp: sandboxInfo.timestamp || null,
|
|
@@ -1663,7 +1662,7 @@ ${regression}
|
|
|
1663
1662
|
// Returns sandboxId to use if AMI/instance type match current requirements
|
|
1664
1663
|
getRecentSandboxId() {
|
|
1665
1664
|
const sandboxInfo = this.getLastSandboxId();
|
|
1666
|
-
|
|
1665
|
+
|
|
1667
1666
|
if (!sandboxInfo || !sandboxInfo.sandboxId) {
|
|
1668
1667
|
return null;
|
|
1669
1668
|
}
|
|
@@ -1690,13 +1689,13 @@ ${regression}
|
|
|
1690
1689
|
saveLastSandboxId(sandboxId, osType = "linux") {
|
|
1691
1690
|
const lastSandboxFile = this.getLastSandboxFilePath();
|
|
1692
1691
|
const testdriverDir = path.dirname(lastSandboxFile);
|
|
1693
|
-
|
|
1692
|
+
|
|
1694
1693
|
try {
|
|
1695
1694
|
// Ensure .testdriver directory exists
|
|
1696
1695
|
if (!fs.existsSync(testdriverDir)) {
|
|
1697
1696
|
fs.mkdirSync(testdriverDir, { recursive: true });
|
|
1698
1697
|
}
|
|
1699
|
-
|
|
1698
|
+
|
|
1700
1699
|
const sandboxInfo = {
|
|
1701
1700
|
sandboxId: sandboxId,
|
|
1702
1701
|
os: osType,
|
|
@@ -1757,15 +1756,9 @@ ${regression}
|
|
|
1757
1756
|
// Also clear this.sandboxId to prevent reconnection attempts
|
|
1758
1757
|
this.sandboxId = null;
|
|
1759
1758
|
if (!this.config.CI && !this.newSandbox) {
|
|
1760
|
-
this.emitter.emit(
|
|
1761
|
-
events.log.log,
|
|
1762
|
-
theme.dim("--`new` flag detected, will create a new sandbox"),
|
|
1763
|
-
);
|
|
1759
|
+
this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
|
|
1764
1760
|
} else if (this.newSandbox) {
|
|
1765
|
-
this.emitter.emit(
|
|
1766
|
-
events.log.log,
|
|
1767
|
-
theme.dim("--new-sandbox flag detected, will create a new sandbox"),
|
|
1768
|
-
);
|
|
1761
|
+
this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
|
|
1769
1762
|
}
|
|
1770
1763
|
}
|
|
1771
1764
|
|
|
@@ -1802,7 +1795,7 @@ ${regression}
|
|
|
1802
1795
|
theme.dim(`using recent sandbox: ${recentId}`),
|
|
1803
1796
|
);
|
|
1804
1797
|
this.sandboxId = recentId;
|
|
1805
|
-
|
|
1798
|
+
|
|
1806
1799
|
try {
|
|
1807
1800
|
let instance = await this.connectToSandboxDirect(
|
|
1808
1801
|
this.sandboxId,
|
|
@@ -1850,13 +1843,17 @@ ${regression}
|
|
|
1850
1843
|
console.error("Failed to reconnect to sandbox:", error);
|
|
1851
1844
|
}
|
|
1852
1845
|
}
|
|
1853
|
-
|
|
1846
|
+
|
|
1854
1847
|
// Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
|
|
1855
1848
|
if (!this.instance) {
|
|
1856
1849
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
1857
1850
|
this.emitter.emit(
|
|
1858
1851
|
events.log.narration,
|
|
1859
|
-
formatter.getPrefix("connect") +
|
|
1852
|
+
formatter.getPrefix("connect") +
|
|
1853
|
+
" " +
|
|
1854
|
+
theme.green.bold("Creating") +
|
|
1855
|
+
" " +
|
|
1856
|
+
theme.cyan(`new sandbox...`),
|
|
1860
1857
|
);
|
|
1861
1858
|
// We don't have resiliency/retries baked in, so let's at least give it 1 attempt
|
|
1862
1859
|
// to see if that fixes the issue.
|
|
@@ -1869,11 +1866,12 @@ ${regression}
|
|
|
1869
1866
|
});
|
|
1870
1867
|
|
|
1871
1868
|
// Extract the sandbox ID from the newly created sandbox
|
|
1872
|
-
this.sandboxId =
|
|
1873
|
-
|
|
1869
|
+
this.sandboxId =
|
|
1870
|
+
newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
|
|
1871
|
+
|
|
1874
1872
|
// Use the configured sandbox OS type
|
|
1875
1873
|
this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
|
|
1876
|
-
|
|
1874
|
+
|
|
1877
1875
|
let instance = await this.connectToSandboxDirect(
|
|
1878
1876
|
this.sandboxId,
|
|
1879
1877
|
true, // always persist by default
|
|
@@ -2002,7 +2000,6 @@ ${regression}
|
|
|
2002
2000
|
}
|
|
2003
2001
|
|
|
2004
2002
|
async renderSandbox(instance, headless = false) {
|
|
2005
|
-
|
|
2006
2003
|
if (!headless) {
|
|
2007
2004
|
let url;
|
|
2008
2005
|
|
|
@@ -2056,7 +2053,13 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2056
2053
|
}
|
|
2057
2054
|
|
|
2058
2055
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
2059
|
-
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
|
+
);
|
|
2060
2063
|
let ableToAuth = await this.sandbox.auth(this.config.TD_API_KEY);
|
|
2061
2064
|
|
|
2062
2065
|
if (!ableToAuth) {
|
|
@@ -2070,7 +2073,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2070
2073
|
|
|
2071
2074
|
async connectToSandboxDirect(sandboxId, persist = false, keepAlive = null) {
|
|
2072
2075
|
const { formatter } = require("../sdk-log-formatter.js");
|
|
2073
|
-
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
|
+
);
|
|
2074
2084
|
let reply = await this.sandbox.connect(sandboxId, persist, keepAlive);
|
|
2075
2085
|
|
|
2076
2086
|
// reply includes { success, url, sandbox: {...} }
|
|
@@ -2112,15 +2122,18 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2112
2122
|
let response = await this.sandbox.send(sandboxConfig, 60000 * 8);
|
|
2113
2123
|
|
|
2114
2124
|
// Check if queued (all slots in use)
|
|
2115
|
-
if (response.type ===
|
|
2125
|
+
if (response.type === "create.queued") {
|
|
2116
2126
|
this.emitter.emit(
|
|
2117
2127
|
events.log.narration,
|
|
2118
|
-
formatter.getPrefix("queue") +
|
|
2119
|
-
|
|
2128
|
+
formatter.getPrefix("queue") +
|
|
2129
|
+
" " +
|
|
2130
|
+
theme.yellow.bold("Waiting") +
|
|
2131
|
+
" " +
|
|
2132
|
+
theme.dim(response.message),
|
|
2120
2133
|
);
|
|
2121
2134
|
|
|
2122
2135
|
// Wait then retry
|
|
2123
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
2136
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
2124
2137
|
continue;
|
|
2125
2138
|
}
|
|
2126
2139
|
|
|
@@ -2139,10 +2152,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2139
2152
|
// should be start of new session
|
|
2140
2153
|
// If sandbox is connected, get system info; otherwise pass empty objects
|
|
2141
2154
|
const isSandboxConnected = this.sandbox.apiSocketConnected;
|
|
2142
|
-
|
|
2155
|
+
|
|
2143
2156
|
const sessionRes = await this.sdk.req("session/start", {
|
|
2144
|
-
systemInformationOsInfo: isSandboxConnected
|
|
2145
|
-
|
|
2157
|
+
systemInformationOsInfo: isSandboxConnected
|
|
2158
|
+
? await this.system.getSystemInformationOsInfo()
|
|
2159
|
+
: {},
|
|
2160
|
+
mousePosition: isSandboxConnected
|
|
2161
|
+
? await this.system.getMousePosition()
|
|
2162
|
+
: {},
|
|
2146
2163
|
activeWindow: isSandboxConnected ? await this.system.activeWin() : {},
|
|
2147
2164
|
});
|
|
2148
2165
|
|
|
@@ -2153,7 +2170,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2153
2170
|
}
|
|
2154
2171
|
|
|
2155
2172
|
this.session.set(sessionRes.data.id);
|
|
2156
|
-
|
|
2173
|
+
|
|
2157
2174
|
// Set Sentry session trace context for distributed tracing
|
|
2158
2175
|
// This links CLI errors/logs to the same trace as API calls
|
|
2159
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 = {};
|
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
|
@@ -1247,6 +1247,7 @@ export default class TestDriverSDK {
|
|
|
1247
1247
|
// AI Methods (Exploratory Loop)
|
|
1248
1248
|
|
|
1249
1249
|
/**
|
|
1250
|
+
* @deprecated Use ai() instead
|
|
1250
1251
|
* Execute a natural language task using AI
|
|
1251
1252
|
* This is the SDK equivalent of the CLI's exploratory loop
|
|
1252
1253
|
*
|
|
@@ -1256,11 +1257,11 @@ export default class TestDriverSDK {
|
|
|
1256
1257
|
*
|
|
1257
1258
|
* @example
|
|
1258
1259
|
* // Simple execution
|
|
1259
|
-
* await client.
|
|
1260
|
+
* await client.ai('Click the submit button');
|
|
1260
1261
|
*
|
|
1261
1262
|
* @example
|
|
1262
1263
|
* // With validation loop
|
|
1263
|
-
* const result = await client.
|
|
1264
|
+
* const result = await client.ai('Fill out the contact form', { validateAndLoop: true });
|
|
1264
1265
|
* console.log(result); // AI's final assessment
|
|
1265
1266
|
*/
|
|
1266
1267
|
act(
|
|
@@ -1269,7 +1270,6 @@ export default class TestDriverSDK {
|
|
|
1269
1270
|
): Promise<string | void>;
|
|
1270
1271
|
|
|
1271
1272
|
/**
|
|
1272
|
-
* @deprecated Use act() instead
|
|
1273
1273
|
* Execute a natural language task using AI
|
|
1274
1274
|
*
|
|
1275
1275
|
* @param task - Natural language description of what to do
|
package/sdk.js
CHANGED
|
@@ -3434,28 +3434,28 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3434
3434
|
*
|
|
3435
3435
|
* @example
|
|
3436
3436
|
* // Simple execution
|
|
3437
|
-
* const result = await client.
|
|
3437
|
+
* const result = await client.ai('Click the submit button');
|
|
3438
3438
|
* console.log(result.success); // true
|
|
3439
3439
|
*
|
|
3440
3440
|
* @example
|
|
3441
3441
|
* // With custom retry limit
|
|
3442
|
-
* const result = await client.
|
|
3442
|
+
* const result = await client.ai('Fill out the contact form', { tries: 10 });
|
|
3443
3443
|
* console.log(`Completed in ${result.tries} tries`);
|
|
3444
3444
|
*
|
|
3445
3445
|
* @example
|
|
3446
3446
|
* // Handle failures
|
|
3447
3447
|
* try {
|
|
3448
|
-
* await client.
|
|
3448
|
+
* await client.ai('Complete the checkout process', { tries: 3 });
|
|
3449
3449
|
* } catch (error) {
|
|
3450
3450
|
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3451
3451
|
* }
|
|
3452
3452
|
*/
|
|
3453
|
-
async
|
|
3453
|
+
async ai(task, options = {}) {
|
|
3454
3454
|
this._ensureConnected();
|
|
3455
3455
|
|
|
3456
3456
|
const { tries = 7 } = options;
|
|
3457
3457
|
|
|
3458
|
-
this.analytics.track("sdk.
|
|
3458
|
+
this.analytics.track("sdk.ai", { task, tries });
|
|
3459
3459
|
|
|
3460
3460
|
const { events } = require("./agent/events.js");
|
|
3461
3461
|
const startTime = Date.now();
|
|
@@ -3464,7 +3464,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3464
3464
|
const originalCheckLimit = this.agent.checkLimit;
|
|
3465
3465
|
this.agent.checkLimit = tries;
|
|
3466
3466
|
|
|
3467
|
-
// Reset check count for this
|
|
3467
|
+
// Reset check count for this ai() call
|
|
3468
3468
|
const originalCheckCount = this.agent.checkCount;
|
|
3469
3469
|
this.agent.checkCount = 0;
|
|
3470
3470
|
|
|
@@ -3531,7 +3531,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3531
3531
|
}
|
|
3532
3532
|
|
|
3533
3533
|
/**
|
|
3534
|
-
* @deprecated Use
|
|
3534
|
+
* @deprecated Use ai() instead
|
|
3535
3535
|
* Execute a natural language task using AI
|
|
3536
3536
|
*
|
|
3537
3537
|
* @param {string} task - Natural language description of what to do
|
|
@@ -3539,8 +3539,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3539
3539
|
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3540
3540
|
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3541
3541
|
*/
|
|
3542
|
-
async
|
|
3543
|
-
return await this.
|
|
3542
|
+
async act(task, options) {
|
|
3543
|
+
return await this.ai(task, options);
|
|
3544
3544
|
}
|
|
3545
3545
|
}
|
|
3546
3546
|
|