testdriverai 7.3.33 → 7.3.35
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/copilot-instructions.md +58 -9
- package/.github/workflows/windows-self-hosted.yaml +3 -14
- package/CHANGELOG.md +8 -0
- package/agent/index.js +3 -142
- package/agent/lib/debugger-server.js +56 -2
- package/agent/lib/sandbox.js +31 -8
- package/docs/v7/client.mdx +107 -2
- package/docs/v7/customizing-devices.mdx +170 -4
- package/examples/chrome-extension.test.mjs +27 -27
- package/interfaces/vitest-plugin.mjs +4 -1
- package/lib/sentry.js +32 -5
- package/lib/vitest/hooks.mjs +4 -1
- package/lib/vitest/setup-aws.mjs +8 -0
- package/lib/vitest/setup-self-hosted.mjs +15 -0
- package/package.json +1 -2
- package/sdk.js +40 -74
- package/vitest.config.mjs +3 -3
- package/lib/vitest/setup-disable-defender.mjs +0 -52
|
@@ -507,15 +507,64 @@ it("should incrementally build test", async (context) => {
|
|
|
507
507
|
|
|
508
508
|
```javascript
|
|
509
509
|
const testdriver = TestDriver(context, {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
reconnect: false,
|
|
513
|
-
keepAlive: 30000,
|
|
514
|
-
os: "linux",
|
|
515
|
-
resolution: "1366x768",
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
510
|
+
// === Sandbox & Connection ===
|
|
511
|
+
newSandbox: true, // Force creation of a new sandbox (default: true)
|
|
512
|
+
reconnect: false, // Reconnect to last sandbox (default: false)
|
|
513
|
+
keepAlive: 30000, // Keep sandbox alive after test in ms (default: 30000)
|
|
514
|
+
os: "linux", // 'linux' | 'windows' (default: 'linux')
|
|
515
|
+
resolution: "1366x768", // Sandbox resolution (e.g., '1920x1080')
|
|
516
|
+
ip: "203.0.113.42", // Direct IP for self-hosted sandbox
|
|
517
|
+
sandboxAmi: "ami-1234", // Custom AMI ID (AWS deployments)
|
|
518
|
+
sandboxInstance: "i3.metal", // EC2 instance type (AWS deployments)
|
|
519
|
+
|
|
520
|
+
// === Preview & Debugging ===
|
|
521
|
+
preview: "browser", // "browser" | "ide" | "none" (default: "browser")
|
|
522
|
+
headless: false, // @deprecated - use preview: "none" instead
|
|
523
|
+
debugOnFailure: false, // Keep sandbox alive on test failure for debugging
|
|
524
|
+
|
|
525
|
+
// === Caching ===
|
|
526
|
+
cache: true, // Enable element caching (default: true)
|
|
527
|
+
// Or use advanced caching config:
|
|
528
|
+
// cache: {
|
|
529
|
+
// enabled: true,
|
|
530
|
+
// thresholds: {
|
|
531
|
+
// find: { screen: 0.05, element: 0.8 },
|
|
532
|
+
// assert: 0.05
|
|
533
|
+
// }
|
|
534
|
+
// },
|
|
535
|
+
cacheKey: "my-test", // Cache key for element finding operations
|
|
536
|
+
|
|
537
|
+
// === Recording & Screenshots ===
|
|
538
|
+
dashcam: true, // Enable/disable Dashcam video recording (default: true)
|
|
539
|
+
autoScreenshots: true, // Capture screenshots before/after each command (default: true)
|
|
540
|
+
|
|
541
|
+
// === AI Configuration ===
|
|
542
|
+
ai: { // Global AI sampling configuration
|
|
543
|
+
temperature: 0, // 0 = deterministic, higher = more creative
|
|
544
|
+
top: {
|
|
545
|
+
p: 0.9, // Top-P nucleus sampling (0-1)
|
|
546
|
+
k: 40, // Top-K sampling (1 = most likely, 0 = disabled)
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
// === Screen Change Detection ===
|
|
551
|
+
redraw: true, // Enable redraw detection (default: true)
|
|
552
|
+
// Or use advanced redraw config:
|
|
553
|
+
// redraw: {
|
|
554
|
+
// enabled: true,
|
|
555
|
+
// thresholds: {
|
|
556
|
+
// screen: 0.05, // Pixel diff threshold (0-1), false to disable
|
|
557
|
+
// network: false, // Monitor network activity (default: false)
|
|
558
|
+
// }
|
|
559
|
+
// },
|
|
560
|
+
|
|
561
|
+
// === Logging & Analytics ===
|
|
562
|
+
logging: true, // Enable console logging output (default: true)
|
|
563
|
+
analytics: true, // Enable analytics tracking (default: true)
|
|
564
|
+
|
|
565
|
+
// === Advanced ===
|
|
566
|
+
apiRoot: "https://...", // API endpoint URL (for self-hosted deployments)
|
|
567
|
+
environment: {}, // Additional environment variables for the sandbox
|
|
519
568
|
});
|
|
520
569
|
```
|
|
521
570
|
|
|
@@ -22,7 +22,7 @@ on:
|
|
|
22
22
|
|
|
23
23
|
jobs:
|
|
24
24
|
test:
|
|
25
|
-
runs-on:
|
|
25
|
+
runs-on: testdriver-32
|
|
26
26
|
|
|
27
27
|
steps:
|
|
28
28
|
- name: Checkout repository
|
|
@@ -39,19 +39,8 @@ jobs:
|
|
|
39
39
|
- name: Install dependencies
|
|
40
40
|
run: npm ci
|
|
41
41
|
|
|
42
|
-
- name:
|
|
43
|
-
run: |
|
|
44
|
-
echo "Checking environment variables..."
|
|
45
|
-
if [ -n "${{ secrets.TWOCAPTCHA_API_KEY }}" ]; then
|
|
46
|
-
echo "TWOCAPTCHA_API_KEY is set (length: ${#TWOCAPTCHA_API_KEY})"
|
|
47
|
-
else
|
|
48
|
-
echo "TWOCAPTCHA_API_KEY is NOT set"
|
|
49
|
-
fi
|
|
50
|
-
env:
|
|
51
|
-
TWOCAPTCHA_API_KEY: ${{ secrets.TWOCAPTCHA_API_KEY }}
|
|
52
|
-
|
|
53
|
-
- name: Run Windows tests with self-hosted instances
|
|
54
|
-
run: set -o pipefail && npx vitest run ${{ inputs.test_pattern }} 2>&1 | tee test-output.log
|
|
42
|
+
- name: Run Windows tests
|
|
43
|
+
run: set -o pipefail && npx vitest run ${{ inputs.test_pattern }} --maxWorkers 32 --sequence.concurrent 2>&1 | tee test-output.log
|
|
55
44
|
env:
|
|
56
45
|
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
57
46
|
TWOCAPTCHA_API_KEY: ${{ secrets.TWOCAPTCHA_API_KEY }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [7.3.35](https://github.com/testdriverai/testdriverai/compare/v7.3.34...v7.3.35) (2026-02-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## [7.3.34](https://github.com/testdriverai/testdriverai/compare/v7.3.33...v7.3.34) (2026-02-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
## [7.3.33](https://github.com/testdriverai/testdriverai/compare/v7.3.32...v7.3.33) (2026-02-24)
|
|
2
10
|
|
|
3
11
|
|
package/agent/index.js
CHANGED
|
@@ -1635,105 +1635,6 @@ ${regression}
|
|
|
1635
1635
|
this.emitter.emit(events.log.log, `${inputFile} (end)`);
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
|
-
// Returns the path to the last sandbox file
|
|
1639
|
-
getLastSandboxFilePath() {
|
|
1640
|
-
const testdriverDir = path.join(process.cwd(), ".testdriver");
|
|
1641
|
-
return path.join(testdriverDir, "last-sandbox");
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// Returns full sandbox info from last-sandbox file (no timeout - let API validate)
|
|
1645
|
-
getLastSandboxId() {
|
|
1646
|
-
const lastSandboxFile = this.getLastSandboxFilePath();
|
|
1647
|
-
|
|
1648
|
-
if (fs.existsSync(lastSandboxFile)) {
|
|
1649
|
-
try {
|
|
1650
|
-
const fileContent = fs.readFileSync(lastSandboxFile, "utf-8").trim();
|
|
1651
|
-
|
|
1652
|
-
// Parse sandbox info (supports both old format and new format)
|
|
1653
|
-
let sandboxInfo;
|
|
1654
|
-
try {
|
|
1655
|
-
sandboxInfo = JSON.parse(fileContent);
|
|
1656
|
-
} catch {
|
|
1657
|
-
return { sandboxId: fileContent || null };
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
return {
|
|
1661
|
-
sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
|
|
1662
|
-
os: sandboxInfo.os || "linux",
|
|
1663
|
-
ami: sandboxInfo.ami || null,
|
|
1664
|
-
instanceType: sandboxInfo.instanceType || null,
|
|
1665
|
-
timestamp: sandboxInfo.timestamp || null,
|
|
1666
|
-
};
|
|
1667
|
-
} catch {
|
|
1668
|
-
// ignore errors
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
return null;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// Returns sandboxId to use if AMI/instance type match current requirements
|
|
1675
|
-
getRecentSandboxId() {
|
|
1676
|
-
const sandboxInfo = this.getLastSandboxId();
|
|
1677
|
-
|
|
1678
|
-
if (!sandboxInfo || !sandboxInfo.sandboxId) {
|
|
1679
|
-
return null;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// Check if AMI and instance type match current requirements
|
|
1683
|
-
const currentAmi = this.sandboxAmi || null;
|
|
1684
|
-
const currentInstance = this.sandboxInstance || null;
|
|
1685
|
-
const storedAmi = sandboxInfo.ami || null;
|
|
1686
|
-
const storedInstance = sandboxInfo.instanceType || null;
|
|
1687
|
-
|
|
1688
|
-
if (currentAmi === storedAmi && currentInstance === storedInstance) {
|
|
1689
|
-
return sandboxInfo.sandboxId;
|
|
1690
|
-
} else {
|
|
1691
|
-
this.emitter.emit(
|
|
1692
|
-
events.log.log,
|
|
1693
|
-
theme.dim(
|
|
1694
|
-
"Recent sandbox found but AMI/instance type doesn't match current requirements",
|
|
1695
|
-
),
|
|
1696
|
-
);
|
|
1697
|
-
return null;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
saveLastSandboxId(sandboxId, osType = "linux") {
|
|
1702
|
-
const lastSandboxFile = this.getLastSandboxFilePath();
|
|
1703
|
-
const testdriverDir = path.dirname(lastSandboxFile);
|
|
1704
|
-
|
|
1705
|
-
try {
|
|
1706
|
-
// Ensure .testdriver directory exists
|
|
1707
|
-
if (!fs.existsSync(testdriverDir)) {
|
|
1708
|
-
fs.mkdirSync(testdriverDir, { recursive: true });
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
const sandboxInfo = {
|
|
1712
|
-
sandboxId: sandboxId,
|
|
1713
|
-
os: osType,
|
|
1714
|
-
ami: this.sandboxAmi || null,
|
|
1715
|
-
instanceType: this.sandboxInstance || null,
|
|
1716
|
-
timestamp: new Date().toISOString(),
|
|
1717
|
-
};
|
|
1718
|
-
fs.writeFileSync(lastSandboxFile, JSON.stringify(sandboxInfo, null, 2), {
|
|
1719
|
-
encoding: "utf-8",
|
|
1720
|
-
});
|
|
1721
|
-
} catch {
|
|
1722
|
-
// ignore errors
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
clearRecentSandboxId() {
|
|
1727
|
-
const lastSandboxFile = this.getLastSandboxFilePath();
|
|
1728
|
-
try {
|
|
1729
|
-
if (fs.existsSync(lastSandboxFile)) {
|
|
1730
|
-
fs.unlinkSync(lastSandboxFile);
|
|
1731
|
-
}
|
|
1732
|
-
} catch {
|
|
1733
|
-
// ignore errors
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
1638
|
async buildEnv(options = {}) {
|
|
1738
1639
|
// If instance already exists, do not build environment again
|
|
1739
1640
|
if (this.instance) {
|
|
@@ -1762,10 +1663,8 @@ ${regression}
|
|
|
1762
1663
|
|
|
1763
1664
|
if (heal) this.healMode = heal;
|
|
1764
1665
|
|
|
1765
|
-
// If createNew flag is set, clear
|
|
1666
|
+
// If createNew flag is set, clear sandboxId to prevent reconnection attempts
|
|
1766
1667
|
if (createNew) {
|
|
1767
|
-
this.clearRecentSandboxId();
|
|
1768
|
-
// Also clear this.sandboxId to prevent reconnection attempts
|
|
1769
1668
|
this.sandboxId = null;
|
|
1770
1669
|
if (!this.config.CI && !this.newSandbox) {
|
|
1771
1670
|
this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
|
|
@@ -1780,8 +1679,6 @@ ${regression}
|
|
|
1780
1679
|
// order is important!
|
|
1781
1680
|
await this.connectToSandboxService();
|
|
1782
1681
|
|
|
1783
|
-
const recentId = createNew ? null : this.getRecentSandboxId();
|
|
1784
|
-
|
|
1785
1682
|
// Set sandbox ID for reconnection (only if not creating new and recent ID exists)
|
|
1786
1683
|
if (this.ip) {
|
|
1787
1684
|
let instance = await this.sandbox.send({
|
|
@@ -1794,13 +1691,13 @@ ${regression}
|
|
|
1794
1691
|
// Store connection params for reconnection
|
|
1795
1692
|
// For direct IP connections, store as a direct type so reconnection
|
|
1796
1693
|
// sends a 'direct' message instead of 'connect' with an IP as sandboxId
|
|
1797
|
-
this.sandbox.
|
|
1694
|
+
this.sandbox.setConnectionParams({
|
|
1798
1695
|
type: 'direct',
|
|
1799
1696
|
ip: this.ip,
|
|
1800
1697
|
sandboxId: instance?.instance?.instanceId || instance?.instance?.sandboxId || null,
|
|
1801
1698
|
persist: true,
|
|
1802
1699
|
keepAlive: this.keepAlive,
|
|
1803
|
-
};
|
|
1700
|
+
});
|
|
1804
1701
|
|
|
1805
1702
|
// Mark instance socket as connected so console logs are forwarded
|
|
1806
1703
|
this.sandbox.instanceSocketConnected = true;
|
|
@@ -1811,33 +1708,6 @@ ${regression}
|
|
|
1811
1708
|
await this.runLifecycle("provision");
|
|
1812
1709
|
|
|
1813
1710
|
return;
|
|
1814
|
-
} else if (!createNew && recentId) {
|
|
1815
|
-
// Only attempt to connect to existing sandbox if not in CI mode and not creating new
|
|
1816
|
-
this.emitter.emit(
|
|
1817
|
-
events.log.narration,
|
|
1818
|
-
theme.dim(`using recent sandbox: ${recentId}`),
|
|
1819
|
-
);
|
|
1820
|
-
this.sandboxId = recentId;
|
|
1821
|
-
|
|
1822
|
-
try {
|
|
1823
|
-
let instance = await this.connectToSandboxDirect(
|
|
1824
|
-
this.sandboxId,
|
|
1825
|
-
true, // always persist by default
|
|
1826
|
-
this.keepAlive, // pass keepAlive TTL
|
|
1827
|
-
);
|
|
1828
|
-
|
|
1829
|
-
this.instance = instance;
|
|
1830
|
-
|
|
1831
|
-
await this.renderSandbox(instance, headless);
|
|
1832
|
-
return;
|
|
1833
|
-
} catch (error) {
|
|
1834
|
-
// If connection fails, fall through to creating a new sandbox
|
|
1835
|
-
this.emitter.emit(
|
|
1836
|
-
events.log.narration,
|
|
1837
|
-
theme.dim(`failed to connect to recent sandbox, creating new one...`),
|
|
1838
|
-
);
|
|
1839
|
-
console.error("Failed to reconnect to sandbox:", error);
|
|
1840
|
-
}
|
|
1841
1711
|
} else if (!createNew && this.sandboxId && !this.config.CI) {
|
|
1842
1712
|
// Only attempt to connect to existing sandbox if not in CI mode and not creating new
|
|
1843
1713
|
// Attempt to connect to known instance
|
|
@@ -1892,9 +1762,6 @@ ${regression}
|
|
|
1892
1762
|
this.sandboxId =
|
|
1893
1763
|
newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
|
|
1894
1764
|
|
|
1895
|
-
// Use the configured sandbox OS type
|
|
1896
|
-
this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
|
|
1897
|
-
|
|
1898
1765
|
let instance = await this.connectToSandboxDirect(
|
|
1899
1766
|
this.sandboxId,
|
|
1900
1767
|
true, // always persist by default
|
|
@@ -2332,12 +2199,6 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2332
2199
|
}
|
|
2333
2200
|
|
|
2334
2201
|
// Success - got a sandbox
|
|
2335
|
-
if (response.sandbox && response.sandbox.sandboxId) {
|
|
2336
|
-
this.saveLastSandboxId(response.sandbox.sandboxId, this.sandboxOs);
|
|
2337
|
-
} else if (response.sandbox && response.sandbox.instanceId) {
|
|
2338
|
-
this.saveLastSandboxId(response.sandbox.instanceId, this.sandboxOs);
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
2202
|
return response;
|
|
2342
2203
|
}
|
|
2343
2204
|
}
|
|
@@ -8,6 +8,8 @@ const logger = require("./logger");
|
|
|
8
8
|
let server = null;
|
|
9
9
|
let wss = null;
|
|
10
10
|
let clients = new Set();
|
|
11
|
+
let refCount = 0; // Number of active consumers (for concurrent test safety)
|
|
12
|
+
let debuggerUrl = null; // Stored URL of running debugger
|
|
11
13
|
|
|
12
14
|
function createDebuggerServer(config = {}) {
|
|
13
15
|
const port = config.TD_DEBUGGER_PORT || 0; // 0 means find available port
|
|
@@ -76,7 +78,12 @@ function createDebuggerServer(config = {}) {
|
|
|
76
78
|
|
|
77
79
|
// Start server on available port
|
|
78
80
|
server.listen(port, "localhost", () => {
|
|
79
|
-
const
|
|
81
|
+
const address = server.address();
|
|
82
|
+
if (!address) {
|
|
83
|
+
reject(new Error("Server started but address is not available"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const actualPort = address.port;
|
|
80
87
|
resolve({ port: actualPort, server, wss });
|
|
81
88
|
});
|
|
82
89
|
|
|
@@ -124,7 +131,43 @@ async function startDebugger(config = {}, emitter) {
|
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Acquire a reference to the debugger server.
|
|
136
|
+
* Starts the server on first call; subsequent calls reuse the existing server.
|
|
137
|
+
* Each call increments a reference count — call releaseDebugger() when done.
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} config - Debugger configuration
|
|
140
|
+
* @param {EventEmitter} emitter - Event emitter for broadcasting
|
|
141
|
+
* @returns {Promise<{port: number, url: string}>} Debugger connection info
|
|
142
|
+
*/
|
|
143
|
+
async function acquireDebugger(config = {}, emitter) {
|
|
144
|
+
refCount++;
|
|
145
|
+
if (server && debuggerUrl) {
|
|
146
|
+
// Server already running — reuse it
|
|
147
|
+
return { url: debuggerUrl };
|
|
148
|
+
}
|
|
149
|
+
// First consumer — start the server
|
|
150
|
+
const result = await startDebugger(config, emitter);
|
|
151
|
+
debuggerUrl = result.url;
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Release a reference to the debugger server.
|
|
157
|
+
* Only actually stops the server when the last consumer releases.
|
|
158
|
+
*/
|
|
159
|
+
function releaseDebugger() {
|
|
160
|
+
if (refCount > 0) refCount--;
|
|
161
|
+
if (refCount > 0) return; // Other tests still using it
|
|
162
|
+
forceStopDebugger();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Forcefully stop the debugger server regardless of reference count.
|
|
167
|
+
* Used for process exit cleanup.
|
|
168
|
+
*/
|
|
169
|
+
function forceStopDebugger() {
|
|
170
|
+
refCount = 0;
|
|
128
171
|
if (wss) {
|
|
129
172
|
wss.close();
|
|
130
173
|
wss = null;
|
|
@@ -136,12 +179,23 @@ function stopDebugger() {
|
|
|
136
179
|
}
|
|
137
180
|
|
|
138
181
|
clients.clear();
|
|
182
|
+
debuggerUrl = null;
|
|
183
|
+
module.exports.debuggerUrl = null;
|
|
184
|
+
module.exports.config = null;
|
|
139
185
|
logger.log("Debugger server stopped");
|
|
140
186
|
}
|
|
141
187
|
|
|
188
|
+
// Keep stopDebugger as alias for forceStopDebugger for backward compatibility
|
|
189
|
+
function stopDebugger() {
|
|
190
|
+
forceStopDebugger();
|
|
191
|
+
}
|
|
192
|
+
|
|
142
193
|
module.exports = {
|
|
143
194
|
startDebugger,
|
|
144
195
|
stopDebugger,
|
|
196
|
+
acquireDebugger,
|
|
197
|
+
releaseDebugger,
|
|
198
|
+
forceStopDebugger,
|
|
145
199
|
broadcastEvent,
|
|
146
200
|
createDebuggerServer,
|
|
147
201
|
debuggerUrl: null,
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -47,6 +47,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
47
47
|
this.reconnecting = false; // Prevent duplicate reconnection attempts
|
|
48
48
|
this.pendingTimeouts = new Map(); // Track per-message timeouts
|
|
49
49
|
this.pendingRetryQueue = []; // Queue of requests to retry after reconnection
|
|
50
|
+
this._lastConnectParams = null; // Connection params for reconnection (per-instance, not shared)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
@@ -98,7 +99,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
98
99
|
// Add sandboxId to every message if we have a connected sandbox
|
|
99
100
|
// This allows the API to reconnect if the connection was rerouted
|
|
100
101
|
// Don't inject IP addresses as sandboxId — only valid instance/sandbox IDs
|
|
101
|
-
if (this._lastConnectParams?.sandboxId && !message.sandboxId) {
|
|
102
|
+
if (this._lastConnectParams?.sandboxId && !message.sandboxId) {
|
|
102
103
|
const id = this._lastConnectParams.sandboxId;
|
|
103
104
|
// Only inject if it looks like a valid ID (not an IP address)
|
|
104
105
|
if (id && !/^\d+\.\d+\.\d+\.\d+$/.test(id)) {
|
|
@@ -198,6 +199,22 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Set connection params for reconnection logic and sandboxId injection.
|
|
204
|
+
* Use this instead of directly assigning this._lastConnectParams from
|
|
205
|
+
* external code. Keeps the shape consistent and avoids stale state
|
|
206
|
+
* leaking across concurrent test runs.
|
|
207
|
+
* @param {Object|null} params
|
|
208
|
+
* @param {string} [params.type] - 'direct' for IP-based connections
|
|
209
|
+
* @param {string} [params.ip] - IP address for direct connections
|
|
210
|
+
* @param {string} [params.sandboxId] - Sandbox/instance ID
|
|
211
|
+
* @param {boolean} [params.persist] - Whether to persist the sandbox
|
|
212
|
+
* @param {number|null} [params.keepAlive] - Keep-alive TTL in ms
|
|
213
|
+
*/
|
|
214
|
+
setConnectionParams(params) {
|
|
215
|
+
this._lastConnectParams = params ? { ...params } : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
201
218
|
async connect(sandboxId, persist = false, keepAlive = null) {
|
|
202
219
|
let reply = await this.send({
|
|
203
220
|
type: "connect",
|
|
@@ -209,14 +226,14 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
209
226
|
if (reply.success) {
|
|
210
227
|
// Only store connection params after successful connection
|
|
211
228
|
// This prevents malformed sandboxId from being attached to subsequent messages
|
|
212
|
-
this.
|
|
229
|
+
this.setConnectionParams({ sandboxId, persist, keepAlive });
|
|
213
230
|
this.instanceSocketConnected = true;
|
|
214
231
|
emitter.emit(events.sandbox.connected);
|
|
215
232
|
// Return the full reply (includes url and sandbox)
|
|
216
233
|
return reply;
|
|
217
234
|
} else {
|
|
218
235
|
// Clear any previous connection params on failure
|
|
219
|
-
this.
|
|
236
|
+
this.setConnectionParams(null);
|
|
220
237
|
// Throw error to trigger fallback to creating new sandbox
|
|
221
238
|
throw new Error(reply.errorMessage || "Failed to connect to sandbox");
|
|
222
239
|
}
|
|
@@ -461,12 +478,17 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
461
478
|
|
|
462
479
|
if (!this.ps[message.requestId]) {
|
|
463
480
|
// This can happen during reconnection (ps was cleared) or after timeout
|
|
464
|
-
// (promise was deleted).
|
|
481
|
+
// (promise was deleted). Expected during polling loops (e.g. Chrome
|
|
482
|
+
// debugger readiness checks) where short-timeout exec calls regularly
|
|
483
|
+
// expire before the sandbox responds. Only log in debug/verbose mode.
|
|
465
484
|
if (!this.reconnecting) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
485
|
+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
486
|
+
if (debugMode) {
|
|
487
|
+
console.warn(
|
|
488
|
+
"No pending promise found for requestId:",
|
|
489
|
+
message.requestId,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
470
492
|
}
|
|
471
493
|
return;
|
|
472
494
|
}
|
|
@@ -528,6 +550,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
528
550
|
this.instanceSocketConnected = false;
|
|
529
551
|
this.authenticated = false;
|
|
530
552
|
this.instance = null;
|
|
553
|
+
this._lastConnectParams = null;
|
|
531
554
|
|
|
532
555
|
// Silently clear pending promises and retry queue without rejecting
|
|
533
556
|
// (rejecting causes unhandled promise rejections during cleanup)
|
package/docs/v7/client.mdx
CHANGED
|
@@ -25,7 +25,7 @@ const testdriver = new TestDriver(apiKey, options)
|
|
|
25
25
|
Configuration options for the client
|
|
26
26
|
|
|
27
27
|
<Expandable title="properties">
|
|
28
|
-
<ParamField path="os" type="string" default="
|
|
28
|
+
<ParamField path="os" type="string" default="linux">
|
|
29
29
|
Operating system for the sandbox: `'windows'` or `'linux'`
|
|
30
30
|
</ParamField>
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ const testdriver = new TestDriver(apiKey, options)
|
|
|
33
33
|
Screen resolution for the sandbox (e.g., `'1920x1080'`, `'1366x768'`)
|
|
34
34
|
</ParamField>
|
|
35
35
|
|
|
36
|
-
<ParamField path="apiRoot" type="string"
|
|
36
|
+
<ParamField path="apiRoot" type="string">
|
|
37
37
|
API endpoint URL (typically only changed for self-hosted deployments)
|
|
38
38
|
</ParamField>
|
|
39
39
|
|
|
@@ -49,6 +49,111 @@ const testdriver = new TestDriver(apiKey, options)
|
|
|
49
49
|
Automatically capture screenshots before and after each command. Screenshots are saved to `.testdriver/screenshots/<test>/` with descriptive filenames that include the line number and action name. Format: `<seq>-<action>-<phase>-L<line>-<description>.png`
|
|
50
50
|
</ParamField>
|
|
51
51
|
|
|
52
|
+
<ParamField path="newSandbox" type="boolean" default="true">
|
|
53
|
+
Force creation of a new sandbox instead of reusing an existing one
|
|
54
|
+
</ParamField>
|
|
55
|
+
|
|
56
|
+
<ParamField path="reconnect" type="boolean" default="false">
|
|
57
|
+
Reconnect to the last used sandbox instead of creating a new one. When `true`, provision methods (`chrome`, `vscode`, `installer`, etc.) will be skipped since the application is already running. Throws error if no previous sandbox exists.
|
|
58
|
+
</ParamField>
|
|
59
|
+
|
|
60
|
+
<ParamField path="keepAlive" type="number" default="60000">
|
|
61
|
+
Keep sandbox alive for the specified number of milliseconds after disconnect. Set to `0` to terminate immediately on disconnect. Useful for debugging or reconnecting to the same sandbox.
|
|
62
|
+
</ParamField>
|
|
63
|
+
|
|
64
|
+
<ParamField path="preview" type="string" default="browser">
|
|
65
|
+
Preview mode for live test visualization:
|
|
66
|
+
- `"browser"` — Opens debugger in default browser (default)
|
|
67
|
+
- `"ide"` — Opens preview in IDE panel (VSCode, Cursor - requires TestDriver extension)
|
|
68
|
+
- `"none"` — Headless mode, no visual preview
|
|
69
|
+
</ParamField>
|
|
70
|
+
|
|
71
|
+
<ParamField path="headless" type="boolean" default="false">
|
|
72
|
+
**Deprecated**: Use `preview: "none"` instead. Run in headless mode without opening the debugger.
|
|
73
|
+
</ParamField>
|
|
74
|
+
|
|
75
|
+
<ParamField path="debugOnFailure" type="boolean" default="false">
|
|
76
|
+
Keep the sandbox alive when a test fails so you can reconnect and debug interactively. The sandbox ID is printed to the console.
|
|
77
|
+
</ParamField>
|
|
78
|
+
|
|
79
|
+
<ParamField path="ip" type="string">
|
|
80
|
+
Direct IP address to connect to a running sandbox instance (for self-hosted deployments)
|
|
81
|
+
</ParamField>
|
|
82
|
+
|
|
83
|
+
<ParamField path="sandboxAmi" type="string">
|
|
84
|
+
Custom AMI ID for the sandbox instance (AWS deployments, e.g., `'ami-1234'`)
|
|
85
|
+
</ParamField>
|
|
86
|
+
|
|
87
|
+
<ParamField path="sandboxInstance" type="string">
|
|
88
|
+
EC2 instance type for the sandbox (AWS deployments, e.g., `'i3.metal'`)
|
|
89
|
+
</ParamField>
|
|
90
|
+
|
|
91
|
+
<ParamField path="cache" type="boolean | object" default="true">
|
|
92
|
+
Enable or disable element caching, or provide advanced threshold configuration.
|
|
93
|
+
|
|
94
|
+
<Expandable title="advanced config">
|
|
95
|
+
<ParamField path="enabled" type="boolean" default="true">
|
|
96
|
+
Enable or disable caching
|
|
97
|
+
</ParamField>
|
|
98
|
+
|
|
99
|
+
<ParamField path="thresholds" type="object">
|
|
100
|
+
Fine-tune cache matching
|
|
101
|
+
|
|
102
|
+
<Expandable title="properties">
|
|
103
|
+
<ParamField path="find" type="object">
|
|
104
|
+
Thresholds for `find()` operations
|
|
105
|
+
|
|
106
|
+
<Expandable title="properties">
|
|
107
|
+
<ParamField path="screen" type="number" default="0.05">
|
|
108
|
+
Pixel diff threshold for screen comparison (0-1). `0.05` = 5% diff allowed.
|
|
109
|
+
</ParamField>
|
|
110
|
+
|
|
111
|
+
<ParamField path="element" type="number" default="0.8">
|
|
112
|
+
OpenCV template match threshold for element matching (0-1). `0.8` = 80% correlation.
|
|
113
|
+
</ParamField>
|
|
114
|
+
</Expandable>
|
|
115
|
+
</ParamField>
|
|
116
|
+
|
|
117
|
+
<ParamField path="assert" type="number" default="0.05">
|
|
118
|
+
Pixel diff threshold for `assert()` operations (0-1). `0.05` = 5% diff allowed.
|
|
119
|
+
</ParamField>
|
|
120
|
+
</Expandable>
|
|
121
|
+
</ParamField>
|
|
122
|
+
</Expandable>
|
|
123
|
+
</ParamField>
|
|
124
|
+
|
|
125
|
+
<ParamField path="cacheKey" type="string">
|
|
126
|
+
Cache key for element finding operations. If provided, enables caching tied to this key.
|
|
127
|
+
</ParamField>
|
|
128
|
+
|
|
129
|
+
<ParamField path="dashcam" type="boolean" default="true">
|
|
130
|
+
Enable or disable Dashcam video recording
|
|
131
|
+
</ParamField>
|
|
132
|
+
|
|
133
|
+
<ParamField path="redraw" type="boolean | object" default="true">
|
|
134
|
+
Enable or disable screen-change (redraw) detection, or provide advanced configuration.
|
|
135
|
+
|
|
136
|
+
<Expandable title="advanced config">
|
|
137
|
+
<ParamField path="enabled" type="boolean" default="true">
|
|
138
|
+
Enable or disable redraw detection
|
|
139
|
+
</ParamField>
|
|
140
|
+
|
|
141
|
+
<ParamField path="thresholds" type="object">
|
|
142
|
+
Threshold configuration
|
|
143
|
+
|
|
144
|
+
<Expandable title="properties">
|
|
145
|
+
<ParamField path="screen" type="number | false" default="0.05">
|
|
146
|
+
Pixel diff threshold (0-1). Set to `false` to disable screen redraw detection.
|
|
147
|
+
</ParamField>
|
|
148
|
+
|
|
149
|
+
<ParamField path="network" type="boolean" default="false">
|
|
150
|
+
Enable or disable network activity monitoring
|
|
151
|
+
</ParamField>
|
|
152
|
+
</Expandable>
|
|
153
|
+
</ParamField>
|
|
154
|
+
</Expandable>
|
|
155
|
+
</ParamField>
|
|
156
|
+
|
|
52
157
|
<ParamField path="environment" type="object">
|
|
53
158
|
Additional environment variables to pass to the sandbox
|
|
54
159
|
</ParamField>
|
|
@@ -10,7 +10,64 @@ Configure TestDriver behavior with options passed to the `TestDriver()` function
|
|
|
10
10
|
|
|
11
11
|
```javascript
|
|
12
12
|
const testdriver = TestDriver(context, {
|
|
13
|
-
|
|
13
|
+
// === Sandbox & Connection ===
|
|
14
|
+
newSandbox: true, // Force creation of a new sandbox (default: true)
|
|
15
|
+
reconnect: false, // Reconnect to last sandbox (default: false)
|
|
16
|
+
keepAlive: 60000, // Keep sandbox alive after disconnect in ms (default: 60000)
|
|
17
|
+
os: "linux", // 'linux' | 'windows' (default: 'linux')
|
|
18
|
+
resolution: "1366x768", // Sandbox resolution (e.g., '1920x1080')
|
|
19
|
+
ip: "203.0.113.42", // Direct IP for self-hosted sandbox
|
|
20
|
+
sandboxAmi: "ami-1234", // Custom AMI ID (AWS deployments)
|
|
21
|
+
sandboxInstance: "i3.metal", // EC2 instance type (AWS deployments)
|
|
22
|
+
|
|
23
|
+
// === Preview & Debugging ===
|
|
24
|
+
preview: "browser", // "browser" | "ide" | "none" (default: "browser")
|
|
25
|
+
headless: false, // @deprecated - use preview: "none" instead
|
|
26
|
+
debugOnFailure: false, // Keep sandbox alive on test failure for debugging
|
|
27
|
+
|
|
28
|
+
// === Caching ===
|
|
29
|
+
cache: true, // Enable element caching (default: true)
|
|
30
|
+
// Or use advanced caching config:
|
|
31
|
+
// cache: {
|
|
32
|
+
// enabled: true,
|
|
33
|
+
// thresholds: {
|
|
34
|
+
// find: { screen: 0.05, element: 0.8 },
|
|
35
|
+
// assert: 0.05
|
|
36
|
+
// }
|
|
37
|
+
// },
|
|
38
|
+
cacheKey: "my-test", // Cache key for element finding operations
|
|
39
|
+
|
|
40
|
+
// === Recording & Screenshots ===
|
|
41
|
+
dashcam: true, // Enable/disable Dashcam video recording (default: true)
|
|
42
|
+
autoScreenshots: true, // Capture screenshots before/after each command (default: true)
|
|
43
|
+
|
|
44
|
+
// === AI Configuration ===
|
|
45
|
+
ai: { // Global AI sampling configuration
|
|
46
|
+
temperature: 0, // 0 = deterministic, higher = more creative
|
|
47
|
+
top: {
|
|
48
|
+
p: 0.9, // Top-P nucleus sampling (0-1)
|
|
49
|
+
k: 40, // Top-K sampling (1 = most likely, 0 = disabled)
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// === Screen Change Detection ===
|
|
54
|
+
redraw: true, // Enable redraw detection (default: true)
|
|
55
|
+
// Or use advanced redraw config:
|
|
56
|
+
// redraw: {
|
|
57
|
+
// enabled: true,
|
|
58
|
+
// thresholds: {
|
|
59
|
+
// screen: 0.05, // Pixel diff threshold (0-1), false to disable
|
|
60
|
+
// network: false, // Monitor network activity (default: false)
|
|
61
|
+
// }
|
|
62
|
+
// },
|
|
63
|
+
|
|
64
|
+
// === Logging & Analytics ===
|
|
65
|
+
logging: true, // Enable console logging output (default: true)
|
|
66
|
+
analytics: true, // Enable analytics tracking (default: true)
|
|
67
|
+
|
|
68
|
+
// === Advanced ===
|
|
69
|
+
apiRoot: "https://...", // API endpoint URL (for self-hosted deployments)
|
|
70
|
+
environment: {}, // Additional environment variables for the sandbox
|
|
14
71
|
});
|
|
15
72
|
```
|
|
16
73
|
|
|
@@ -54,6 +111,16 @@ const testdriver = TestDriver(context, {
|
|
|
54
111
|
The legacy `headless: true` option still works for backward compatibility and maps to `preview: "none"`.
|
|
55
112
|
</Note>
|
|
56
113
|
|
|
114
|
+
### Debug on Failure
|
|
115
|
+
|
|
116
|
+
Keep the sandbox alive when a test fails so you can reconnect and debug interactively. The sandbox ID is printed to the console along with instructions for reconnecting via MCP.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const testdriver = TestDriver(context, {
|
|
120
|
+
debugOnFailure: true,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
57
124
|
### IP Target
|
|
58
125
|
|
|
59
126
|
If self-hosting TestDriver, use `ip` to specify the device IP. See [Self-Hosting TestDriver](../self-hosting.md) for details.
|
|
@@ -105,11 +172,102 @@ steps:
|
|
|
105
172
|
- run: TD_OS=${{ matrix.os }} vitest run
|
|
106
173
|
```
|
|
107
174
|
|
|
108
|
-
|
|
175
|
+
### Dashcam Recording
|
|
176
|
+
|
|
177
|
+
Dashcam video recording is enabled by default. Disable it to skip recording:
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
const testdriver = TestDriver(context, {
|
|
181
|
+
dashcam: false,
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Automatic Screenshots
|
|
186
|
+
|
|
187
|
+
Screenshots are automatically captured before and after every command (click, type, find, assert, etc.) by default. Each screenshot filename includes the line number from your test file.
|
|
188
|
+
|
|
189
|
+
Disable automatic screenshots:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const testdriver = TestDriver(context, {
|
|
193
|
+
autoScreenshots: false,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Caching
|
|
198
|
+
|
|
199
|
+
Element caching speeds up repeated `find()` and `assert()` calls. Enabled by default.
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
// Disable caching
|
|
203
|
+
const testdriver = TestDriver(context, {
|
|
204
|
+
cache: false,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Advanced: custom thresholds
|
|
208
|
+
const testdriver = TestDriver(context, {
|
|
209
|
+
cache: {
|
|
210
|
+
enabled: true,
|
|
211
|
+
thresholds: {
|
|
212
|
+
find: { screen: 0.05, element: 0.8 },
|
|
213
|
+
assert: 0.05,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
cacheKey: "my-test",
|
|
217
|
+
});
|
|
218
|
+
```
|
|
109
219
|
|
|
110
|
-
|
|
220
|
+
### Redraw Detection
|
|
111
221
|
|
|
112
|
-
|
|
222
|
+
Redraw detection waits for the screen to stabilize before taking actions. Enabled by default.
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// Disable redraw detection
|
|
226
|
+
const testdriver = TestDriver(context, {
|
|
227
|
+
redraw: false,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Advanced: custom thresholds with network monitoring
|
|
231
|
+
const testdriver = TestDriver(context, {
|
|
232
|
+
redraw: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
thresholds: {
|
|
235
|
+
screen: 0.05,
|
|
236
|
+
network: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### AI Configuration
|
|
243
|
+
|
|
244
|
+
Control how the AI model generates responses for `find()` verification and `assert()` calls:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const testdriver = TestDriver(context, {
|
|
248
|
+
ai: {
|
|
249
|
+
temperature: 0, // 0 = deterministic
|
|
250
|
+
top: { p: 0.9, k: 40 },
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Environment Variables
|
|
256
|
+
|
|
257
|
+
Pass additional environment variables to the sandbox:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const testdriver = TestDriver(context, {
|
|
261
|
+
environment: {
|
|
262
|
+
MY_VAR: "value",
|
|
263
|
+
DEBUG: "true",
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Keepalive
|
|
269
|
+
|
|
270
|
+
By default, sandboxes stay alive for 60 seconds after disconnect. Customize this with `keepAlive`:
|
|
113
271
|
|
|
114
272
|
```javascript
|
|
115
273
|
const testdriver = TestDriver(context, {
|
|
@@ -117,6 +275,14 @@ const testdriver = TestDriver(context, {
|
|
|
117
275
|
});
|
|
118
276
|
```
|
|
119
277
|
|
|
278
|
+
Set to `0` to terminate immediately:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const testdriver = TestDriver(context, {
|
|
282
|
+
keepAlive: 0, // Terminate sandbox immediately on disconnect
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
120
286
|
### Reconnecting to Existing Sandbox
|
|
121
287
|
|
|
122
288
|
Speed up test development by reconnecting to an existing sandbox instead of starting fresh each time. This lets you iterate quickly on failing steps without re-running the entire test from the beginning.
|
|
@@ -65,31 +65,31 @@ describe("Chrome Extension Test", () => {
|
|
|
65
65
|
expect(popupResult).toBeTruthy();
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it("should load Loom from Chrome Web Store by extensionId", async (context) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
68
|
+
// it("should load Loom from Chrome Web Store by extensionId", async (context) => {
|
|
69
|
+
// const testdriver = TestDriver(context, { ...getDefaults(context) });
|
|
70
|
+
|
|
71
|
+
// // Launch Chrome with Loom loaded by its Chrome Web Store ID
|
|
72
|
+
// // Loom ID: liecbddmkiiihnedobmlmillhodjkdmb
|
|
73
|
+
// await testdriver.provision.chromeExtension({
|
|
74
|
+
// extensionId: 'liecbddmkiiihnedobmlmillhodjkdmb'
|
|
75
|
+
// });
|
|
76
|
+
|
|
77
|
+
// // Navigate to testdriver.ai (extensions don't load on New Tab)
|
|
78
|
+
// const addressBar = await testdriver.find("Chrome address bar");
|
|
79
|
+
// await addressBar.click();
|
|
80
|
+
// await testdriver.type("testdriver.ai");
|
|
81
|
+
// await testdriver.pressKeys(["enter"]);
|
|
82
|
+
|
|
83
|
+
// // Wait for page to load
|
|
84
|
+
// const pageResult = await testdriver.assert("I can see testdriver.ai");
|
|
85
|
+
// expect(pageResult).toBeTruthy();
|
|
86
|
+
|
|
87
|
+
// // Click on the extensions button (puzzle piece icon) in Chrome toolbar
|
|
88
|
+
// const extensionsButton = await testdriver.find("The puzzle-shaped icon in the Chrome toolbar.", {zoom: true});
|
|
89
|
+
// await extensionsButton.click();
|
|
90
|
+
|
|
91
|
+
// // Look for Loom in the extensions menu
|
|
92
|
+
// const loomExtension = await testdriver.find("Loom extension in the extensions dropdown");
|
|
93
|
+
// expect(loomExtension.found()).toBeTruthy();
|
|
94
|
+
// });
|
|
95
95
|
});
|
|
@@ -1142,7 +1142,10 @@ class TestDriverReporter {
|
|
|
1142
1142
|
|
|
1143
1143
|
const suiteName = test.suite?.name;
|
|
1144
1144
|
const startTime = Date.now() - duration; // Calculate start time from duration
|
|
1145
|
-
|
|
1145
|
+
// In Vitest v4, retryCount is on diagnostic(), not result()
|
|
1146
|
+
// result() only returns { state, errors }, while diagnostic() has retryCount, duration, etc.
|
|
1147
|
+
const diagnostic = test.diagnostic?.();
|
|
1148
|
+
const retryCount = diagnostic?.retryCount || 0;
|
|
1146
1149
|
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
1147
1150
|
const consoleUrl = getConsoleUrl(pluginState.apiRoot);
|
|
1148
1151
|
const hasRetries = retryCount > 0 && dashcamUrls.length > 1;
|
package/lib/sentry.js
CHANGED
|
@@ -16,7 +16,11 @@ const os = require("os");
|
|
|
16
16
|
const { version } = require("../package.json");
|
|
17
17
|
const logger = require("../agent/lib/logger");
|
|
18
18
|
|
|
19
|
-
// Store
|
|
19
|
+
// Store trace contexts per session so concurrent tests don't overwrite each other.
|
|
20
|
+
// Keys are sessionIds, values are { traceId, sessionId }.
|
|
21
|
+
const _traceContexts = new Map();
|
|
22
|
+
|
|
23
|
+
// For backward compatibility, track the most recently set session
|
|
20
24
|
let currentTraceId = null;
|
|
21
25
|
let currentSessionId = null;
|
|
22
26
|
|
|
@@ -177,7 +181,13 @@ function setSessionTraceContext(sessionId) {
|
|
|
177
181
|
if (!isEnabled() || !sessionId) return;
|
|
178
182
|
|
|
179
183
|
// Derive trace ID from session ID (same algorithm as API)
|
|
180
|
-
|
|
184
|
+
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
185
|
+
|
|
186
|
+
// Store per-session trace context for concurrent safety
|
|
187
|
+
_traceContexts.set(sessionId, { traceId, sessionId });
|
|
188
|
+
|
|
189
|
+
// Also update the module-level "latest" for backward compatibility
|
|
190
|
+
currentTraceId = traceId;
|
|
181
191
|
currentSessionId = sessionId;
|
|
182
192
|
|
|
183
193
|
// Set as global tag so all events include it
|
|
@@ -203,9 +213,26 @@ function setSessionTraceContext(sessionId) {
|
|
|
203
213
|
/**
|
|
204
214
|
* Clear the session trace context
|
|
205
215
|
*/
|
|
206
|
-
function clearSessionTraceContext() {
|
|
207
|
-
|
|
208
|
-
|
|
216
|
+
function clearSessionTraceContext(sessionId) {
|
|
217
|
+
if (sessionId) {
|
|
218
|
+
_traceContexts.delete(sessionId);
|
|
219
|
+
// If the cleared session was the "latest", pick another or null
|
|
220
|
+
if (currentSessionId === sessionId) {
|
|
221
|
+
if (_traceContexts.size > 0) {
|
|
222
|
+
const last = Array.from(_traceContexts.values()).pop();
|
|
223
|
+
currentTraceId = last.traceId;
|
|
224
|
+
currentSessionId = last.sessionId;
|
|
225
|
+
} else {
|
|
226
|
+
currentTraceId = null;
|
|
227
|
+
currentSessionId = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
// Clear all (backward compatibility)
|
|
232
|
+
_traceContexts.clear();
|
|
233
|
+
currentTraceId = null;
|
|
234
|
+
currentSessionId = null;
|
|
235
|
+
}
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
/**
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -131,7 +131,10 @@ function forwardToAllSandboxes(args) {
|
|
|
131
131
|
* reporter output).
|
|
132
132
|
*/
|
|
133
133
|
function installConsoleSpy() {
|
|
134
|
-
|
|
134
|
+
// Check both installed flag AND that spies are still valid.
|
|
135
|
+
// Guards against a race where cleanupConsoleSpy restores mocks (setting
|
|
136
|
+
// installed=false) while a new test is starting up concurrently.
|
|
137
|
+
if (_consoleSpy.installed && _consoleSpy.spies) return;
|
|
135
138
|
_consoleSpy.installed = true;
|
|
136
139
|
|
|
137
140
|
// Capture originals once — these are whatever console methods look like
|
package/lib/vitest/setup-aws.mjs
CHANGED
|
@@ -140,6 +140,14 @@ beforeEach(async (context) => {
|
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// If ip is provided via plugin options, skip spawning
|
|
144
|
+
const pluginIp = globalThis.__testdriverPlugin?.state?.testDriverOptions?.ip;
|
|
145
|
+
if (pluginIp) {
|
|
146
|
+
console.log(`[TestDriver] Using ip from plugin options: ${pluginIp}`);
|
|
147
|
+
context.ip = pluginIp;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
if (!process.env.AWS_LAUNCH_TEMPLATE_ID || !process.env.AMI_ID) {
|
|
144
152
|
throw new Error(
|
|
145
153
|
"[TestDriver] TD_OS=windows requires AWS_LAUNCH_TEMPLATE_ID and AMI_ID environment variables",
|
|
@@ -81,6 +81,21 @@ beforeEach(async (context) => {
|
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// If TD_IP is already set, use it and skip spawning
|
|
85
|
+
if (process.env.TD_IP) {
|
|
86
|
+
console.log(`[TestDriver] Using existing instance at ${process.env.TD_IP}`);
|
|
87
|
+
context.ip = process.env.TD_IP;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If ip is provided via plugin options, skip spawning
|
|
92
|
+
const pluginIp = globalThis.__testdriverPlugin?.state?.testDriverOptions?.ip;
|
|
93
|
+
if (pluginIp) {
|
|
94
|
+
console.log(`[TestDriver] Using ip from plugin options: ${pluginIp}`);
|
|
95
|
+
context.ip = pluginIp;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
// Verify AWS credentials are available
|
|
85
100
|
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_LAUNCH_TEMPLATE_ID || !process.env.AMI_ID) {
|
|
86
101
|
throw new Error('[TestDriver] TD_OS=windows requires AWS credentials (AWS_ACCESS_KEY_ID, AWS_LAUNCH_TEMPLATE_ID, AMI_ID)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.35",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "sdk.js",
|
|
6
6
|
"types": "sdk.d.ts",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"./vitest/setup": "./lib/vitest/setup.mjs",
|
|
25
25
|
"./vitest/setup-aws": "./lib/vitest/setup-aws.mjs",
|
|
26
|
-
"./vitest/setup-disable-defender": "./lib/vitest/setup-disable-defender.mjs",
|
|
27
26
|
"./vitest/hooks": {
|
|
28
27
|
"types": "./lib/vitest/hooks.d.ts",
|
|
29
28
|
"default": "./lib/vitest/hooks.mjs"
|
package/sdk.js
CHANGED
|
@@ -1399,6 +1399,10 @@ const TestDriverAgent = require("./agent/index.js");
|
|
|
1399
1399
|
const { events } = require("./agent/events.js");
|
|
1400
1400
|
const { createMarkdownLogger } = require("./interfaces/logger.js");
|
|
1401
1401
|
|
|
1402
|
+
// Track screenshot directories already cleaned in this process to avoid
|
|
1403
|
+
// concurrent tests in the same file from nuking each other's screenshots.
|
|
1404
|
+
const _cleanedScreenshotDirs = new Set();
|
|
1405
|
+
|
|
1402
1406
|
class TestDriverSDK {
|
|
1403
1407
|
constructor(apiKey, options = {}) {
|
|
1404
1408
|
// Support calling with just options: new TestDriver({ os: 'windows' })
|
|
@@ -1432,6 +1436,12 @@ class TestDriverSDK {
|
|
|
1432
1436
|
...options.environment,
|
|
1433
1437
|
};
|
|
1434
1438
|
|
|
1439
|
+
// Auto-detect CI environment (GitHub Actions, etc.) and pass through
|
|
1440
|
+
// This ensures the API creates fresh sandboxes instead of reusing hot-pool instances
|
|
1441
|
+
if (!environment.CI && process.env.CI) {
|
|
1442
|
+
environment.CI = process.env.CI;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1435
1445
|
// Create the underlying agent with minimal CLI args
|
|
1436
1446
|
this.agent = new TestDriverAgent(environment, {
|
|
1437
1447
|
command: "sdk",
|
|
@@ -1710,22 +1720,29 @@ class TestDriverSDK {
|
|
|
1710
1720
|
* @param {number} [timeoutMs=60000] - Maximum time to wait in ms
|
|
1711
1721
|
* @returns {Promise<void>}
|
|
1712
1722
|
*/
|
|
1713
|
-
async _waitForChromeDebuggerReady(timeoutMs =
|
|
1723
|
+
async _waitForChromeDebuggerReady(timeoutMs = 60000) {
|
|
1714
1724
|
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1715
1725
|
const portCheckCmd = this.os === "windows"
|
|
1716
1726
|
? `$tcp = New-Object System.Net.Sockets.TcpClient; $tcp.Connect('127.0.0.1', 9222); $tcp.Close(); echo 'open'`
|
|
1717
1727
|
: `curl -s -o /dev/null --connect-timeout 2 http://localhost:9222 2>/dev/null && echo 'open' || echo 'closed'`;
|
|
1718
|
-
const pageCheckCmd = this.os === "windows"
|
|
1719
|
-
? `(Invoke-RestMethod -Uri 'http://localhost:9222/json' -TimeoutSec 2) | Where-Object { $_.type -eq 'page' } | Select-Object -First 1 | ConvertTo-Json`
|
|
1720
|
-
: `curl -s http://localhost:9222/json 2>/dev/null | grep '"type": "page"'`;
|
|
1721
1728
|
|
|
1722
1729
|
const deadline = Date.now() + timeoutMs;
|
|
1723
1730
|
|
|
1731
|
+
// Use commands.exec directly to bypass auto-screenshots wrapper.
|
|
1732
|
+
// The polling loop fires many rapid exec calls with short timeouts;
|
|
1733
|
+
// going through the wrapper adds 2-3 extra sandbox messages
|
|
1734
|
+
// (screenshot before/after/error) per iteration, overwhelming the
|
|
1735
|
+
// WebSocket and generating cascading "No pending promise" warnings
|
|
1736
|
+
// when timed-out responses arrive after the promise has been cleaned up.
|
|
1737
|
+
const execDirect = this.commands?.exec
|
|
1738
|
+
? (...args) => this.commands.exec(...args)
|
|
1739
|
+
: (...args) => this.exec(...args); // fallback if commands not ready
|
|
1740
|
+
|
|
1724
1741
|
// Wait for port 9222 to be listening
|
|
1725
1742
|
let portReady = false;
|
|
1726
1743
|
while (Date.now() < deadline) {
|
|
1727
1744
|
try {
|
|
1728
|
-
const result = await
|
|
1745
|
+
const result = await execDirect(shell, portCheckCmd, 10000, true);
|
|
1729
1746
|
if (result && result.includes("open")) {
|
|
1730
1747
|
portReady = true;
|
|
1731
1748
|
break;
|
|
@@ -1733,7 +1750,7 @@ class TestDriverSDK {
|
|
|
1733
1750
|
} catch (_) {
|
|
1734
1751
|
// Port not ready yet
|
|
1735
1752
|
}
|
|
1736
|
-
await new Promise((r) => setTimeout(r,
|
|
1753
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1737
1754
|
}
|
|
1738
1755
|
if (!portReady) {
|
|
1739
1756
|
throw new Error(
|
|
@@ -1741,25 +1758,6 @@ class TestDriverSDK {
|
|
|
1741
1758
|
);
|
|
1742
1759
|
}
|
|
1743
1760
|
|
|
1744
|
-
// Wait for a page target to appear via CDP
|
|
1745
|
-
let pageReady = false;
|
|
1746
|
-
while (Date.now() < deadline) {
|
|
1747
|
-
try {
|
|
1748
|
-
const result = await this.exec(shell, pageCheckCmd, 5000, true);
|
|
1749
|
-
if (result && result.trim().length > 0) {
|
|
1750
|
-
pageReady = true;
|
|
1751
|
-
break;
|
|
1752
|
-
}
|
|
1753
|
-
} catch (_) {
|
|
1754
|
-
// No page target yet
|
|
1755
|
-
}
|
|
1756
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1757
|
-
}
|
|
1758
|
-
if (!pageReady) {
|
|
1759
|
-
throw new Error(
|
|
1760
|
-
`Chrome page target did not become available within ${timeoutMs}ms`,
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
1761
|
}
|
|
1764
1762
|
|
|
1765
1763
|
_createProvisionAPI() {
|
|
@@ -2706,7 +2704,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2706
2704
|
);
|
|
2707
2705
|
}
|
|
2708
2706
|
|
|
2709
|
-
// Clean up screenshots folder for this test file before running
|
|
2707
|
+
// Clean up screenshots folder for this test file before running.
|
|
2708
|
+
// Only clean once per directory per process to avoid concurrent tests
|
|
2709
|
+
// in the same file (--sequence.concurrent) from nuking each other's screenshots.
|
|
2710
2710
|
if (this.testFile) {
|
|
2711
2711
|
const testFileName = path.basename(
|
|
2712
2712
|
this.testFile,
|
|
@@ -2718,8 +2718,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2718
2718
|
"screenshots",
|
|
2719
2719
|
testFileName,
|
|
2720
2720
|
);
|
|
2721
|
-
if (
|
|
2722
|
-
|
|
2721
|
+
if (!_cleanedScreenshotDirs.has(screenshotsDir)) {
|
|
2722
|
+
_cleanedScreenshotDirs.add(screenshotsDir);
|
|
2723
|
+
if (fs.existsSync(screenshotsDir)) {
|
|
2724
|
+
fs.rmSync(screenshotsDir, { recursive: true, force: true });
|
|
2725
|
+
}
|
|
2723
2726
|
}
|
|
2724
2727
|
}
|
|
2725
2728
|
|
|
@@ -2746,33 +2749,6 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2746
2749
|
: this.newSandbox,
|
|
2747
2750
|
};
|
|
2748
2751
|
|
|
2749
|
-
// Handle reconnect option - use last sandbox file
|
|
2750
|
-
// Check both connectOptions and constructor options
|
|
2751
|
-
const shouldReconnect =
|
|
2752
|
-
connectOptions.reconnect !== undefined
|
|
2753
|
-
? connectOptions.reconnect
|
|
2754
|
-
: this.reconnect;
|
|
2755
|
-
|
|
2756
|
-
// Skip reconnect if IP is supplied - directly connect to the provided IP
|
|
2757
|
-
const hasIp = Boolean(connectOptions.ip || this.ip);
|
|
2758
|
-
|
|
2759
|
-
if (shouldReconnect && !hasIp) {
|
|
2760
|
-
const lastSandbox = this.agent.getLastSandboxId();
|
|
2761
|
-
if (!lastSandbox || !lastSandbox.sandboxId) {
|
|
2762
|
-
throw new Error(
|
|
2763
|
-
"Cannot reconnect: No previous sandbox found. Run a test first to create a sandbox, or remove the reconnect option.",
|
|
2764
|
-
);
|
|
2765
|
-
}
|
|
2766
|
-
this.agent.sandboxId = lastSandbox.sandboxId;
|
|
2767
|
-
buildEnvOptions.new = false;
|
|
2768
|
-
|
|
2769
|
-
// Use OS from last sandbox if not explicitly specified
|
|
2770
|
-
if (!connectOptions.os && lastSandbox.os) {
|
|
2771
|
-
this.agent.sandboxOs = lastSandbox.os;
|
|
2772
|
-
this.os = lastSandbox.os;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
2752
|
// Set agent properties for buildEnv to use
|
|
2777
2753
|
if (connectOptions.sandboxId) {
|
|
2778
2754
|
this.agent.sandboxId = connectOptions.sandboxId;
|
|
@@ -2883,10 +2859,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2883
2859
|
}
|
|
2884
2860
|
}
|
|
2885
2861
|
|
|
2886
|
-
//
|
|
2862
|
+
// Release our reference to the shared debugger server.
|
|
2863
|
+
// The server only actually stops when the last concurrent test disconnects.
|
|
2887
2864
|
try {
|
|
2888
|
-
const {
|
|
2889
|
-
|
|
2865
|
+
const { releaseDebugger } = require("./agent/lib/debugger-server.js");
|
|
2866
|
+
releaseDebugger();
|
|
2890
2867
|
} catch (err) {
|
|
2891
2868
|
// Ignore if debugger wasn't started
|
|
2892
2869
|
}
|
|
@@ -2916,14 +2893,6 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2916
2893
|
return this.session?.get() || null;
|
|
2917
2894
|
}
|
|
2918
2895
|
|
|
2919
|
-
/**
|
|
2920
|
-
* Get the last sandbox info from the stored file
|
|
2921
|
-
* @returns {Object|null} Last sandbox info including sandboxId, os, ami, instanceType, timestamp, or null if not found
|
|
2922
|
-
*/
|
|
2923
|
-
getLastSandboxId() {
|
|
2924
|
-
return this.agent.getLastSandboxId();
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
2896
|
// ====================================
|
|
2928
2897
|
// Element Finding API
|
|
2929
2898
|
// ====================================
|
|
@@ -3924,16 +3893,13 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3924
3893
|
* @private
|
|
3925
3894
|
*/
|
|
3926
3895
|
async _initializeDebugger() {
|
|
3927
|
-
//
|
|
3928
|
-
|
|
3896
|
+
// Use reference-counted debugger server so concurrent tests share one
|
|
3897
|
+
// server and it only shuts down when the last test disconnects.
|
|
3898
|
+
const { acquireDebugger } = require("./agent/lib/debugger-server.js");
|
|
3929
3899
|
|
|
3930
|
-
// Only initialize once
|
|
3931
3900
|
if (!this.agent.debuggerUrl) {
|
|
3932
|
-
const
|
|
3933
|
-
|
|
3934
|
-
this.emitter,
|
|
3935
|
-
);
|
|
3936
|
-
this.agent.debuggerUrl = debuggerProcess.url || null;
|
|
3901
|
+
const result = await acquireDebugger(this.config, this.emitter);
|
|
3902
|
+
this.agent.debuggerUrl = result.url || null;
|
|
3937
3903
|
}
|
|
3938
3904
|
}
|
|
3939
3905
|
|
package/vitest.config.mjs
CHANGED
|
@@ -5,17 +5,17 @@ import { defineConfig } from "vitest/config";
|
|
|
5
5
|
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
6
6
|
const setupFiles = [
|
|
7
7
|
"testdriverai/vitest/setup",
|
|
8
|
-
"testdriverai/vitest/setup-aws"
|
|
9
|
-
'testdriverai/vitest/setup-disable-defender'
|
|
8
|
+
"testdriverai/vitest/setup-aws"
|
|
10
9
|
];
|
|
11
10
|
|
|
12
11
|
export default defineConfig({
|
|
13
12
|
test: {
|
|
14
|
-
retry:
|
|
13
|
+
retry: 0,
|
|
15
14
|
testTimeout: 900000,
|
|
16
15
|
hookTimeout: 900000,
|
|
17
16
|
disableConsoleIntercept: true,
|
|
18
17
|
maxConcurrency: 100,
|
|
18
|
+
maxWorkers: 16,
|
|
19
19
|
reporters: [
|
|
20
20
|
"default",
|
|
21
21
|
TestDriver(),
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-spawn hook to disable Windows Defender
|
|
3
|
-
*
|
|
4
|
-
* Usage in vitest.config.mjs:
|
|
5
|
-
* ```js
|
|
6
|
-
* setupFiles: [
|
|
7
|
-
* 'testdriverai/vitest/setup',
|
|
8
|
-
* 'testdriverai/vitest/setup-aws',
|
|
9
|
-
* 'testdriverai/vitest/setup-disable-defender'
|
|
10
|
-
* ]
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { execSync } from 'child_process';
|
|
15
|
-
import { dirname, join } from 'path';
|
|
16
|
-
import { fileURLToPath } from 'url';
|
|
17
|
-
import { beforeEach } from 'vitest';
|
|
18
|
-
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname = dirname(__filename);
|
|
21
|
-
|
|
22
|
-
beforeEach(async (context) => {
|
|
23
|
-
// Only run if we have an instance IP (self-hosted mode)
|
|
24
|
-
if (!context.ip) return;
|
|
25
|
-
|
|
26
|
-
// Get instance ID from global state set by setup-aws
|
|
27
|
-
const instanceInfo = globalThis.__testdriverAWS?.instances?.get(context.task.id);
|
|
28
|
-
if (!instanceInfo?.instanceId) {
|
|
29
|
-
console.warn('[TestDriver] No instance ID found, skipping Defender disable');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const { instanceId, awsRegion } = instanceInfo;
|
|
34
|
-
const scriptPath = join(__dirname, '../../setup/aws/disable-defender.sh');
|
|
35
|
-
|
|
36
|
-
console.log(`[TestDriver] Disabling Windows Defender on ${instanceId}...`);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
execSync(`bash ${scriptPath}`, {
|
|
40
|
-
encoding: 'utf-8',
|
|
41
|
-
env: {
|
|
42
|
-
...process.env,
|
|
43
|
-
AWS_REGION: awsRegion,
|
|
44
|
-
INSTANCE_ID: instanceId,
|
|
45
|
-
},
|
|
46
|
-
stdio: 'inherit',
|
|
47
|
-
});
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn('[TestDriver] Failed to disable Defender:', error.message);
|
|
50
|
-
// Don't throw - this is optional optimization
|
|
51
|
-
}
|
|
52
|
-
});
|