@testdriverai/mcp 7.10.0-canary.0 → 7.10.1-test

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.
Files changed (196) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/agent/index.js +116 -71
  3. package/agent/interface.js +13 -1
  4. package/agent/lib/commands.js +35 -15
  5. package/agent/lib/provision-commands.js +92 -11
  6. package/agent/lib/sandbox.js +263 -30
  7. package/agent/lib/sdk.js +16 -12
  8. package/agent/lib/system.js +107 -31
  9. package/ai/.claude-plugin/plugin.json +89 -4
  10. package/ai/agents/testdriver.md +4 -8
  11. package/ai/skills/testdriver-assert/SKILL.md +0 -1
  12. package/ai/skills/testdriver-aws-setup/SKILL.md +1 -1
  13. package/ai/skills/testdriver-caching/SKILL.md +13 -6
  14. package/ai/skills/testdriver-ci-cd/SKILL.md +57 -14
  15. package/ai/skills/testdriver-client/SKILL.md +10 -6
  16. package/ai/skills/testdriver-customizing-devices/SKILL.md +7 -5
  17. package/ai/skills/testdriver-dashcam/SKILL.md +36 -3
  18. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +28 -4
  19. package/ai/skills/testdriver-device-config/SKILL.md +5 -5
  20. package/ai/skills/testdriver-find/SKILL.md +50 -11
  21. package/ai/skills/testdriver-generating-tests/SKILL.md +163 -8
  22. package/ai/skills/testdriver-hosted/SKILL.md +1 -1
  23. package/ai/skills/testdriver-machine-setup/SKILL.md +331 -0
  24. package/ai/skills/testdriver-making-assertions/SKILL.md +78 -2
  25. package/ai/skills/testdriver-mcp-workflow/SKILL.md +2 -2
  26. package/ai/skills/testdriver-parse/SKILL.md +1 -1
  27. package/ai/skills/testdriver-performing-actions/SKILL.md +97 -3
  28. package/ai/skills/testdriver-provision/SKILL.md +0 -20
  29. package/ai/skills/testdriver-quickstart/SKILL.md +299 -21
  30. package/ai/skills/testdriver-reusable-code/SKILL.md +3 -3
  31. package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
  32. package/ai/skills/testdriver-screenshot/SKILL.md +5 -5
  33. package/ai/skills/testdriver-scroll/SKILL.md +30 -160
  34. package/ai/skills/testdriver-test-writer/SKILL.md +3 -5
  35. package/ai/skills/testdriver-testdriver/SKILL.md +3 -7
  36. package/ai/skills/testdriver-wait/SKILL.md +1 -1
  37. package/{docs/v7/ai.mdx → ai/skills/testdriver:ai/SKILL.md} +3 -4
  38. package/ai/skills/testdriver:assert/SKILL.md +315 -0
  39. package/ai/skills/testdriver:aws-setup/SKILL.md +448 -0
  40. package/ai/skills/testdriver:cache/SKILL.md +221 -0
  41. package/ai/skills/testdriver:caching/SKILL.md +124 -0
  42. package/ai/skills/testdriver:captcha/SKILL.md +158 -0
  43. package/ai/skills/testdriver:ci-cd/SKILL.md +659 -0
  44. package/ai/skills/testdriver:click/SKILL.md +286 -0
  45. package/ai/skills/testdriver:client/SKILL.md +477 -0
  46. package/ai/skills/testdriver:customizing-devices/SKILL.md +319 -0
  47. package/ai/skills/testdriver:dashcam/SKILL.md +451 -0
  48. package/ai/skills/testdriver:debugging-with-screenshots/SKILL.md +415 -0
  49. package/{docs/v7/device-config.mdx → ai/skills/testdriver:device-config/SKILL.md} +8 -8
  50. package/ai/skills/testdriver:double-click/SKILL.md +102 -0
  51. package/ai/skills/testdriver:elements/SKILL.md +605 -0
  52. package/ai/skills/testdriver:enterprise/SKILL.md +7 -0
  53. package/ai/skills/testdriver:errors/SKILL.md +246 -0
  54. package/ai/skills/testdriver:events/SKILL.md +356 -0
  55. package/ai/skills/testdriver:exec/SKILL.md +317 -0
  56. package/ai/skills/testdriver:find/SKILL.md +860 -0
  57. package/ai/skills/testdriver:focus-application/SKILL.md +293 -0
  58. package/ai/skills/testdriver:generating-tests/SKILL.md +25 -0
  59. package/ai/skills/testdriver:hosted/SKILL.md +156 -0
  60. package/ai/skills/testdriver:hover/SKILL.md +278 -0
  61. package/{docs/v7/locating-elements.mdx → ai/skills/testdriver:locating-elements/SKILL.md} +3 -3
  62. package/ai/skills/testdriver:making-assertions/SKILL.md +32 -0
  63. package/ai/skills/testdriver:mcp/SKILL.md +7 -0
  64. package/ai/skills/testdriver:mouse-down/SKILL.md +161 -0
  65. package/ai/skills/testdriver:mouse-up/SKILL.md +164 -0
  66. package/ai/skills/testdriver:parse/SKILL.md +236 -0
  67. package/ai/skills/testdriver:performing-actions/SKILL.md +53 -0
  68. package/ai/skills/testdriver:press-keys/SKILL.md +348 -0
  69. package/ai/skills/testdriver:provision/SKILL.md +331 -0
  70. package/ai/skills/testdriver:quickstart/SKILL.md +172 -0
  71. package/ai/skills/testdriver:redraw/SKILL.md +214 -0
  72. package/ai/skills/testdriver:reusable-code/SKILL.md +249 -0
  73. package/ai/skills/testdriver:right-click/SKILL.md +123 -0
  74. package/{docs/v7/running-tests.mdx → ai/skills/testdriver:running-tests/SKILL.md} +4 -4
  75. package/ai/skills/testdriver:screenshot/SKILL.md +248 -0
  76. package/ai/skills/testdriver:screenshots/SKILL.md +184 -0
  77. package/ai/skills/testdriver:scroll/SKILL.md +205 -0
  78. package/ai/skills/testdriver:secrets/SKILL.md +115 -0
  79. package/ai/skills/testdriver:self-hosted/SKILL.md +147 -0
  80. package/ai/skills/testdriver:test-results-json/SKILL.md +257 -0
  81. package/ai/skills/testdriver:testdriver/SKILL.md +624 -0
  82. package/ai/skills/testdriver:type/SKILL.md +357 -0
  83. package/ai/skills/testdriver:variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver:wait/SKILL.md +50 -0
  85. package/{docs/v7/waiting-for-elements.mdx → ai/skills/testdriver:waiting-for-elements/SKILL.md} +3 -3
  86. package/ai/skills/testdriver:what-is-testdriver/SKILL.md +54 -0
  87. package/debugger/index.html +12 -0
  88. package/docs/_data/examples-manifest.json +54 -46
  89. package/docs/_data/examples-manifest.schema.json +12 -2
  90. package/docs/_scripts/extract-example-urls.js +21 -3
  91. package/docs/_scripts/generate-examples.js +64 -16
  92. package/docs/changelog.mdx +29 -2
  93. package/docs/claude-mcp-plugin.mdx +65 -18
  94. package/docs/docs.json +16 -33
  95. package/docs/snippets/tests/scroll-yaml.mdx +2 -2
  96. package/docs/v6/commands/scroll-until-image.mdx +1 -1
  97. package/docs/v6/commands/scroll-until-text.mdx +2 -2
  98. package/docs/v6/commands/scroll.mdx +2 -2
  99. package/docs/v7/_drafts/agents.mdx +0 -4
  100. package/docs/v7/_drafts/best-practices.mdx +0 -12
  101. package/docs/v7/_drafts/commands/scroll-until-image.mdx +1 -1
  102. package/docs/v7/_drafts/commands/scroll-until-text.mdx +2 -2
  103. package/docs/v7/_drafts/commands/scroll.mdx +1 -1
  104. package/docs/v7/_drafts/configuration.mdx +0 -12
  105. package/docs/v7/_drafts/faq.mdx +2 -10
  106. package/docs/v7/_drafts/readme.mdx +0 -1
  107. package/docs/v7/_drafts/troubleshooting.mdx +1 -4
  108. package/docs/v7/assert.mdx +0 -1
  109. package/docs/v7/aws-setup.mdx +1 -1
  110. package/docs/v7/caching.mdx +14 -10
  111. package/docs/v7/ci-cd.mdx +98 -14
  112. package/docs/v7/client.mdx +10 -6
  113. package/docs/v7/copilot/auto-healing.mdx +167 -18
  114. package/docs/v7/copilot/running-tests.mdx +915 -54
  115. package/docs/v7/customizing-devices.mdx +7 -5
  116. package/docs/v7/dashcam.mdx +36 -3
  117. package/docs/v7/debugging-with-screenshots.mdx +31 -7
  118. package/docs/v7/examples/ai.mdx +13 -8
  119. package/docs/v7/examples/assert.mdx +1 -1
  120. package/docs/v7/examples/chrome-extension.mdx +30 -13
  121. package/docs/v7/examples/element-not-found.mdx +1 -1
  122. package/docs/v7/examples/findall-coffee-icons.mdx +3 -2
  123. package/docs/v7/examples/formatted-logging.mdx +74 -0
  124. package/docs/v7/examples/hover-image.mdx +1 -1
  125. package/docs/v7/examples/hover-text-with-description.mdx +2 -2
  126. package/docs/v7/examples/hover-text.mdx +1 -1
  127. package/docs/v7/examples/installer.mdx +1 -1
  128. package/docs/v7/examples/launch-vscode-linux.mdx +27 -27
  129. package/docs/v7/examples/match-image.mdx +1 -1
  130. package/docs/v7/examples/parse.mdx +3 -2
  131. package/docs/v7/examples/press-keys.mdx +2 -2
  132. package/docs/v7/examples/scroll-keyboard.mdx +2 -2
  133. package/docs/v7/examples/scroll-until-text.mdx +1 -1
  134. package/docs/v7/examples/scroll.mdx +1 -1
  135. package/docs/v7/examples/type.mdx +1 -1
  136. package/docs/v7/find.mdx +50 -11
  137. package/docs/v7/generating-tests.mdx +166 -10
  138. package/docs/v7/hosted.mdx +1 -1
  139. package/docs/v7/making-assertions.mdx +81 -4
  140. package/docs/v7/parse.mdx +1 -1
  141. package/docs/v7/performing-actions.mdx +100 -5
  142. package/docs/v7/provision.mdx +0 -20
  143. package/docs/v7/quickstart.mdx +299 -21
  144. package/docs/v7/reusable-code.mdx +3 -3
  145. package/docs/v7/screenshot.mdx +5 -5
  146. package/docs/v7/scroll.mdx +30 -160
  147. package/docs/v7/wait.mdx +1 -1
  148. package/examples/chrome-extension.test.mjs +29 -12
  149. package/examples/config.mjs +1 -1
  150. package/examples/launch-vscode-linux.test.mjs +25 -25
  151. package/interfaces/cli/commands/init.js +84 -2
  152. package/interfaces/vitest-plugin.mjs +51 -61
  153. package/lib/core/Dashcam.js +43 -2
  154. package/lib/core/index.d.ts +13 -1
  155. package/lib/github-comment.mjs +11 -5
  156. package/lib/init-project.js +98 -44
  157. package/lib/install-clients.js +470 -0
  158. package/lib/provision.js +749 -0
  159. package/lib/resolve-channel.js +2 -2
  160. package/lib/sentry.js +15 -2
  161. package/lib/vitest/hooks.mjs +44 -34
  162. package/mcp-server/README.md +32 -0
  163. package/mcp-server/dist/core/actions.d.ts +176 -0
  164. package/mcp-server/dist/core/actions.js +753 -0
  165. package/mcp-server/dist/env-utils.d.ts +45 -0
  166. package/mcp-server/dist/env-utils.js +63 -0
  167. package/mcp-server/dist/provision-types.d.ts +6 -2
  168. package/mcp-server/dist/provision-types.js +3 -1
  169. package/mcp-server/dist/server.mjs +449 -751
  170. package/mcp-server/package-lock.json +384 -1
  171. package/mcp-server/package.json +5 -2
  172. package/mcp-server/src/core/actions.ts +912 -0
  173. package/mcp-server/src/env-utils.test.ts +82 -0
  174. package/mcp-server/src/env-utils.ts +77 -0
  175. package/mcp-server/src/provision-types.ts +4 -1
  176. package/mcp-server/src/server.ts +533 -954
  177. package/mcp-server/tsconfig.json +1 -1
  178. package/mcp-server/vitest.config.ts +7 -0
  179. package/package.json +15 -3
  180. package/sdk.d.ts +11 -9
  181. package/sdk.js +116 -746
  182. package/setup/aws/spawn-runner.sh +24 -3
  183. package/docs/v7/_drafts/caching-ai.mdx +0 -215
  184. package/docs/v7/_drafts/caching.mdx +0 -366
  185. package/docs/v7/_drafts/prompt-cache.mdx +0 -200
  186. package/docs/v7/copilot/creating-tests.mdx +0 -156
  187. package/docs/v7/copilot/github.mdx +0 -143
  188. package/docs/v7/copilot/setup.mdx +0 -143
  189. package/docs/v7/examples/exec-output.mdx +0 -84
  190. package/docs/v7/examples/exec-pwsh.mdx +0 -82
  191. package/docs/v7/examples/focus-window.mdx +0 -61
  192. package/docs/v7/examples/scroll-until-image.mdx +0 -82
  193. package/docs/v7/examples/windows-installer.mdx +0 -95
  194. package/examples/formatted-logging.test.mjs +0 -27
  195. package/examples/match-image.test.mjs +0 -55
  196. package/manual/exec-stream-logs.test.mjs +0 -25
