testdriverai 7.3.34 → 7.3.36

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/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 = 300000) {
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 this.exec(shell, portCheckCmd, 5000, true);
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, 200));
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 (fs.existsSync(screenshotsDir)) {
2722
- fs.rmSync(screenshotsDir, { recursive: true, force: true });
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
- // Stop the debugger server (HTTP + WebSocket server) to release the port
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 { stopDebugger } = require("./agent/lib/debugger-server.js");
2889
- stopDebugger();
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
- // Import createDebuggerProcess at the module level if not already done
3928
- const { createDebuggerProcess } = require("./agent/lib/debugger.js");
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 debuggerProcess = await createDebuggerProcess(
3933
- this.config,
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: 1,
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
- });