package/sdk.js CHANGED
@@ -3,6 +3,7 @@ const path = require("path");
3
3
  const os = require("os");
4
4
  const crypto = require("crypto");
5
5
  const { formatter } = require("./sdk-log-formatter");
6
+ const { createProvisionAPI } = require("./lib/provision");
6
7
 
7
8
  // Load .env — use monorepo root .env when running inside the monorepo,
8
9
  // otherwise fall back to default dotenv.config() for end users.
@@ -471,17 +472,20 @@ class Element {
471
472
  this.sdk.emitter.emit(events.log.log, findingMessage);
472
473
 
473
474
  try {
474
- const screenshot = await this.system.captureScreenBase64();
475
+ // Returns { imageKey } (fast S3-key path, no local round-trip) or
476
+ // { image } (base64 fallback). See system.captureScreenImage.
477
+ const imagePayload = await this.system.captureScreenImage();
475
478
  // Only store screenshot in DEBUG mode to prevent memory leaks
476
- if (debugMode) {
477
- this._screenshot = screenshot;
479
+ if (debugMode && imagePayload.image) {
480
+ this._screenshot = imagePayload.image;
478
481
  }
479
482
 
480
483
  // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold/cache
481
484
  let cacheKey = null;
482
485
  let cacheThreshold = null;
483
486
  let perCommandThresholds = null; // Per-command { screen, element } override
484
- let zoom = true; // Default to enabled
487
+ let zoom = false; // Default to disabled
488
+ let verify = false; // Default to disabled (skip AI verification)
485
489
  let perCommandAi = null; // Per-command AI config override
486
490
 
487
491
  let minConfidence = null; // Minimum confidence threshold
@@ -494,8 +498,10 @@ class Element {
494
498
  // New: options is an object with cacheKey and/or cacheThreshold
495
499
  cacheKey = options.cacheKey || null;
496
500
  cacheThreshold = options.cacheThreshold ?? null;
497
- // zoom defaults to true unless explicitly set to false
498
- zoom = options.zoom !== false;
501
+ // zoom defaults to false unless explicitly set to true
502
+ zoom = options.zoom === true;
503
+ // verify defaults to false unless explicitly set to true
504
+ verify = options.verify === true;
499
505
  // Minimum confidence threshold: fail find if AI confidence is below this value
500
506
  minConfidence = options.confidence ?? null;
501
507
  // Element type hint for prompt wrapping
@@ -562,13 +568,14 @@ class Element {
562
568
  response = await this.sdk.apiClient.req("find", {
563
569
  session: this.sdk.getSessionId(),
564
570
  element: description,
565
- image: screenshot,
571
+ ...imagePayload,
566
572
  threshold: threshold,
567
573
  elementSimilarity: elementSimilarity,
568
574
  cacheKey: cacheKey,
569
575
  os: this.sdk.os,
570
576
  resolution: this.sdk.resolution,
571
577
  zoom: zoom === true ? 1 : zoom === false ? 0 : zoom,
578
+ skipVerify: !verify,
572
579
  confidence: minConfidence,
573
580
  type: elementType,
574
581
  ai: {
@@ -1505,6 +1512,25 @@ class TestDriverSDK {
1505
1512
  this.reconnect =
1506
1513
  options.reconnect !== undefined ? options.reconnect : false;
1507
1514
 
1515
+ // Explicit sandbox id to reconnect to (overrides last-sandbox file)
1516
+ this.sandboxId = options.sandboxId || null;
1517
+
1518
+ // When reconnect is requested, an explicit sandboxId implies newSandbox=false.
1519
+ // If reconnect:true but no sandboxId given, try to load from .testdriver/last-sandbox.
1520
+ if (this.reconnect && !this.sandboxId) {
1521
+ const last = TestDriverSDK._readLastSandbox();
1522
+ if (last && last.sandboxId) {
1523
+ this.sandboxId = last.sandboxId;
1524
+ }
1525
+ }
1526
+ if (this.sandboxId && options.newSandbox === undefined) {
1527
+ this.newSandbox = false;
1528
+ }
1529
+
1530
+ // Store keepAlive preference from options
1531
+ this.keepAlive =
1532
+ options.keepAlive !== undefined ? options.keepAlive : undefined;
1533
+
1508
1534
  // Store dashcam preference (default: true)
1509
1535
  this.dashcamEnabled = options.dashcam !== false;
1510
1536
 
@@ -1790,733 +1816,7 @@ class TestDriverSDK {
1790
1816
  }
1791
1817
 
1792
1818
  _createProvisionAPI() {
1793
- const self = this;
1794
-
1795
- const provisionMethods = {
1796
- /**
1797
- * Launch Chrome browser
1798
- * @param {Object} options - Chrome launch options
1799
- * @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
1800
- * @param {boolean} [options.maximized=true] - Start maximized
1801
- * @param {boolean} [options.guest=false] - Use guest mode
1802
- * @returns {Promise<void>}
1803
- */
1804
- chrome: async (options = {}) => {
1805
- const {
1806
- url = "http://testdriver-sandbox.vercel.app/",
1807
- maximized = true,
1808
- guest = false,
1809
- } = options;
1810
-
1811
- // Store the URL for domain-specific web log tracking
1812
- self._provisionedChromeUrl = url;
1813
-
1814
- // Set up Chrome profile with preferences
1815
- const shell = this.os === "windows" ? "pwsh" : "sh";
1816
- const userDataDir =
1817
- this.os === "windows"
1818
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1819
- : "/tmp/testdriver-chrome-profile";
1820
-
1821
- // Create user data directory and Default profile directory
1822
- const defaultProfileDir =
1823
- this.os === "windows"
1824
- ? `${userDataDir}\\Default`
1825
- : `${userDataDir}/Default`;
1826
-
1827
- const createDirCmd =
1828
- this.os === "windows"
1829
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1830
- : `mkdir -p "${defaultProfileDir}"`;
1831
-
1832
- await this.exec(shell, createDirCmd, 60000, true);
1833
-
1834
- // Write Chrome preferences
1835
- const chromePrefs = {
1836
- credentials_enable_service: false,
1837
- profile: {
1838
- password_manager_enabled: false,
1839
- default_content_setting_values: {},
1840
- },
1841
- signin: {
1842
- allowed: false,
1843
- },
1844
- sync: {
1845
- requested: false,
1846
- first_setup_complete: true,
1847
- sync_all_os_types: false,
1848
- },
1849
- autofill: {
1850
- enabled: false,
1851
- },
1852
- local_state: {
1853
- browser: {
1854
- has_seen_welcome_page: true,
1855
- },
1856
- },
1857
- };
1858
-
1859
- const prefsPath =
1860
- this.os === "windows"
1861
- ? `${defaultProfileDir}\\Preferences`
1862
- : `${defaultProfileDir}/Preferences`;
1863
-
1864
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
1865
- const writePrefCmd =
1866
- this.os === "windows"
1867
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1868
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1869
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1870
-
1871
- await this.exec(shell, writePrefCmd, 60000, true);
1872
-
1873
- // Build Chrome launch command
1874
- const chromeArgs = [];
1875
- if (maximized) chromeArgs.push("--start-maximized");
1876
- if (guest) chromeArgs.push("--guest");
1877
- chromeArgs.push(
1878
- "--disable-fre",
1879
- "--no-default-browser-check",
1880
- "--no-first-run",
1881
- "--no-experiments",
1882
- "--disable-infobars",
1883
- "--disable-features=StartupBrowserCreator",
1884
- "--disable-features=ChromeWhatsNewUI",
1885
- `--user-data-dir=${userDataDir}`,
1886
- );
1887
-
1888
- // Add remote debugging port for captcha solving support
1889
- chromeArgs.push("--remote-debugging-port=9222");
1890
-
1891
- // Add dashcam-chrome extension
1892
- const dashcamChromePath = await this._getDashcamChromeExtensionPath();
1893
- if (dashcamChromePath) {
1894
- chromeArgs.push(`--load-extension=${dashcamChromePath}`);
1895
- }
1896
-
1897
- // Launch Chrome
1898
-
1899
- if (this.os === "windows") {
1900
- const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
1901
- await this.exec(
1902
- shell,
1903
- `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1904
- 30000,
1905
- );
1906
- } else {
1907
- const argsString = chromeArgs.join(" ");
1908
- await this.exec(
1909
- shell,
1910
- `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1911
- 30000,
1912
- );
1913
- }
1914
-
1915
- // Wait for Chrome debugger port and page to be ready
1916
- await this._waitForChromeDebuggerReady();
1917
- await this.focusApplication("Google Chrome");
1918
-
1919
- // Add web log tracking with domain wildcard pattern, then start dashcam
1920
- if (this.dashcamEnabled) {
1921
- const domainPattern = this._getUrlDomainPattern(url);
1922
- await this.dashcam.addWebLog(domainPattern, "Web Logs");
1923
-
1924
- // Start dashcam recording after logs are configured
1925
- if (!(await this.dashcam.isRecording())) {
1926
- await this.dashcam.start();
1927
- }
1928
- }
1929
- },
1930
-
1931
- /**
1932
- * Launch Chrome browser with a custom extension loaded
1933
- * @param {Object} options - Chrome extension launch options
1934
- * @param {string} [options.extensionPath] - Local filesystem path to the unpacked extension directory
1935
- * @param {string} [options.extensionId] - Chrome Web Store extension ID (e.g., "cjpalhdlnbpafiamejdnhcphjbkeiagm" for uBlock Origin)
1936
- * @param {boolean} [options.maximized=true] - Start maximized
1937
- * @returns {Promise<void>}
1938
- * @example
1939
- * // Load extension from local path
1940
- * await testdriver.exec('sh', 'git clone https://github.com/user/extension.git /tmp/extension');
1941
- * await testdriver.provision.chromeExtension({
1942
- * extensionPath: '/tmp/extension'
1943
- * });
1944
- *
1945
- * @example
1946
- * // Load extension by Chrome Web Store ID
1947
- * await testdriver.provision.chromeExtension({
1948
- * extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm' // uBlock Origin
1949
- * });
1950
- */
1951
- chromeExtension: async (options = {}) => {
1952
- const {
1953
- extensionPath: providedExtensionPath,
1954
- extensionId,
1955
- maximized = true,
1956
- } = options;
1957
-
1958
- if (!providedExtensionPath && !extensionId) {
1959
- throw new Error(
1960
- "[provision.chromeExtension] Either extensionPath or extensionId is required",
1961
- );
1962
- }
1963
-
1964
- let extensionPath = providedExtensionPath;
1965
- const shell = this.os === "windows" ? "pwsh" : "sh";
1966
-
1967
- // If extensionId is provided, download and extract the extension from Chrome Web Store
1968
- if (extensionId && !extensionPath) {
1969
- console.log(
1970
- `[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`,
1971
- );
1972
-
1973
- const extensionDir =
1974
- this.os === "windows"
1975
- ? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
1976
- : `/tmp/testdriver-extensions/${extensionId}`;
1977
-
1978
- // Create extension directory
1979
- const mkdirCmd =
1980
- this.os === "windows"
1981
- ? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
1982
- : `mkdir -p "${extensionDir}"`;
1983
- await this.exec(shell, mkdirCmd, 60000, true);
1984
-
1985
- // Download CRX from Chrome Web Store
1986
- // The CRX download URL format for Chrome Web Store
1987
- const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0.0.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26installsource%3Dondemand%26uc`;
1988
- const crxPath =
1989
- this.os === "windows"
1990
- ? `${extensionDir}\\extension.crx`
1991
- : `${extensionDir}/extension.crx`;
1992
-
1993
- if (this.os === "windows") {
1994
- await this.exec(
1995
- "pwsh",
1996
- `Invoke-WebRequest -Uri "${crxUrl}" -OutFile "${crxPath}"`,
1997
- 60000,
1998
- true,
1999
- );
2000
- } else {
2001
- await this.exec(
2002
- "sh",
2003
- `curl -L -o "${crxPath}" "${crxUrl}"`,
2004
- 60000,
2005
- true,
2006
- );
2007
- }
2008
-
2009
- // Extract the CRX file (CRX is a ZIP with a header)
2010
- // Skip the CRX header and extract as ZIP
2011
- if (this.os === "windows") {
2012
- // PowerShell: Read CRX, skip header, extract ZIP
2013
- await this.exec(
2014
- "pwsh",
2015
- `
2016
- $crxBytes = [System.IO.File]::ReadAllBytes("${crxPath}")
2017
- # CRX3 header: 4 bytes magic + 4 bytes version + 4 bytes header length + header
2018
- $magic = [System.Text.Encoding]::ASCII.GetString($crxBytes[0..3])
2019
- if ($magic -eq "Cr24") {
2020
- $headerLen = [BitConverter]::ToUInt32($crxBytes, 8)
2021
- $zipStart = 12 + $headerLen
2022
- } else {
2023
- # CRX2 format
2024
- $zipStart = 16 + [BitConverter]::ToUInt32($crxBytes, 8) + [BitConverter]::ToUInt32($crxBytes, 12)
2025
- }
2026
- $zipBytes = $crxBytes[$zipStart..($crxBytes.Length - 1)]
2027
- $zipPath = "${extensionDir}\\extension.zip"
2028
- [System.IO.File]::WriteAllBytes($zipPath, $zipBytes)
2029
- Expand-Archive -Path $zipPath -DestinationPath "${extensionDir}\\unpacked" -Force
2030
- `,
2031
- 30000,
2032
- true,
2033
- );
2034
- extensionPath = `${extensionDir}\\unpacked`;
2035
- } else {
2036
- // Linux: Use unzip with offset or python to extract
2037
- await this.exec(
2038
- "sh",
2039
- `
2040
- cd "${extensionDir}"
2041
- # Extract CRX (skip header and unzip)
2042
- # CRX3 format: magic(4) + version(4) + header_length(4) + header + zip
2043
- python3 -c "
2044
- import struct
2045
- import zipfile
2046
- import io
2047
- import os
2048
-
2049
- with open('extension.crx', 'rb') as f:
2050
- data = f.read()
2051
-
2052
- # Check magic number
2053
- magic = data[:4]
2054
- if magic == b'Cr24':
2055
- # CRX3 format
2056
- header_len = struct.unpack('<I', data[8:12])[0]
2057
- zip_start = 12 + header_len
2058
- else:
2059
- # CRX2 format
2060
- pub_key_len = struct.unpack('<I', data[8:12])[0]
2061
- sig_len = struct.unpack('<I', data[12:16])[0]
2062
- zip_start = 16 + pub_key_len + sig_len
2063
-
2064
- zip_data = data[zip_start:]
2065
- os.makedirs('unpacked', exist_ok=True)
2066
- with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2067
- zf.extractall('unpacked')
2068
- "
2069
- `,
2070
- 30000,
2071
- true,
2072
- );
2073
- extensionPath = `${extensionDir}/unpacked`;
2074
- }
2075
-
2076
- console.log(
2077
- `[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`,
2078
- );
2079
- }
2080
-
2081
- // Set up Chrome profile with preferences
2082
- const userDataDir =
2083
- this.os === "windows"
2084
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
2085
- : "/tmp/testdriver-chrome-profile";
2086
-
2087
- // Create user data directory and Default profile directory
2088
- const defaultProfileDir =
2089
- this.os === "windows"
2090
- ? `${userDataDir}\\Default`
2091
- : `${userDataDir}/Default`;
2092
-
2093
- const createDirCmd =
2094
- this.os === "windows"
2095
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
2096
- : `mkdir -p "${defaultProfileDir}"`;
2097
-
2098
- await this.exec(shell, createDirCmd, 60000, true);
2099
-
2100
- // Write Chrome preferences
2101
- const chromePrefs = {
2102
- credentials_enable_service: false,
2103
- profile: {
2104
- password_manager_enabled: false,
2105
- default_content_setting_values: {},
2106
- },
2107
- signin: {
2108
- allowed: false,
2109
- },
2110
- sync: {
2111
- requested: false,
2112
- first_setup_complete: true,
2113
- sync_all_os_types: false,
2114
- },
2115
- autofill: {
2116
- enabled: false,
2117
- },
2118
- local_state: {
2119
- browser: {
2120
- has_seen_welcome_page: true,
2121
- },
2122
- },
2123
- };
2124
-
2125
- const prefsPath =
2126
- this.os === "windows"
2127
- ? `${defaultProfileDir}\\Preferences`
2128
- : `${defaultProfileDir}/Preferences`;
2129
-
2130
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
2131
- const writePrefCmd =
2132
- this.os === "windows"
2133
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
2134
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
2135
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
2136
-
2137
- await this.exec(shell, writePrefCmd, 60000, true);
2138
-
2139
- // Build Chrome launch command
2140
- const chromeArgs = [];
2141
- if (maximized) chromeArgs.push("--start-maximized");
2142
- chromeArgs.push(
2143
- "--disable-fre",
2144
- "--no-default-browser-check",
2145
- "--no-first-run",
2146
- "--no-experiments",
2147
- "--disable-infobars",
2148
- "--disable-features=ChromeLabs",
2149
- `--user-data-dir=${userDataDir}`,
2150
- );
2151
-
2152
- // Add remote debugging port for captcha solving support
2153
- chromeArgs.push("--remote-debugging-port=9222");
2154
-
2155
- // Add user extension and dashcam-chrome extension
2156
- const dashcamChromePath = await this._getDashcamChromeExtensionPath();
2157
- if (dashcamChromePath) {
2158
- // Load both user extension and dashcam-chrome for web log capture
2159
- chromeArgs.push(
2160
- `--load-extension=${extensionPath},${dashcamChromePath}`,
2161
- );
2162
- } else {
2163
- // If dashcam-chrome unavailable, just load user extension
2164
- chromeArgs.push(`--load-extension=${extensionPath}`);
2165
- }
2166
-
2167
- // Launch Chrome (opens to New Tab by default)
2168
- if (this.os === "windows") {
2169
- const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
2170
- await this.exec(
2171
- shell,
2172
- `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}`,
2173
- 30000,
2174
- );
2175
- } else {
2176
- const argsString = chromeArgs.join(" ");
2177
- await this.exec(
2178
- shell,
2179
- `chrome-for-testing ${argsString} >/dev/null 2>&1 &`,
2180
- 30000,
2181
- );
2182
- }
2183
-
2184
- // Wait for Chrome debugger port and page to be ready
2185
- await this._waitForChromeDebuggerReady();
2186
- await this.focusApplication("Google Chrome");
2187
-
2188
- // Start dashcam recording
2189
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2190
- await this.dashcam.start();
2191
- }
2192
- },
2193
-
2194
- /**
2195
- * Launch VS Code
2196
- * @param {Object} options - VS Code launch options
2197
- * @param {string} [options.workspace] - Workspace/folder to open
2198
- * @param {string[]} [options.extensions=[]] - Extensions to install
2199
- * @returns {Promise<void>}
2200
- */
2201
- vscode: async (options = {}) => {
2202
- const { workspace = null, extensions = [] } = options;
2203
-
2204
- const shell = this.os === "windows" ? "pwsh" : "sh";
2205
-
2206
- // Install extensions if provided
2207
- for (const extension of extensions) {
2208
- console.log(`[provision.vscode] Installing extension: ${extension}`);
2209
- await this.exec(
2210
- shell,
2211
- `code --install-extension ${extension} --force`,
2212
- 120000,
2213
- true,
2214
- );
2215
- console.log(
2216
- `[provision.vscode] ✅ Extension installed: ${extension}`,
2217
- );
2218
- }
2219
-
2220
- // Launch VS Code
2221
- const workspaceArg = workspace ? `"${workspace}"` : "";
2222
-
2223
- if (this.os === "windows") {
2224
- await this.exec(
2225
- shell,
2226
- `Start-Process code -ArgumentList ${workspaceArg}`,
2227
- 30000,
2228
- );
2229
- } else {
2230
- await this.exec(
2231
- shell,
2232
- `code ${workspaceArg} >/dev/null 2>&1 &`,
2233
- 30000,
2234
- );
2235
- }
2236
-
2237
- // Wait for VS Code to start up
2238
- await new Promise((resolve) => setTimeout(resolve, 3000));
2239
-
2240
- // Wait for VS Code to be ready
2241
- await this.focusApplication("Visual Studio Code");
2242
-
2243
- // Start dashcam recording
2244
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2245
- await this.dashcam.start();
2246
- }
2247
- },
2248
-
2249
- /**
2250
- * Download and install an application
2251
- * @param {Object} options - Installer options
2252
- * @param {string} options.url - URL to download the installer from
2253
- * @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
2254
- * @param {string} [options.appName] - Application name to focus after install
2255
- * @param {boolean} [options.launch=true] - Whether to launch the app after installation
2256
- * @returns {Promise<string>} Path to the downloaded file
2257
- * @example
2258
- * // Install a .deb package on Linux (auto-detected)
2259
- * await testdriver.provision.installer({
2260
- * url: 'https://example.com/app.deb',
2261
- * appName: 'MyApp'
2262
- * });
2263
- *
2264
- * @example
2265
- * // Download and run custom commands
2266
- * const filePath = await testdriver.provision.installer({
2267
- * url: 'https://example.com/app.AppImage',
2268
- * launch: false
2269
- * });
2270
- * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
2271
- */
2272
- installer: async (options = {}) => {
2273
- const { url, filename, appName, launch = true } = options;
2274
-
2275
- if (!url) {
2276
- throw new Error("[provision.installer] url is required");
2277
- }
2278
-
2279
- const shell = this.os === "windows" ? "pwsh" : "sh";
2280
-
2281
- // Determine download directory
2282
- const downloadDir =
2283
- this.os === "windows" ? "C:\\Users\\testdriver\\Downloads" : "/tmp";
2284
-
2285
- console.log(`[provision.installer] Downloading ${url}...`);
2286
-
2287
- let actualFilePath;
2288
-
2289
- // Download the file and get the actual filename (handles redirects)
2290
- if (this.os === "windows") {
2291
- // Simple approach: download first, then get the actual filename from the response
2292
- const tempFile = `${downloadDir}\\installer_temp_${Date.now()}`;
2293
-
2294
- const downloadScript = `
2295
- $ProgressPreference = 'SilentlyContinue'
2296
- $response = Invoke-WebRequest -Uri "${url}" -OutFile "${tempFile}" -PassThru -UseBasicParsing
2297
-
2298
- # Try to get filename from Content-Disposition header
2299
- $filename = $null
2300
- if ($response.Headers['Content-Disposition']) {
2301
- if ($response.Headers['Content-Disposition'] -match 'filename=\\"?([^\\"]+)\\"?') {
2302
- $filename = $matches[1]
2303
- }
2304
- }
2305
-
2306
- # If no filename from header, try to get from URL or use default
2307
- if (-not $filename) {
2308
- $uri = [System.Uri]"${url}"
2309
- $filename = [System.IO.Path]::GetFileName($uri.LocalPath)
2310
- if (-not $filename -or $filename -eq '') {
2311
- $filename = "installer"
2312
- }
2313
- }
2314
-
2315
- # Move temp file to final location with proper filename
2316
- $finalPath = Join-Path "${downloadDir}" $filename
2317
- Move-Item -Path "${tempFile}" -Destination $finalPath -Force
2318
- Write-Output $finalPath
2319
- `;
2320
-
2321
- const result = await this.exec(shell, downloadScript, 300000, true);
2322
- actualFilePath = result ? result.trim() : null;
2323
-
2324
- if (!actualFilePath) {
2325
- throw new Error("[provision.installer] Failed to download file");
2326
- }
2327
- } else {
2328
- // Use curl with options to get the final filename
2329
- const tempMarker = `installer_${Date.now()}`;
2330
- const downloadScript = `
2331
- cd "${downloadDir}"
2332
- curl -L -J -O -w "%{filename_effective}" "${url}" 2>/dev/null || echo "${tempMarker}"
2333
- `;
2334
-
2335
- const result = await this.exec(shell, downloadScript, 300000, true);
2336
- const downloadedFile = result ? result.trim() : null;
2337
-
2338
- if (downloadedFile && downloadedFile !== tempMarker) {
2339
- actualFilePath = `${downloadDir}/${downloadedFile}`;
2340
- } else {
2341
- // Fallback: use curl without -J and specify output file
2342
- const fallbackFilename = filename || "installer";
2343
- actualFilePath = `${downloadDir}/${fallbackFilename}`;
2344
- await this.exec(
2345
- shell,
2346
- `curl -L -o "${actualFilePath}" "${url}"`,
2347
- 300000,
2348
- true,
2349
- );
2350
- }
2351
- }
2352
-
2353
- console.log(`[provision.installer] ✅ Downloaded to ${actualFilePath}`);
2354
-
2355
- // Auto-detect install command based on file extension (use actualFilePath for extension detection)
2356
- const actualFilename = actualFilePath.split(/[/\\]/).pop() || "";
2357
- const ext = actualFilename.split(".").pop()?.toLowerCase();
2358
- let installCommand = null;
2359
-
2360
- if (this.os === "windows") {
2361
- if (ext === "msi") {
2362
- installCommand = `Start-Process msiexec -ArgumentList '/i', '"${actualFilePath}"', '/quiet', '/norestart' -Wait`;
2363
- } else if (ext === "exe") {
2364
- installCommand = `Start-Process "${actualFilePath}" -ArgumentList '/S' -Wait`;
2365
- }
2366
- } else if (this.os === "linux") {
2367
- if (ext === "deb") {
2368
- installCommand = `sudo dpkg -i "${actualFilePath}" && sudo apt-get install -f -y`;
2369
- } else if (ext === "rpm") {
2370
- installCommand = `sudo rpm -i "${actualFilePath}"`;
2371
- } else if (ext === "appimage") {
2372
- installCommand = `chmod +x "${actualFilePath}"`;
2373
- } else if (ext === "sh") {
2374
- installCommand = `chmod +x "${actualFilePath}" && "${actualFilePath}"`;
2375
- }
2376
- } else if (this.os === "darwin") {
2377
- if (ext === "dmg") {
2378
- installCommand = `hdiutil attach "${actualFilePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
2379
- } else if (ext === "pkg") {
2380
- installCommand = `sudo installer -pkg "${actualFilePath}" -target /`;
2381
- }
2382
- }
2383
-
2384
- if (installCommand) {
2385
- console.log(`[provision.installer] Installing...`);
2386
- await this.exec(shell, installCommand, 300000, true);
2387
- console.log(`[provision.installer] ✅ Installation complete`);
2388
- }
2389
-
2390
- // Launch and focus the app if appName is provided and launch is true
2391
- if (appName && launch) {
2392
- await new Promise((resolve) => setTimeout(resolve, 2000));
2393
- await this.focusApplication(appName);
2394
- }
2395
-
2396
- // Start dashcam recording
2397
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2398
- await this.dashcam.start();
2399
- }
2400
-
2401
- return actualFilePath;
2402
- },
2403
-
2404
- /**
2405
- * Launch Electron app
2406
- * @param {Object} options - Electron launch options
2407
- * @param {string} options.appPath - Path to Electron app (required)
2408
- * @param {string[]} [options.args=[]] - Additional electron args
2409
- * @returns {Promise<void>}
2410
- */
2411
- electron: async (options = {}) => {
2412
- const { appPath, args = [] } = options;
2413
-
2414
- if (!appPath) {
2415
- throw new Error("provision.electron requires appPath option");
2416
- }
2417
-
2418
- const shell = this.os === "windows" ? "pwsh" : "sh";
2419
-
2420
- const argsString = args.join(" ");
2421
-
2422
- if (this.os === "windows") {
2423
- await this.exec(
2424
- shell,
2425
- `Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
2426
- 30000,
2427
- );
2428
- } else {
2429
- await this.exec(
2430
- shell,
2431
- `electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
2432
- 30000,
2433
- );
2434
- }
2435
-
2436
- await this.focusApplication("Electron");
2437
-
2438
- // Start dashcam recording
2439
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2440
- await this.dashcam.start();
2441
- }
2442
- },
2443
-
2444
- /**
2445
- * Initialize Dashcam recording with logging
2446
- * @param {Object} options - Dashcam options
2447
- * @param {string} [options.logPath] - Path to log file (auto-generated if not provided)
2448
- * @param {string} [options.logName='TestDriver Log'] - Display name for the log
2449
- * @param {boolean} [options.webLogs=true] - Enable web log tracking
2450
- * @param {string} [options.title] - Custom title for the recording
2451
- * @returns {Promise<void>}
2452
- */
2453
- dashcam: async (options = {}) => {
2454
- const {
2455
- logPath,
2456
- logName = "TestDriver Log",
2457
- webLogs = true,
2458
- title,
2459
- } = options;
2460
-
2461
- // Ensure dashcam is enabled
2462
- if (!this.dashcamEnabled) {
2463
- console.warn(
2464
- "[provision.dashcam] Dashcam is not enabled. Skipping.",
2465
- );
2466
- return;
2467
- }
2468
-
2469
- // Set custom title if provided
2470
- if (title) {
2471
- this.dashcam.setTitle(title);
2472
- }
2473
-
2474
- // Add file log tracking
2475
- const actualLogPath =
2476
- logPath ||
2477
- (this.os === "windows"
2478
- ? "C:\\Users\\testdriver\\testdriver.log"
2479
- : "/tmp/testdriver.log");
2480
-
2481
- await this.dashcam.addFileLog(actualLogPath, logName);
2482
-
2483
- // Add web log tracking if enabled
2484
- // Use domain pattern from provisioned Chrome URL if available
2485
- if (webLogs) {
2486
- const pattern = this._provisionedChromeUrl
2487
- ? this._getUrlDomainPattern(this._provisionedChromeUrl)
2488
- : "**";
2489
- await this.dashcam.addWebLog(pattern, "Web Logs");
2490
- }
2491
-
2492
- // Start recording if not already recording
2493
- if (!(await this.dashcam.isRecording())) {
2494
- await this.dashcam.start();
2495
- }
2496
-
2497
- console.log("[provision.dashcam] ✅ Dashcam recording started");
2498
- },
2499
- };
2500
-
2501
- // Wrap all provision methods with reconnect check using Proxy
2502
- return new Proxy(provisionMethods, {
2503
- get(target, prop) {
2504
- const method = target[prop];
2505
- if (typeof method === "function") {
2506
- return async (...args) => {
2507
- // Skip provisioning if reconnecting to existing sandbox
2508
- if (self.reconnect) {
2509
- console.log(
2510
- `[provision.${prop}] Skipping provisioning (reconnect mode)`,
2511
- );
2512
- return;
2513
- }
2514
- return method(...args);
2515
- };
2516
- }
2517
- return method;
2518
- },
2519
- });
1819
+ return createProvisionAPI(this);
2520
1820
  }
2521
1821
 
2522
1822
  /**
@@ -2787,6 +2087,9 @@ CAPTCHA_SOLVER_EOF`,
2787
2087
  // Set agent properties for buildEnv to use
2788
2088
  if (connectOptions.sandboxId) {
2789
2089
  this.agent.sandboxId = connectOptions.sandboxId;
2090
+ } else if (this.sandboxId) {
2091
+ // Constructor-provided sandboxId (explicit or loaded from .testdriver/last-sandbox)
2092
+ this.agent.sandboxId = this.sandboxId;
2790
2093
  }
2791
2094
  // Use IP from connectOptions if provided, otherwise fall back to constructor IP
2792
2095
  if (connectOptions.ip !== undefined) {
@@ -2826,9 +2129,11 @@ CAPTCHA_SOLVER_EOF`,
2826
2129
  } else {
2827
2130
  this.agent.sandboxOs = this.os;
2828
2131
  }
2829
- // Use keepAlive from connectOptions if provided
2132
+ // Use keepAlive from connectOptions if provided, otherwise fall back to constructor option
2830
2133
  if (connectOptions.keepAlive !== undefined) {
2831
2134
  this.agent.keepAlive = connectOptions.keepAlive;
2135
+ } else if (this.keepAlive !== undefined) {
2136
+ this.agent.keepAlive = this.keepAlive;
2832
2137
  }
2833
2138
 
2834
2139
  // Set redrawThreshold on agent's cliArgs.options
@@ -2883,6 +2188,20 @@ CAPTCHA_SOLVER_EOF`,
2883
2188
  sandboxId: this.instance?.instanceId,
2884
2189
  });
2885
2190
 
2191
+ // Persist the active sandbox id so a later run with `reconnect: true`
2192
+ // can reattach without the caller having to thread the id through.
2193
+ const activeSandboxId =
2194
+ this.instance?.sandboxId || this.instance?.instanceId || null;
2195
+ if (activeSandboxId) {
2196
+ this.sandboxId = activeSandboxId;
2197
+ TestDriverSDK._writeLastSandbox({
2198
+ sandboxId: activeSandboxId,
2199
+ os: this.os,
2200
+ e2bTemplateId: this.e2bTemplateId || null,
2201
+ createdAt: Date.now(),
2202
+ });
2203
+ }
2204
+
2886
2205
  return this.instance;
2887
2206
  }
2888
2207
 
@@ -3088,7 +2407,8 @@ CAPTCHA_SOLVER_EOF`,
3088
2407
  const { events } = require("./agent/events.js");
3089
2408
 
3090
2409
  try {
3091
- const screenshot = await this.system.captureScreenBase64();
2410
+ // { imageKey } (fast S3-key path) or { image } (base64 fallback).
2411
+ const imagePayload = await this.system.captureScreenImage();
3092
2412
 
3093
2413
  // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold/cache
3094
2414
  let cacheKey = null;
@@ -3157,7 +2477,7 @@ CAPTCHA_SOLVER_EOF`,
3157
2477
  {
3158
2478
  session: this.getSessionId(),
3159
2479
  element: description,
3160
- image: screenshot,
2480
+ ...imagePayload,
3161
2481
  threshold: threshold,
3162
2482
  cacheKey: cacheKey,
3163
2483
  os: this.os,
@@ -3196,11 +2516,12 @@ CAPTCHA_SOLVER_EOF`,
3196
2516
  elementData,
3197
2517
  );
3198
2518
 
3199
- // Only store screenshot in DEBUG mode
2519
+ // Only store screenshot in DEBUG mode (and only when we have the
2520
+ // bytes locally — the fast imageKey path doesn't download them)
3200
2521
  const debugMode =
3201
2522
  process.env.VERBOSE || process.env.TD_DEBUG;
3202
- if (debugMode) {
3203
- element._screenshot = screenshot;
2523
+ if (debugMode && imagePayload.image) {
2524
+ element._screenshot = imagePayload.image;
3204
2525
  }
3205
2526
 
3206
2527
  return element;
@@ -3684,11 +3005,9 @@ CAPTCHA_SOLVER_EOF`,
3684
3005
  const { events } = require("./agent/events.js");
3685
3006
  this.emitter.emit(events.log.log, "🔍 Running OmniParser screen analysis...");
3686
3007
 
3687
- const screenshot = await this.system.captureScreenBase64();
3688
-
3689
3008
  const response = await this.apiClient.req("parse", {
3690
3009
  session: this.getSessionId(),
3691
- image: screenshot,
3010
+ ...(await this.system.captureScreenImage()),
3692
3011
  });
3693
3012
 
3694
3013
  if (response.error) {
@@ -3845,8 +3164,9 @@ CAPTCHA_SOLVER_EOF`,
3845
3164
  const apiRoot = this.config?.TD_API_ROOT || 'unknown';
3846
3165
  const apiKey = this.config?.TD_API_KEY || '';
3847
3166
  const maskedKey = apiKey.length > 4 ? '***' + apiKey.slice(-4) : '(not set)';
3848
- const env = process.env.TD_CHANNEL || process.env.TD_ENV || 'unknown';
3849
- const os = this.agent?.options?.os || process.env.TD_OS || 'linux';
3167
+ const channelConfig = require('./lib/resolve-channel.js');
3168
+ const env = process.env.TD_CHANNEL || process.env.TD_ENV || channelConfig.active || 'unknown';
3169
+ const os = this.os || this.agent?.options?.os || process.env.TD_OS || 'linux';
3850
3170
  const sdkVersion = require('./package.json').version;
3851
3171
 
3852
3172
  // Always print local config immediately
@@ -4345,7 +3665,11 @@ CAPTCHA_SOLVER_EOF`,
4345
3665
  if (this._logBuffer.length === 0) return "";
4346
3666
  const startTime = this._logBuffer[0].time;
4347
3667
  return this._logBuffer
4348
- .map((entry) => JSON.stringify({ ...entry, time: entry.time - startTime }))
3668
+ .map((entry, index) => {
3669
+ const obj = { ...entry, time: entry.time - startTime };
3670
+ if (index === 0) obj.epoch = startTime; // absolute epoch for video alignment
3671
+ return JSON.stringify(obj);
3672
+ })
4349
3673
  .join("\n");
4350
3674
  }
4351
3675
 
@@ -4355,6 +3679,52 @@ CAPTCHA_SOLVER_EOF`,
4355
3679
  clearLogs() {
4356
3680
  this._logBuffer = [];
4357
3681
  }
3682
+
3683
+ /**
3684
+ * Return info about the most recently provisioned sandbox (from this process
3685
+ * or persisted from a previous run via .testdriver/last-sandbox).
3686
+ * @returns {{ sandboxId: string, os?: string, e2bTemplateId?: string|null, createdAt?: number } | null}
3687
+ */
3688
+ getLastSandboxId() {
3689
+ if (this.sandboxId) {
3690
+ return {
3691
+ sandboxId: this.sandboxId,
3692
+ os: this.os,
3693
+ e2bTemplateId: this.e2bTemplateId || null,
3694
+ };
3695
+ }
3696
+ return TestDriverSDK._readLastSandbox();
3697
+ }
3698
+
3699
+ /** @private */
3700
+ static _lastSandboxPath() {
3701
+ return path.join(process.cwd(), ".testdriver", "last-sandbox");
3702
+ }
3703
+
3704
+ /** @private */
3705
+ static _readLastSandbox() {
3706
+ try {
3707
+ const p = TestDriverSDK._lastSandboxPath();
3708
+ if (!fs.existsSync(p)) return null;
3709
+ const raw = fs.readFileSync(p, "utf8");
3710
+ const data = JSON.parse(raw);
3711
+ if (!data || !data.sandboxId) return null;
3712
+ return data;
3713
+ } catch (_) {
3714
+ return null;
3715
+ }
3716
+ }
3717
+
3718
+ /** @private */
3719
+ static _writeLastSandbox(data) {
3720
+ try {
3721
+ const p = TestDriverSDK._lastSandboxPath();
3722
+ fs.mkdirSync(path.dirname(p), { recursive: true });
3723
+ fs.writeFileSync(p, JSON.stringify(data, null, 2));
3724
+ } catch (_) {
3725
+ // Best-effort — don't fail the test if we can't persist.
3726
+ }
3727
+ }
4358
3728
  }
4359
3729
 
4360
3730
  // Expose SDK version as a static property for use by vitest hooks/plugins