@testdriverai/agent 7.8.0-test.38

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 (528) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +1 -0
  5. package/CHANGELOG.md +953 -0
  6. package/README.md +81 -0
  7. package/agent/events.js +135 -0
  8. package/agent/index.js +2450 -0
  9. package/agent/interface.js +35 -0
  10. package/agent/lib/analytics.js +22 -0
  11. package/agent/lib/censorship.js +75 -0
  12. package/agent/lib/commander.js +246 -0
  13. package/agent/lib/commands.js +1684 -0
  14. package/agent/lib/config.js +60 -0
  15. package/agent/lib/generator.js +91 -0
  16. package/agent/lib/http.js +144 -0
  17. package/agent/lib/logger.js +56 -0
  18. package/agent/lib/outputs.js +29 -0
  19. package/agent/lib/parser.js +209 -0
  20. package/agent/lib/redraw.js +386 -0
  21. package/agent/lib/resources/cursor-2.png +0 -0
  22. package/agent/lib/sandbox.js +1104 -0
  23. package/agent/lib/sdk.js +633 -0
  24. package/agent/lib/session.js +25 -0
  25. package/agent/lib/source-mapper.js +342 -0
  26. package/agent/lib/subimage/index.js +77 -0
  27. package/agent/lib/subimage/opencv.js +69 -0
  28. package/agent/lib/system.js +204 -0
  29. package/agent/lib/theme.js +14 -0
  30. package/agent/lib/valid-version.js +21 -0
  31. package/agent/lib/validation.js +169 -0
  32. package/ai/.claude-plugin/plugin.json +9 -0
  33. package/ai/agents/testdriver.md +638 -0
  34. package/ai/skills/testdriver-ai/SKILL.md +204 -0
  35. package/ai/skills/testdriver-assert/SKILL.md +315 -0
  36. package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
  37. package/ai/skills/testdriver-cache/SKILL.md +221 -0
  38. package/ai/skills/testdriver-caching/SKILL.md +124 -0
  39. package/ai/skills/testdriver-captcha/SKILL.md +158 -0
  40. package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
  41. package/ai/skills/testdriver-click/SKILL.md +286 -0
  42. package/ai/skills/testdriver-client/SKILL.md +477 -0
  43. package/ai/skills/testdriver-cloud/SKILL.md +119 -0
  44. package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
  45. package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
  46. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
  47. package/ai/skills/testdriver-device-config/SKILL.md +317 -0
  48. package/ai/skills/testdriver-double-click/SKILL.md +102 -0
  49. package/ai/skills/testdriver-elements/SKILL.md +605 -0
  50. package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
  51. package/ai/skills/testdriver-errors/SKILL.md +246 -0
  52. package/ai/skills/testdriver-events/SKILL.md +356 -0
  53. package/ai/skills/testdriver-examples/SKILL.md +7 -0
  54. package/ai/skills/testdriver-exec/SKILL.md +317 -0
  55. package/ai/skills/testdriver-find/SKILL.md +829 -0
  56. package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
  57. package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
  58. package/ai/skills/testdriver-hover/SKILL.md +278 -0
  59. package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
  60. package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
  61. package/ai/skills/testdriver-mcp/SKILL.md +7 -0
  62. package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
  63. package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
  64. package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
  65. package/ai/skills/testdriver-parse/SKILL.md +236 -0
  66. package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
  67. package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
  68. package/ai/skills/testdriver-provision/SKILL.md +331 -0
  69. package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
  70. package/ai/skills/testdriver-redraw/SKILL.md +214 -0
  71. package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
  72. package/ai/skills/testdriver-right-click/SKILL.md +123 -0
  73. package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
  74. package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
  75. package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
  76. package/ai/skills/testdriver-scroll/SKILL.md +335 -0
  77. package/ai/skills/testdriver-secrets/SKILL.md +115 -0
  78. package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
  79. package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
  80. package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
  81. package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -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/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
  86. package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
  87. package/bin/testdriverai.js +22 -0
  88. package/debugger/bg.png +0 -0
  89. package/debugger/icon.png +0 -0
  90. package/debugger/index.html +469 -0
  91. package/debugger/td.png +0 -0
  92. package/debugger/tray-buffered.png +0 -0
  93. package/debugger/tray.png +0 -0
  94. package/docs/GITHUB_COMMENTS.md +330 -0
  95. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  96. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  97. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  98. package/docs/_data/examples-manifest.json +177 -0
  99. package/docs/_data/examples-manifest.schema.json +41 -0
  100. package/docs/_scripts/extract-example-urls.js +165 -0
  101. package/docs/_scripts/generate-examples.js +560 -0
  102. package/docs/_scripts/generate-skills.js +154 -0
  103. package/docs/_scripts/link-replacer.js +164 -0
  104. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  105. package/docs/changelog.mdx +161 -0
  106. package/docs/claude-mcp-plugin.mdx +160 -0
  107. package/docs/docs.json +442 -0
  108. package/docs/github-integration-setup.md +266 -0
  109. package/docs/guide/best-practices-polling.mdx +174 -0
  110. package/docs/images/content/account/newprojectsettings.png +0 -0
  111. package/docs/images/content/account/projectpage.png +0 -0
  112. package/docs/images/content/account/projectreplays.png +0 -0
  113. package/docs/images/content/account/team-manage.png +0 -0
  114. package/docs/images/content/account/teampage.png +0 -0
  115. package/docs/images/content/extension/cursor.svg +1 -0
  116. package/docs/images/content/extension/vscode.svg +57 -0
  117. package/docs/images/content/extension/windsurf.svg +3 -0
  118. package/docs/images/content/parse/output.png +0 -0
  119. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  120. package/docs/images/content/side-by-side.png +0 -0
  121. package/docs/images/content/vscode/ide-full.png +0 -0
  122. package/docs/images/content/vscode/running.png +0 -0
  123. package/docs/images/content/vscode/v7-chat.png +0 -0
  124. package/docs/images/content/vscode/v7-choose-agent.png +0 -0
  125. package/docs/images/content/vscode/v7-full.png +0 -0
  126. package/docs/images/content/vscode/v7-onboarding.png +0 -0
  127. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  128. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  129. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  130. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  131. package/docs/images/content/vscode/vscode-install.png +0 -0
  132. package/docs/images/content/vscode/vscode-overview.png +0 -0
  133. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  134. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  135. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  136. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  137. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  138. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  139. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  140. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  141. package/docs/images/template/dark.png +0 -0
  142. package/docs/images/template/icon.png +0 -0
  143. package/docs/images/template/light.png +0 -0
  144. package/docs/snippets/calendar-link.mdx +4 -0
  145. package/docs/snippets/gitignore-warning.mdx +7 -0
  146. package/docs/snippets/lifecycle-warning.mdx +6 -0
  147. package/docs/snippets/test-prereqs.mdx +12 -0
  148. package/docs/snippets/tests/assert-replay.mdx +7 -0
  149. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  150. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  151. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  152. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  153. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  154. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  155. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  156. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  157. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  158. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  159. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  160. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  161. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  162. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  163. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  164. package/docs/snippets/tests/remember-replay.mdx +7 -0
  165. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  166. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  167. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  168. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  169. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  170. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  171. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  172. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  173. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  174. package/docs/snippets/tests/type-replay.mdx +7 -0
  175. package/docs/snippets/tests/type-yaml.mdx +28 -0
  176. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  177. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  178. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  179. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  180. package/docs/snippets/tests/wait-replay.mdx +7 -0
  181. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  182. package/docs/styles.css +65 -0
  183. package/docs/v6/account/dashboard.mdx +16 -0
  184. package/docs/v6/account/enterprise.mdx +110 -0
  185. package/docs/v6/account/pricing.mdx +33 -0
  186. package/docs/v6/account/projects.mdx +33 -0
  187. package/docs/v6/account/team.mdx +35 -0
  188. package/docs/v6/action/ami.mdx +109 -0
  189. package/docs/v6/action/performance.mdx +105 -0
  190. package/docs/v6/action/secrets.mdx +93 -0
  191. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  192. package/docs/v6/apps/desktop-apps.mdx +93 -0
  193. package/docs/v6/apps/mobile-apps.mdx +26 -0
  194. package/docs/v6/apps/static-websites.mdx +54 -0
  195. package/docs/v6/apps/tauri-apps.mdx +361 -0
  196. package/docs/v6/bugs/jira.mdx +232 -0
  197. package/docs/v6/cli/overview.mdx +66 -0
  198. package/docs/v6/commands/assert.mdx +45 -0
  199. package/docs/v6/commands/exec.mdx +276 -0
  200. package/docs/v6/commands/focus-application.mdx +44 -0
  201. package/docs/v6/commands/hover-image.mdx +69 -0
  202. package/docs/v6/commands/hover-text.mdx +47 -0
  203. package/docs/v6/commands/if.mdx +53 -0
  204. package/docs/v6/commands/match-image.mdx +67 -0
  205. package/docs/v6/commands/press-keys.mdx +87 -0
  206. package/docs/v6/commands/remember.mdx +49 -0
  207. package/docs/v6/commands/run.mdx +44 -0
  208. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  209. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  210. package/docs/v6/commands/scroll.mdx +69 -0
  211. package/docs/v6/commands/type.mdx +45 -0
  212. package/docs/v6/commands/wait-for-image.mdx +54 -0
  213. package/docs/v6/commands/wait-for-text.mdx +48 -0
  214. package/docs/v6/commands/wait.mdx +45 -0
  215. package/docs/v6/exporting/junit.mdx +218 -0
  216. package/docs/v6/exporting/playwright.mdx +197 -0
  217. package/docs/v6/features/auto-healing.mdx +144 -0
  218. package/docs/v6/features/generation.mdx +116 -0
  219. package/docs/v6/features/parallel-testing.mdx +151 -0
  220. package/docs/v6/features/reusable-snippets.mdx +131 -0
  221. package/docs/v6/features/selectorless.mdx +80 -0
  222. package/docs/v6/features/visual-assertions.mdx +139 -0
  223. package/docs/v6/getting-started/ci.mdx +146 -0
  224. package/docs/v6/getting-started/cli.mdx +91 -0
  225. package/docs/v6/getting-started/editing.mdx +100 -0
  226. package/docs/v6/getting-started/playwright.mdx +342 -0
  227. package/docs/v6/getting-started/running.mdx +48 -0
  228. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  229. package/docs/v6/getting-started/vscode.mdx +88 -0
  230. package/docs/v6/guide/assertions.mdx +189 -0
  231. package/docs/v6/guide/authentication.mdx +136 -0
  232. package/docs/v6/guide/code.mdx +65 -0
  233. package/docs/v6/guide/dashcam.mdx +118 -0
  234. package/docs/v6/guide/environment-variables.mdx +26 -0
  235. package/docs/v6/guide/lifecycle.mdx +242 -0
  236. package/docs/v6/guide/locating.mdx +141 -0
  237. package/docs/v6/guide/protips.mdx +43 -0
  238. package/docs/v6/guide/variables.mdx +143 -0
  239. package/docs/v6/guide/waiting.mdx +130 -0
  240. package/docs/v6/importing/csv.mdx +196 -0
  241. package/docs/v6/importing/gherkin.mdx +143 -0
  242. package/docs/v6/importing/jira.mdx +164 -0
  243. package/docs/v6/importing/testrail.mdx +162 -0
  244. package/docs/v6/integrations/electron.mdx +146 -0
  245. package/docs/v6/integrations/netlify.mdx +100 -0
  246. package/docs/v6/integrations/vercel.mdx +125 -0
  247. package/docs/v6/interactive/explore.mdx +99 -0
  248. package/docs/v6/interactive/run.mdx +52 -0
  249. package/docs/v6/interactive/save.mdx +63 -0
  250. package/docs/v6/overview/comparison.mdx +101 -0
  251. package/docs/v6/overview/faq.mdx +162 -0
  252. package/docs/v6/overview/performance.mdx +52 -0
  253. package/docs/v6/overview/quickstart.mdx +137 -0
  254. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  255. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  256. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  257. package/docs/v6/scenarios/file-upload.mdx +33 -0
  258. package/docs/v6/scenarios/form-filling.mdx +32 -0
  259. package/docs/v6/scenarios/log-in.mdx +75 -0
  260. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  261. package/docs/v6/scenarios/spell-check.mdx +22 -0
  262. package/docs/v6/security/action.mdx +84 -0
  263. package/docs/v6/security/agent.mdx +73 -0
  264. package/docs/v6/security/platform.mdx +77 -0
  265. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  266. package/docs/v6/tutorials/basic-test.mdx +45 -0
  267. package/docs/v7/_drafts/agents.mdx +843 -0
  268. package/docs/v7/_drafts/architecture.mdx +399 -0
  269. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  270. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  271. package/docs/v7/_drafts/best-practices.mdx +486 -0
  272. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  273. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  274. package/docs/v7/_drafts/caching.mdx +366 -0
  275. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  276. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  277. package/docs/v7/_drafts/commands/exec.mdx +276 -0
  278. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  279. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  280. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  281. package/docs/v7/_drafts/commands/if.mdx +53 -0
  282. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  283. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  284. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  285. package/docs/v7/_drafts/commands/run.mdx +44 -0
  286. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  287. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  288. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  289. package/docs/v7/_drafts/commands/type.mdx +45 -0
  290. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  291. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  292. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  293. package/docs/v7/_drafts/configuration.mdx +378 -0
  294. package/docs/v7/_drafts/contributing.mdx +174 -0
  295. package/docs/v7/_drafts/core.mdx +458 -0
  296. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  297. package/docs/v7/_drafts/debugging.mdx +349 -0
  298. package/docs/v7/_drafts/error-handling.mdx +501 -0
  299. package/docs/v7/_drafts/faq.mdx +393 -0
  300. package/docs/v7/_drafts/hooks.mdx +360 -0
  301. package/docs/v7/_drafts/init-command.mdx +95 -0
  302. package/docs/v7/_drafts/installation.mdx +420 -0
  303. package/docs/v7/_drafts/migration.mdx +562 -0
  304. package/docs/v7/_drafts/observable.mdx +604 -0
  305. package/docs/v7/_drafts/playwright.mdx +342 -0
  306. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  307. package/docs/v7/_drafts/powerful.mdx +419 -0
  308. package/docs/v7/_drafts/presets.mdx +210 -0
  309. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  310. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  311. package/docs/v7/_drafts/provision.mdx +390 -0
  312. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  313. package/docs/v7/_drafts/readme.mdx +135 -0
  314. package/docs/v7/_drafts/reports.mdx +414 -0
  315. package/docs/v7/_drafts/scalable.mdx +763 -0
  316. package/docs/v7/_drafts/screenshot.mdx +155 -0
  317. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  318. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  319. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  320. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  321. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  322. package/docs/v7/_drafts/test-recording.mdx +382 -0
  323. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  324. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  325. package/docs/v7/_drafts/vitest.mdx +535 -0
  326. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  327. package/docs/v7/ai.mdx +205 -0
  328. package/docs/v7/assert.mdx +316 -0
  329. package/docs/v7/aws-setup.mdx +449 -0
  330. package/docs/v7/cache.mdx +223 -0
  331. package/docs/v7/caching.mdx +128 -0
  332. package/docs/v7/captcha.mdx +159 -0
  333. package/docs/v7/ci-cd.mdx +603 -0
  334. package/docs/v7/click.mdx +287 -0
  335. package/docs/v7/client.mdx +478 -0
  336. package/docs/v7/copilot/auto-healing.mdx +265 -0
  337. package/docs/v7/copilot/creating-tests.mdx +156 -0
  338. package/docs/v7/copilot/github.mdx +143 -0
  339. package/docs/v7/copilot/running-tests.mdx +149 -0
  340. package/docs/v7/copilot/setup.mdx +143 -0
  341. package/docs/v7/customizing-devices.mdx +319 -0
  342. package/docs/v7/dashcam.mdx +419 -0
  343. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  344. package/docs/v7/device-config.mdx +317 -0
  345. package/docs/v7/double-click.mdx +102 -0
  346. package/docs/v7/elements.mdx +606 -0
  347. package/docs/v7/enterprise.mdx +9 -0
  348. package/docs/v7/errors.mdx +248 -0
  349. package/docs/v7/events.mdx +358 -0
  350. package/docs/v7/examples/ai.mdx +72 -0
  351. package/docs/v7/examples/assert.mdx +72 -0
  352. package/docs/v7/examples/captcha-api.mdx +92 -0
  353. package/docs/v7/examples/chrome-extension.mdx +132 -0
  354. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  355. package/docs/v7/examples/element-not-found.mdx +67 -0
  356. package/docs/v7/examples/exec-output.mdx +85 -0
  357. package/docs/v7/examples/exec-pwsh.mdx +83 -0
  358. package/docs/v7/examples/focus-window.mdx +62 -0
  359. package/docs/v7/examples/hover-image.mdx +94 -0
  360. package/docs/v7/examples/hover-text.mdx +69 -0
  361. package/docs/v7/examples/installer.mdx +91 -0
  362. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  363. package/docs/v7/examples/match-image.mdx +96 -0
  364. package/docs/v7/examples/press-keys.mdx +92 -0
  365. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  366. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  367. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  368. package/docs/v7/examples/scroll.mdx +81 -0
  369. package/docs/v7/examples/type.mdx +92 -0
  370. package/docs/v7/examples/windows-installer.mdx +89 -0
  371. package/docs/v7/exec.mdx +318 -0
  372. package/docs/v7/find.mdx +830 -0
  373. package/docs/v7/focus-application.mdx +294 -0
  374. package/docs/v7/generating-tests.mdx +36 -0
  375. package/docs/v7/hosted.mdx +158 -0
  376. package/docs/v7/hover.mdx +279 -0
  377. package/docs/v7/locating-elements.mdx +71 -0
  378. package/docs/v7/making-assertions.mdx +32 -0
  379. package/docs/v7/mcp.mdx +9 -0
  380. package/docs/v7/mouse-down.mdx +161 -0
  381. package/docs/v7/mouse-up.mdx +164 -0
  382. package/docs/v7/parse.mdx +237 -0
  383. package/docs/v7/performing-actions.mdx +54 -0
  384. package/docs/v7/press-keys.mdx +349 -0
  385. package/docs/v7/provision.mdx +333 -0
  386. package/docs/v7/quickstart.mdx +173 -0
  387. package/docs/v7/redraw.mdx +216 -0
  388. package/docs/v7/reusable-code.mdx +249 -0
  389. package/docs/v7/right-click.mdx +123 -0
  390. package/docs/v7/running-tests.mdx +185 -0
  391. package/docs/v7/screenshot.mdx +249 -0
  392. package/docs/v7/screenshots.mdx +186 -0
  393. package/docs/v7/scroll.mdx +336 -0
  394. package/docs/v7/secrets.mdx +115 -0
  395. package/docs/v7/self-hosted.mdx +149 -0
  396. package/docs/v7/type.mdx +358 -0
  397. package/docs/v7/variables.mdx +111 -0
  398. package/docs/v7/wait.mdx +52 -0
  399. package/docs/v7/waiting-for-elements.mdx +90 -0
  400. package/docs/v7/what-is-testdriver.mdx +54 -0
  401. package/eslint.config.js +67 -0
  402. package/examples/ai.test.mjs +31 -0
  403. package/examples/assert.test.mjs +47 -0
  404. package/examples/chrome-extension.test.mjs +97 -0
  405. package/examples/config.mjs +5 -0
  406. package/examples/element-not-found.test.mjs +27 -0
  407. package/examples/exec-output.test.mjs +60 -0
  408. package/examples/exec-pwsh.test.mjs +58 -0
  409. package/examples/findall-coffee-icons.test.mjs +42 -0
  410. package/examples/focus-window.test.mjs +37 -0
  411. package/examples/formatted-logging.test.mjs +27 -0
  412. package/examples/hover-image.test.mjs +53 -0
  413. package/examples/hover-text-with-description.test.mjs +57 -0
  414. package/examples/hover-text.test.mjs +28 -0
  415. package/examples/installer.test.mjs +50 -0
  416. package/examples/launch-vscode-linux.test.mjs +55 -0
  417. package/examples/match-image.test.mjs +55 -0
  418. package/examples/parse.test.mjs +19 -0
  419. package/examples/press-keys.test.mjs +44 -0
  420. package/examples/prompt.test.mjs +34 -0
  421. package/examples/scroll-keyboard.test.mjs +38 -0
  422. package/examples/scroll-until-image.test.mjs +40 -0
  423. package/examples/scroll.test.mjs +42 -0
  424. package/examples/type.test.mjs +46 -0
  425. package/examples/windows-installer.test.mjs +54 -0
  426. package/index.js +2 -0
  427. package/interfaces/cli/commands/init.js +438 -0
  428. package/interfaces/cli/commands/setup.js +382 -0
  429. package/interfaces/cli/lib/base.js +285 -0
  430. package/interfaces/cli.js +20 -0
  431. package/interfaces/junit-reporter.js +290 -0
  432. package/interfaces/logger.js +388 -0
  433. package/interfaces/readline.js +234 -0
  434. package/interfaces/shared-test-state.mjs +64 -0
  435. package/interfaces/vitest-plugin.d.ts +115 -0
  436. package/interfaces/vitest-plugin.mjs +1698 -0
  437. package/lib/captcha/solver.js +358 -0
  438. package/lib/core/Dashcam.js +533 -0
  439. package/lib/core/index.d.ts +172 -0
  440. package/lib/core/index.js +12 -0
  441. package/lib/environments.json +18 -0
  442. package/lib/github-comment-formatter.js +263 -0
  443. package/lib/github-comment.mjs +452 -0
  444. package/lib/init-project.js +575 -0
  445. package/lib/presets/index.mjs +331 -0
  446. package/lib/resolve-channel.js +46 -0
  447. package/lib/sentry.js +417 -0
  448. package/lib/vitest/hooks.d.ts +57 -0
  449. package/lib/vitest/hooks.mjs +674 -0
  450. package/lib/vitest/setup-aws.mjs +247 -0
  451. package/lib/vitest/setup-self-hosted.mjs +151 -0
  452. package/lib/vitest/setup.mjs +46 -0
  453. package/manual/captcha-api.test.mjs +51 -0
  454. package/manual/drag-and-drop.test.mjs +59 -0
  455. package/manual/flake-diffthreshold-001.test.mjs +9 -0
  456. package/manual/flake-diffthreshold-01.test.mjs +9 -0
  457. package/manual/flake-diffthreshold-05.test.mjs +9 -0
  458. package/manual/flake-noredraw-cache.test.mjs +9 -0
  459. package/manual/flake-noredraw-nocache.test.mjs +9 -0
  460. package/manual/flake-redraw-cache.test.mjs +9 -0
  461. package/manual/flake-redraw-nocache.test.mjs +9 -0
  462. package/manual/flake-rocket-match.test.mjs +30 -0
  463. package/manual/flake-shared.mjs +51 -0
  464. package/manual/no-provision.test.mjs +31 -0
  465. package/manual/packer-hover-image.test.mjs +176 -0
  466. package/manual/scroll-until-text.test.mjs +68 -0
  467. package/manual/test-init-command.js +223 -0
  468. package/mcp-server/README.md +322 -0
  469. package/mcp-server/dist/codegen.d.ts +9 -0
  470. package/mcp-server/dist/codegen.js +165 -0
  471. package/mcp-server/dist/mcp-app.html +114 -0
  472. package/mcp-server/dist/package.json +1 -0
  473. package/mcp-server/dist/provision-types.d.ts +290 -0
  474. package/mcp-server/dist/provision-types.js +174 -0
  475. package/mcp-server/dist/server.d.ts +6 -0
  476. package/mcp-server/dist/server.mjs +1925 -0
  477. package/mcp-server/dist/session.d.ts +85 -0
  478. package/mcp-server/dist/session.js +152 -0
  479. package/mcp-server/mcp-app.html +28 -0
  480. package/mcp-server/mcp-config.example.json +19 -0
  481. package/mcp-server/package-lock.json +4027 -0
  482. package/mcp-server/package.json +31 -0
  483. package/mcp-server/src/codegen.ts +189 -0
  484. package/mcp-server/src/mcp-app.css +360 -0
  485. package/mcp-server/src/mcp-app.ts +547 -0
  486. package/mcp-server/src/provision-types.ts +209 -0
  487. package/mcp-server/src/server.ts +2391 -0
  488. package/mcp-server/src/session.ts +194 -0
  489. package/mcp-server/tsconfig.json +16 -0
  490. package/mcp-server/vite.config.ts +23 -0
  491. package/package.json +158 -0
  492. package/schema.json +1046 -0
  493. package/scripts/generate-skills.js +94 -0
  494. package/sdk-log-formatter.js +1157 -0
  495. package/sdk.d.ts +1486 -0
  496. package/sdk.js +4336 -0
  497. package/setup/aws/cloudformation.yaml +463 -0
  498. package/setup/aws/disable-defender.sh +42 -0
  499. package/setup/aws/install-dev-runner.sh +79 -0
  500. package/setup/aws/spawn-runner.sh +289 -0
  501. package/test/captcha-solver.test.mjs +152 -0
  502. package/test/chrome-remote-debugging.test.mjs +66 -0
  503. package/test/duckduckgo/experiment.test.mjs +28 -0
  504. package/test/duckduckgo/setup.test.mjs +29 -0
  505. package/test/manual/debug-locate-response.js +82 -0
  506. package/test/manual/reconnect-provision.test.mjs +49 -0
  507. package/test/manual/test-console-logs.test.mjs +42 -0
  508. package/test/manual/test-find-api.js +73 -0
  509. package/test/manual/test-init.sh +54 -0
  510. package/test/manual/test-prompt-cache.js +97 -0
  511. package/test/manual/test-provision-auth.mjs +22 -0
  512. package/test/manual/test-sandbox-render.js +29 -0
  513. package/test/manual/test-sdk-methods.js +15 -0
  514. package/test/manual/test-sdk-refactor.js +53 -0
  515. package/test/manual/test-stack-trace.mjs +57 -0
  516. package/test/manual/verify-element-api.js +89 -0
  517. package/test/manual/verify-types.js +0 -0
  518. package/test/manual-unawaited-promise.test.mjs +31 -0
  519. package/vitest.config.mjs +58 -0
  520. package/vitest.runner.config.mjs +33 -0
  521. package/vscode-extension/.vscodeignore +12 -0
  522. package/vscode-extension/README.md +94 -0
  523. package/vscode-extension/media/icon.png +0 -0
  524. package/vscode-extension/package-lock.json +4126 -0
  525. package/vscode-extension/package.json +86 -0
  526. package/vscode-extension/src/extension.ts +829 -0
  527. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  528. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,1104 @@
1
+ const Ably = require("ably");
2
+ const axios = require("axios");
3
+ const { events } = require("../events");
4
+ const logger = require("./logger");
5
+ const { version } = require("../../package.json");
6
+ const { withRetry, getSentryTraceHeaders } = require("./sdk");
7
+ const sentry = require("../../lib/sentry");
8
+
9
+ const createSandbox = function (emitter, analytics, sessionInstance) {
10
+ class Sandbox {
11
+ constructor() {
12
+ this._ably = null;
13
+ this._sessionChannel = null;
14
+ this._channelName = null;
15
+ this.ps = {};
16
+ this._execBuffers = {}; // accumulate streamed exec.output chunks per requestId
17
+ this.heartbeat = null;
18
+ this.apiSocketConnected = false;
19
+ this.instanceSocketConnected = false;
20
+ this.authenticated = false;
21
+ this.instance = null;
22
+ this.messageId = 0;
23
+ this.uniqueId = Math.random().toString(36).substring(7);
24
+ this.os = null;
25
+ this.sessionInstance = sessionInstance;
26
+ this.traceId = null;
27
+ this.apiRoot = null;
28
+ this.apiKey = null;
29
+ this._lastConnectParams = null;
30
+ this._teamId = null;
31
+ this._sandboxId = null;
32
+
33
+ // Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
34
+ this._publishLastTime = 0;
35
+ this._publishMinIntervalMs = 25; // 40 msg/sec max, safely under Ably's 50 limit
36
+ this._publishCount = 0;
37
+ this._publishWindowStart = Date.now();
38
+ }
39
+
40
+ getTraceId() {
41
+ return this.traceId;
42
+ }
43
+
44
+ getTraceUrl() {
45
+ if (!this.traceId) return null;
46
+ return (
47
+ "https://testdriver.sentry.io/explore/traces/trace/" + this.traceId
48
+ );
49
+ }
50
+
51
+ async _initAbly(ablyToken, channelName) {
52
+ if (this._ably) {
53
+ try {
54
+ this._ably.close();
55
+ } catch (e) {
56
+ /* ignore */
57
+ }
58
+ }
59
+ this._channelName = channelName;
60
+ var self = this;
61
+
62
+ this._ably = new Ably.Realtime({
63
+ authCallback: function (tokenParams, callback) {
64
+ callback(null, ablyToken);
65
+ },
66
+ clientId: "sdk-" + this._sandboxId,
67
+ disconnectedRetryTimeout: 5000, // retry reconnect every 5s (default 15s)
68
+ suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
69
+ });
70
+
71
+ logger.log(`[ably] Connecting as sdk-${this._sandboxId}...`);
72
+
73
+ await new Promise(function (resolve, reject) {
74
+ self._ably.connection.on("connected", resolve);
75
+ self._ably.connection.on("failed", function () {
76
+ reject(new Error("Ably connection failed"));
77
+ });
78
+ setTimeout(function () {
79
+ reject(new Error("Ably connection timeout"));
80
+ }, 30000);
81
+ });
82
+
83
+ this._sessionChannel = this._ably.channels.get(channelName);
84
+
85
+ logger.log(`[ably] Channel initialized: ${channelName}`);
86
+
87
+ // Enter presence on the session channel so the API can count connected SDK clients
88
+ try {
89
+ await this._sessionChannel.presence.enter({
90
+ sandboxId: this._sandboxId,
91
+ connectedAt: Date.now(),
92
+ });
93
+ logger.log(`[ably] Entered presence on session channel (sandbox=${this._sandboxId})`);
94
+ } catch (e) {
95
+ // Non-fatal — presence is used for concurrency counting, not critical path
96
+ logger.warn("Failed to enter presence on session channel: " + (e.message || e));
97
+ }
98
+
99
+ this._sessionChannel.subscribe("response", function (msg) {
100
+ var message = msg.data;
101
+ if (!message) return;
102
+
103
+ logger.log(`[ably] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
104
+
105
+ if (message.type === "sandbox.progress") {
106
+ emitter.emit(events.sandbox.progress, {
107
+ step: message.step,
108
+ message: message.message,
109
+ });
110
+ return;
111
+ }
112
+
113
+ if (
114
+ message.type === "before.file" ||
115
+ message.type === "after.file" ||
116
+ message.type === "screenshot.file"
117
+ ) {
118
+ emitter.emit(events.sandbox.file, message);
119
+ return;
120
+ }
121
+
122
+ // Streaming exec output chunks — accumulate per requestId so the
123
+ // full stdout can be reconstructed when the final response arrives.
124
+ // (The runner streams stdout in ~16KB chunks and omits it from the
125
+ // final response to stay under Ably's 64KB message limit.)
126
+ if (message.type === "exec.output") {
127
+ if (message.requestId) {
128
+ if (!self._execBuffers[message.requestId]) {
129
+ self._execBuffers[message.requestId] = '';
130
+ }
131
+ self._execBuffers[message.requestId] += (message.chunk || '');
132
+ }
133
+ emitter.emit(events.exec.output, { chunk: message.chunk, requestId: message.requestId });
134
+ return;
135
+ }
136
+
137
+ // Runner debug logs — only received when debug mode is enabled
138
+ if (message.type === "runner.log") {
139
+ var logLevel = message.level || "info";
140
+ var logMsg = "[runner] " + (message.message || "");
141
+ if (logLevel === "error") {
142
+ logger.error(logMsg);
143
+ } else {
144
+ logger.log(logMsg);
145
+ }
146
+ emitter.emit(events.runner.log, {
147
+ level: logLevel,
148
+ message: message.message,
149
+ timestamp: message.timestamp,
150
+ });
151
+ return;
152
+ }
153
+
154
+ if (!message.requestId || !self.ps[message.requestId]) {
155
+ var debugMode =
156
+ process.env.VERBOSE || process.env.TD_DEBUG;
157
+ if (debugMode) {
158
+ console.warn(
159
+ "No pending promise found for requestId:",
160
+ message.requestId,
161
+ );
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (message.error) {
167
+ var pendingMessage =
168
+ self.ps[message.requestId] &&
169
+ self.ps[message.requestId].message;
170
+ if (!pendingMessage || pendingMessage.type !== "output") {
171
+ emitter.emit(events.error.sandbox, message.errorMessage);
172
+ }
173
+ var error = new Error(message.errorMessage || "Sandbox error");
174
+ error.responseData = message;
175
+ delete self._execBuffers[message.requestId];
176
+ self.ps[message.requestId].reject(error);
177
+ } else {
178
+ emitter.emit(events.sandbox.received);
179
+ if (self.ps[message.requestId]) {
180
+ // Unwrap the result from the Ably response envelope
181
+ // The runner sends { requestId, type, result, success }
182
+ // But SDK commands expect just the result object
183
+ var resolvedValue = message.result !== undefined ? message.result : message;
184
+
185
+ // For exec (commands.run): the runner streams stdout via exec.output
186
+ // chunks and sends only returncode+stderr in the final response.
187
+ // Reconstruct stdout from the accumulated buffer.
188
+ var streamedStdout = self._execBuffers[message.requestId];
189
+ if (streamedStdout !== undefined && resolvedValue && resolvedValue.out) {
190
+ resolvedValue.out.stdout = streamedStdout;
191
+ }
192
+ delete self._execBuffers[message.requestId];
193
+
194
+ self.ps[message.requestId].resolve(resolvedValue);
195
+ }
196
+ }
197
+ delete self.ps[message.requestId];
198
+ });
199
+
200
+ this._sessionChannel.subscribe("file", function (msg) {
201
+ var message = msg.data;
202
+ if (!message) return;
203
+ logger.log(`[ably] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
204
+ if (message.requestId && self.ps[message.requestId]) {
205
+ emitter.emit(events.sandbox.received);
206
+ self.ps[message.requestId].resolve(message);
207
+ delete self.ps[message.requestId];
208
+ }
209
+ emitter.emit(events.sandbox.file, message);
210
+ });
211
+
212
+ this.heartbeat = setInterval(function () {}, 5000);
213
+ if (this.heartbeat.unref) this.heartbeat.unref();
214
+
215
+ // ─── Periodic stats logging ────────────────────────────────────────
216
+ this._statsInterval = setInterval(() => {
217
+ const connState = this._ably ? this._ably.connection.state : 'no-client';
218
+ const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
219
+ const pending = Object.keys(this.ps).length;
220
+ logger.log(`[ably][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
221
+ }, 10000);
222
+ if (this._statsInterval.unref) this._statsInterval.unref();
223
+
224
+ this._ably.connection.on("disconnected", function () {
225
+ logger.log("[ably] Connection: disconnected - will auto-reconnect");
226
+ });
227
+
228
+ this._ably.connection.on("connected", function () {
229
+ // Log reconnection so the user knows the blip was recovered
230
+ logger.log("[ably] Connection: reconnected");
231
+ });
232
+
233
+ this._ably.connection.on("suspended", function () {
234
+ logger.warn("[ably] Connection: suspended - connection lost for extended period, will keep retrying");
235
+ });
236
+
237
+ this._ably.connection.on("failed", function () {
238
+ logger.error("[ably] Connection: failed");
239
+ self.apiSocketConnected = false;
240
+ self.instanceSocketConnected = false;
241
+ emitter.emit(events.error.sandbox, "Ably connection failed");
242
+ });
243
+ }
244
+
245
+ /**
246
+ * POST to the API with retry for transient network errors (via withRetry)
247
+ * and infinite polling for CONCURRENCY_LIMIT_EXCEEDED (until vitest's
248
+ * testTimeout kills the test).
249
+ */
250
+ async _httpPostWithConcurrencyRetry(path, body, timeout) {
251
+ var concurrencyRetryInterval = 10000; // 10 seconds between concurrency retries
252
+ var startTime = Date.now();
253
+ var sessionId = this.sessionInstance ? this.sessionInstance.get() : null;
254
+
255
+ var self = this;
256
+ var makeRequest = function () {
257
+ return axios({
258
+ method: "post",
259
+ url: self.apiRoot + path,
260
+ data: body,
261
+ headers: {
262
+ "Content-Type": "application/json",
263
+ "User-Agent": "TestDriverSDK/" + version + " (Node.js " + process.version + ")",
264
+ ...getSentryTraceHeaders(sessionId),
265
+ },
266
+ timeout: timeout || 120000,
267
+ });
268
+ };
269
+
270
+ while (true) {
271
+ try {
272
+ var response = await withRetry(makeRequest, {
273
+ retryConfig: {
274
+ maxRetries: 3,
275
+ baseDelayMs: 2000,
276
+ retryableStatusCodes: [500, 502, 503, 504], // Don't retry 429 — handled below
277
+ },
278
+ onRetry: function (attempt, error, delayMs) {
279
+ var elapsed = Date.now() - startTime;
280
+ logger.warn(
281
+ "Transient network error: " + (error.message || error.code) +
282
+ " — POST " + path +
283
+ " — retry " + attempt + "/3" +
284
+ " in " + (delayMs / 1000).toFixed(1) + "s" +
285
+ " (" + Math.round(elapsed / 1000) + "s elapsed)...",
286
+ );
287
+ },
288
+ });
289
+ return response.data;
290
+ } catch (err) {
291
+ // Concurrency limit — poll forever until a slot opens
292
+ var responseData = err.response && err.response.data;
293
+ if (responseData && responseData.errorCode === "CONCURRENCY_LIMIT_EXCEEDED") {
294
+ var elapsed = Date.now() - startTime;
295
+ logger.log(
296
+ "Concurrency limit reached — waiting " +
297
+ concurrencyRetryInterval / 1000 +
298
+ "s for a slot to become available (" +
299
+ Math.round(elapsed / 1000) +
300
+ "s elapsed)...",
301
+ );
302
+ await new Promise(function (resolve) {
303
+ var t = setTimeout(resolve, concurrencyRetryInterval);
304
+ if (t.unref) t.unref();
305
+ });
306
+ continue;
307
+ }
308
+
309
+ // Non-retryable HTTP error — preserve responseData for callers
310
+ if (responseData) {
311
+ var httpErr = new Error(
312
+ responseData.errorMessage || responseData.message || "HTTP " + err.response.status,
313
+ );
314
+ httpErr.responseData = responseData;
315
+ throw httpErr;
316
+ }
317
+
318
+ throw err;
319
+ }
320
+ }
321
+ }
322
+
323
+ send(message, timeout) {
324
+ if (timeout === undefined) timeout = 300000;
325
+ if (message.type === "create" || message.type === "direct") {
326
+ return this._sendHttp(message, timeout);
327
+ }
328
+ return this._sendAbly(message, timeout);
329
+ }
330
+
331
+ async _sendHttp(message, timeout) {
332
+ var sessionId = this.sessionInstance
333
+ ? this.sessionInstance.get()
334
+ : null;
335
+ var body = {
336
+ apiKey: this.apiKey,
337
+ version: version,
338
+ os: message.os || this.os,
339
+ session: sessionId,
340
+ apiRoot: this.apiRoot,
341
+ };
342
+
343
+ if (message.type === "create") {
344
+ body.os = message.os || this.os || "linux";
345
+ body.resolution = message.resolution;
346
+ body.ci = message.ci;
347
+ if (message.ami) body.ami = message.ami;
348
+ if (message.instanceType) body.instanceType = message.instanceType;
349
+ if (message.keepAlive !== undefined) body.keepAlive = message.keepAlive;
350
+ }
351
+
352
+ if (message.type === "direct") {
353
+ body.ip = message.ip;
354
+ body.resolution = message.resolution;
355
+ body.ci = message.ci;
356
+ if (message.instanceId) body.instanceId = message.instanceId;
357
+ }
358
+
359
+ var reply = await this._httpPostWithConcurrencyRetry(
360
+ "/api/v7/sandbox/authenticate",
361
+ body,
362
+ timeout,
363
+ );
364
+
365
+ if (!reply.success) {
366
+ var err = new Error(
367
+ reply.errorMessage || "Failed to allocate sandbox",
368
+ );
369
+ err.responseData = reply;
370
+ throw err;
371
+ }
372
+
373
+ this._sandboxId = reply.sandboxId;
374
+ this._teamId = reply.teamId;
375
+
376
+ if (reply.ably && reply.ably.token) {
377
+ await this._initAbly(reply.ably.token, reply.ably.channel);
378
+ this.instanceSocketConnected = true;
379
+
380
+ // Tell the runner to enable debug log forwarding if debug mode is on
381
+ var debugMode =
382
+ process.env.VERBOSE || process.env.TD_DEBUG;
383
+ if (debugMode && this._sessionChannel) {
384
+ this._sessionChannel.publish("control", {
385
+ type: "debug",
386
+ enabled: true,
387
+ });
388
+ }
389
+ }
390
+
391
+ if (message.type === "create") {
392
+ // E2B (Linux) sandboxes: the API proxies commands and returns a url directly.
393
+ // No runner agent involved — skip runner.ready wait.
394
+ if (reply.url) {
395
+ logger.log(`E2B sandbox ready — url=${reply.url}`);
396
+ return {
397
+ success: true,
398
+ sandbox: {
399
+ sandboxId: reply.sandboxId,
400
+ instanceId: reply.sandbox?.sandboxId || reply.sandboxId,
401
+ os: body.os || 'linux',
402
+ url: reply.url,
403
+ },
404
+ };
405
+ }
406
+
407
+ const runnerIp = reply.runner && reply.runner.ip;
408
+ const noVncPort = reply.runner && reply.runner.noVncPort;
409
+ const runnerVncUrl = reply.runner && reply.runner.vncUrl;
410
+
411
+ logger.log(`Runner claimed — ip=${runnerIp || 'none'}, os=${reply.runner?.os || 'unknown'}, noVncPort=${noVncPort || 'not reported'}, vncUrl=${runnerVncUrl || 'not reported'}`);
412
+
413
+ // For cloud Windows sandboxes (no runner in reply), wait for the
414
+ // agent to signal readiness before sending commands. Without this
415
+ // gate, commands published before the agent subscribes are lost.
416
+ var self = this;
417
+ if (!reply.runner && this._sessionChannel) {
418
+ logger.log('Waiting for runner agent to signal readiness...');
419
+ var readyTimeout = 120000; // 120s — allows for EC2 boot + agent startup
420
+ await new Promise(function (resolve, reject) {
421
+ var resolved = false;
422
+ function finish(data) {
423
+ if (resolved) return;
424
+ resolved = true;
425
+ clearTimeout(timer);
426
+ self._sessionChannel.unsubscribe('control', onCtrl);
427
+ // Update runner info if provided
428
+ if (data && data.os) reply.runner = reply.runner || {};
429
+ if (data && data.os && reply.runner) reply.runner.os = data.os;
430
+ if (data && data.ip && reply.runner) reply.runner.ip = data.ip;
431
+ if (data && data.runnerVersion && reply.runner) reply.runner.version = data.runnerVersion;
432
+ logger.log('Runner agent ready (os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
433
+ if (data && data.update) {
434
+ var u = data.update;
435
+ if (u.status === 'up-to-date') {
436
+ logger.log('Runner is up to date (v' + u.localVersion + ')');
437
+ } else if (u.status === 'updated') {
438
+ logger.log('Runner was auto-updated: v' + u.localVersion + ' \u2192 v' + u.remoteVersion);
439
+ } else if (u.status === 'available:major') {
440
+ logger.warn('Runner update available but not auto-installed (major/minor): v' + u.localVersion + ' \u2192 v' + u.remoteVersion);
441
+ } else if (u.status && u.status.startsWith('error:')) {
442
+ logger.warn('Runner update check failed: ' + u.status.slice(6));
443
+ }
444
+ }
445
+ resolve();
446
+ }
447
+
448
+ var timer = setTimeout(function () {
449
+ if (!resolved) {
450
+ resolved = true;
451
+ self._sessionChannel.unsubscribe('control', onCtrl);
452
+ var err = new Error('Runner agent did not signal readiness within ' + readyTimeout + 'ms');
453
+ sentry.captureException(err, {
454
+ tags: { phase: 'runner_ready', connection_type: 'create' },
455
+ extra: { readyTimeout: readyTimeout, sandboxId: reply.sandboxId },
456
+ });
457
+ reject(err);
458
+ }
459
+ }, readyTimeout);
460
+ if (timer.unref) timer.unref();
461
+
462
+ // Listen for live runner.ready messages
463
+ var onCtrl;
464
+ onCtrl = function (msg) {
465
+ var data = msg.data;
466
+ if (data && data.type === 'runner.ready') {
467
+ finish(data);
468
+ }
469
+ };
470
+ self._sessionChannel.subscribe('control', onCtrl);
471
+
472
+ // Also check channel history in case runner.ready was published
473
+ // before we subscribed (race condition on fast-booting agents).
474
+ try {
475
+ self._sessionChannel.history({ limit: 50 }, function (err, page) {
476
+ if (err) {
477
+ logger.warn('History lookup failed (non-fatal): ' + (err.message || err));
478
+ return;
479
+ }
480
+ if (page && page.items) {
481
+ for (var i = 0; i < page.items.length; i++) {
482
+ var item = page.items[i];
483
+ if (item.name === 'control' && item.data && item.data.type === 'runner.ready') {
484
+ logger.log('Found runner.ready in channel history');
485
+ finish(item.data);
486
+ return;
487
+ }
488
+ }
489
+ }
490
+ });
491
+ } catch (histErr) {
492
+ logger.warn('History call threw (non-fatal): ' + (histErr.message || histErr));
493
+ }
494
+ });
495
+ }
496
+ // Prefer the full vncUrl reported by the runner (infrastructure-agnostic).
497
+ // Fall back to constructing from ip + noVncPort for older runners.
498
+ let url;
499
+ if (runnerVncUrl) {
500
+ url = runnerVncUrl;
501
+ logger.log(`Using runner-provided vncUrl: ${url}`);
502
+ } else if (runnerIp && noVncPort) {
503
+ url = `http://${runnerIp}:${noVncPort}/vnc_lite.html?token=V3b8wG9`;
504
+ logger.log(`noVNC URL constructed from runner ip+port: ${url}`);
505
+ } else if (runnerIp) {
506
+ url = "http://" + runnerIp;
507
+ logger.warn(`Runner did not report noVNC port — using bare IP: ${url}`);
508
+ } else {
509
+ logger.warn('Runner has no IP — preview will not be available');
510
+ }
511
+ return {
512
+ success: true,
513
+ sandbox: {
514
+ sandboxId: reply.sandboxId,
515
+ instanceId: reply.sandboxId,
516
+ os: reply.runner?.os || body.os,
517
+ ip: runnerIp,
518
+ url: url,
519
+ vncPort: noVncPort || undefined,
520
+ runner: reply.runner,
521
+ },
522
+ };
523
+ }
524
+
525
+ if (message.type === "direct") {
526
+ // If the API returned agent config and we have an instanceId,
527
+ // provision the config to the instance via SSM (client-side).
528
+ // This runs from the user's infrastructure where AWS permissions exist,
529
+ // rather than from the API server.
530
+ // NOTE: For direct connections, the user MUST provide the AWS instanceId
531
+ // because the API only knows the sandboxId, not the actual EC2 instance ID.
532
+ var instanceId = message.instanceId;
533
+ if (reply.agentConfig && instanceId) {
534
+ logger.log('Provisioning agent config to instance ' + instanceId + ' via SSM...');
535
+ await this._provisionAgentConfig(instanceId, reply.agentConfig);
536
+ logger.log('Agent config provisioned successfully.');
537
+ } else if (reply.agentConfig && !instanceId) {
538
+ logger.log('Warning: agentConfig returned but no instanceId provided - cannot provision via SSM');
539
+ }
540
+
541
+ // If the API returned agent credentials (reply.agent present),
542
+ // wait for the runner agent to signal readiness before sending commands.
543
+ // Without this gate, commands published before the agent subscribes are lost.
544
+ var self = this;
545
+ if (reply.agent && this._sessionChannel) {
546
+ logger.log('Waiting for runner agent to signal readiness (direct connection)...');
547
+ var readyTimeout = 120000; // 120s — allows for SSM provisioning + agent startup
548
+ await new Promise(function (resolve, reject) {
549
+ var resolved = false;
550
+ function finish(data) {
551
+ if (resolved) return;
552
+ resolved = true;
553
+ clearTimeout(timer);
554
+ self._sessionChannel.unsubscribe('control', onCtrl);
555
+ logger.log('Runner agent ready (direct, os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
556
+ if (data && data.update) {
557
+ var u = data.update;
558
+ if (u.status === 'up-to-date') {
559
+ logger.log('Runner is up to date (v' + u.localVersion + ')');
560
+ } else if (u.status === 'updated') {
561
+ logger.log('Runner was auto-updated: v' + u.localVersion + ' \u2192 v' + u.remoteVersion);
562
+ } else if (u.status === 'available:major') {
563
+ logger.warn('Runner update available but not auto-installed (major/minor): v' + u.localVersion + ' \u2192 v' + u.remoteVersion);
564
+ } else if (u.status && u.status.startsWith('error:')) {
565
+ logger.warn('Runner update check failed: ' + u.status.slice(6));
566
+ }
567
+ }
568
+ resolve();
569
+ }
570
+
571
+ var timer = setTimeout(function () {
572
+ if (!resolved) {
573
+ resolved = true;
574
+ self._sessionChannel.unsubscribe('control', onCtrl);
575
+ var err = new Error('Runner agent did not signal readiness within ' + readyTimeout + 'ms (direct connection)');
576
+ sentry.captureException(err, {
577
+ tags: { phase: 'runner_ready', connection_type: 'direct' },
578
+ extra: { readyTimeout: readyTimeout, sandboxId: reply.sandboxId, instanceId: message.instanceId },
579
+ });
580
+ reject(err);
581
+ }
582
+ }, readyTimeout);
583
+ if (timer.unref) timer.unref();
584
+
585
+ // Listen for live runner.ready messages
586
+ var onCtrl;
587
+ onCtrl = function (msg) {
588
+ var data = msg.data;
589
+ if (data && data.type === 'runner.ready') {
590
+ finish(data);
591
+ }
592
+ };
593
+ self._sessionChannel.subscribe('control', onCtrl);
594
+
595
+ // Also check channel history in case runner.ready was published
596
+ // before we subscribed (race condition on fast-booting agents).
597
+ try {
598
+ self._sessionChannel.history({ limit: 50 }, function (err, page) {
599
+ if (err) {
600
+ logger.warn('History lookup failed (non-fatal): ' + (err.message || err));
601
+ return;
602
+ }
603
+ if (page && page.items) {
604
+ for (var i = 0; i < page.items.length; i++) {
605
+ var item = page.items[i];
606
+ if (item.name === 'control' && item.data && item.data.type === 'runner.ready') {
607
+ logger.log('Found runner.ready in channel history (direct)');
608
+ finish(item.data);
609
+ return;
610
+ }
611
+ }
612
+ }
613
+ });
614
+ } catch (histErr) {
615
+ logger.warn('History call threw (non-fatal): ' + (histErr.message || histErr));
616
+ }
617
+ });
618
+ }
619
+
620
+ // Construct VNC URL — use port 8080 (nginx noVNC proxy) for Windows instances
621
+ var directUrl = message.ip ? "http://" + message.ip + ":8080/vnc_lite.html?token=V3b8wG9" : undefined;
622
+
623
+ return {
624
+ success: true,
625
+ instance: {
626
+ instanceId: reply.sandboxId,
627
+ sandboxId: reply.sandboxId,
628
+ ip: message.ip,
629
+ url: directUrl || "http://" + message.ip,
630
+ },
631
+ };
632
+ }
633
+
634
+ return reply;
635
+ }
636
+
637
+ _sendAbly(message, timeout) {
638
+ if (timeout === undefined) timeout = 300000;
639
+
640
+ if (!this._sessionChannel || !this._ably) {
641
+ return Promise.reject(
642
+ new Error("Sandbox not connected (no Ably client)"),
643
+ );
644
+ }
645
+
646
+ // If temporarily disconnected, wait up to 30s for reconnection
647
+ // instead of failing immediately (dashcam uploads can cause brief blips)
648
+ var self = this;
649
+ var connState = this._ably.connection.state;
650
+ if (connState !== "connected") {
651
+ if (connState === "disconnected" || connState === "connecting" || connState === "suspended") {
652
+ logger.log("Ably is " + connState + ", waiting for reconnect before sending...");
653
+ var waitForConnect = new Promise(function (resolve, reject) {
654
+ var timer = setTimeout(function () {
655
+ self._ably.connection.off("connected", onConnected);
656
+ self._ably.connection.off("failed", onFailed);
657
+ reject(new Error("Sandbox not connected after waiting 30s (state: " + self._ably.connection.state + ")"));
658
+ }, 30000);
659
+ if (timer.unref) timer.unref();
660
+ function onConnected() {
661
+ clearTimeout(timer);
662
+ self._ably.connection.off("failed", onFailed);
663
+ resolve();
664
+ }
665
+ function onFailed() {
666
+ clearTimeout(timer);
667
+ self._ably.connection.off("connected", onConnected);
668
+ reject(new Error("Ably connection failed while waiting to send"));
669
+ }
670
+ self._ably.connection.once("connected", onConnected);
671
+ self._ably.connection.once("failed", onFailed);
672
+ });
673
+ return waitForConnect.then(function () {
674
+ return self._sendAbly(message, timeout);
675
+ });
676
+ }
677
+ return Promise.reject(
678
+ new Error("Sandbox not connected (state: " + connState + ")"),
679
+ );
680
+ }
681
+
682
+ this.messageId++;
683
+ message.requestId = this.uniqueId + "-" + this.messageId;
684
+
685
+ if (message.os) this.os = message.os;
686
+ if (this.os && !message.os) message.os = this.os;
687
+
688
+ if (this.sessionInstance && !message.session) {
689
+ var sessionId = this.sessionInstance.get();
690
+ if (sessionId) message.session = sessionId;
691
+ }
692
+
693
+ if (
694
+ this._lastConnectParams &&
695
+ this._lastConnectParams.sandboxId &&
696
+ !message.sandboxId
697
+ ) {
698
+ var id = this._lastConnectParams.sandboxId;
699
+ if (id && !/^\d+\.\d+\.\d+\.\d+$/.test(id)) {
700
+ message.sandboxId = id;
701
+ }
702
+ }
703
+
704
+ // Attach Sentry distributed trace headers for runner-side tracing
705
+ var traceSessionId = this.sessionInstance
706
+ ? this.sessionInstance.get()
707
+ : message.session;
708
+ if (traceSessionId) {
709
+ var traceHeaders = getSentryTraceHeaders(traceSessionId);
710
+ if (traceHeaders["sentry-trace"]) {
711
+ message.sentryTrace = traceHeaders["sentry-trace"];
712
+ message.baggage = traceHeaders.baggage;
713
+ }
714
+ }
715
+
716
+ var resolvePromise, rejectPromise;
717
+ var self = this;
718
+
719
+ var p = new Promise(function (resolve, reject) {
720
+ resolvePromise = resolve;
721
+ rejectPromise = reject;
722
+ });
723
+
724
+ var requestId = message.requestId;
725
+
726
+ var timeoutId = setTimeout(function () {
727
+ if (self.ps[requestId]) {
728
+ delete self.ps[requestId];
729
+ delete self._execBuffers[requestId];
730
+ rejectPromise(
731
+ new Error(
732
+ "Sandbox message '" +
733
+ message.type +
734
+ "' timed out after " +
735
+ timeout +
736
+ "ms",
737
+ ),
738
+ );
739
+ }
740
+ }, timeout);
741
+ if (timeoutId.unref) timeoutId.unref();
742
+
743
+ this.ps[requestId] = {
744
+ promise: p,
745
+ resolve: function (result) {
746
+ clearTimeout(timeoutId);
747
+ resolvePromise(result);
748
+ },
749
+ reject: function (error) {
750
+ clearTimeout(timeoutId);
751
+ rejectPromise(error);
752
+ },
753
+ message: message,
754
+ startTime: Date.now(),
755
+ };
756
+
757
+ if (message.type === "output") {
758
+ p.catch(function () {});
759
+ }
760
+
761
+ this._throttledPublish(this._sessionChannel, "command", message)
762
+ .then(function () {
763
+ emitter.emit(events.sandbox.sent, message);
764
+ })
765
+ .catch(function (err) {
766
+ if (self.ps[requestId]) {
767
+ clearTimeout(timeoutId);
768
+ delete self.ps[requestId];
769
+ rejectPromise(
770
+ new Error("Failed to send message: " + err.message),
771
+ );
772
+ }
773
+ });
774
+
775
+ return p;
776
+ }
777
+
778
+ /**
779
+ * Throttled publish to stay under Ably's 50 msg/sec per-connection limit.
780
+ * Also tracks and logs the current publish rate for debugging.
781
+ * @param {Object} channel - Ably channel to publish on
782
+ * @param {string} eventName - Event name for the publish
783
+ * @param {Object} message - Message payload
784
+ * @returns {Promise} - Resolves when publish completes
785
+ */
786
+ async _throttledPublish(channel, eventName, message) {
787
+ var self = this;
788
+ var now = Date.now();
789
+
790
+ // Rate limiting: wait if too soon since last publish
791
+ var elapsed = now - this._publishLastTime;
792
+ if (elapsed < this._publishMinIntervalMs) {
793
+ var waitMs = this._publishMinIntervalMs - elapsed;
794
+ await new Promise(function (resolve) {
795
+ var timer = setTimeout(resolve, waitMs);
796
+ if (timer.unref) timer.unref();
797
+ });
798
+ }
799
+ this._publishLastTime = Date.now();
800
+
801
+ // Metrics: track messages per second
802
+ this._publishCount++;
803
+ var windowElapsed = Date.now() - this._publishWindowStart;
804
+ if (windowElapsed >= 1000) {
805
+ var rate = (this._publishCount / windowElapsed) * 1000;
806
+ var rateStr = rate.toFixed(1);
807
+
808
+ // Log rate - warning if approaching limit, debug otherwise
809
+ if (rate > 45) {
810
+ logger.warn("Ably publish rate: " + rateStr + " msg/sec (approaching 50/sec limit)");
811
+ } else if (process.env.VERBOSE || process.env.TD_DEBUG) {
812
+ logger.log("Ably publish rate: " + rateStr + " msg/sec");
813
+ }
814
+
815
+ // Reset window
816
+ this._publishCount = 0;
817
+ this._publishWindowStart = Date.now();
818
+ }
819
+
820
+ return channel.publish(eventName, message).then(function () {
821
+ logger.log(`[ably] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
822
+ });
823
+ }
824
+
825
+ async auth(apiKey) {
826
+ this.apiKey = apiKey;
827
+ var sessionId = this.sessionInstance
828
+ ? this.sessionInstance.get()
829
+ : null;
830
+
831
+ var reply = await this._httpPostWithConcurrencyRetry(
832
+ "/api/v7/sandbox/authenticate",
833
+ {
834
+ apiKey: apiKey,
835
+ version: version,
836
+ session: sessionId,
837
+ },
838
+ );
839
+
840
+ if (reply.success) {
841
+ this.authenticated = true;
842
+ this.apiSocketConnected = true;
843
+ this._teamId = reply.teamId;
844
+
845
+ if (reply.traceId) {
846
+ this.traceId = reply.traceId;
847
+ logger.log("");
848
+ logger.log("Trace Report (Share When Reporting Bugs):");
849
+ logger.log(
850
+ "https://testdriver.sentry.io/explore/traces/trace/" +
851
+ reply.traceId,
852
+ );
853
+ }
854
+
855
+ emitter.emit(events.sandbox.authenticated, {
856
+ traceId: reply.traceId,
857
+ });
858
+ return true;
859
+ }
860
+
861
+ return false;
862
+ }
863
+
864
+ setConnectionParams(params) {
865
+ this._lastConnectParams = params ? Object.assign({}, params) : null;
866
+ }
867
+
868
+ async connect(sandboxId, persist, keepAlive) {
869
+ if (persist === undefined) persist = false;
870
+ if (keepAlive === undefined) keepAlive = null;
871
+ var sessionId = this.sessionInstance
872
+ ? this.sessionInstance.get()
873
+ : null;
874
+
875
+ var reply = await this._httpPostWithConcurrencyRetry(
876
+ "/api/v7/sandbox/authenticate",
877
+ {
878
+ apiKey: this.apiKey,
879
+ version: version,
880
+ sandboxId: sandboxId,
881
+ session: sessionId,
882
+ keepAlive: keepAlive || undefined,
883
+ },
884
+ );
885
+
886
+ if (!reply.success) {
887
+ this.setConnectionParams(null);
888
+ throw new Error(reply.errorMessage || "Failed to connect to sandbox");
889
+ }
890
+
891
+ this._sandboxId = reply.sandboxId;
892
+
893
+ if (reply.ably && reply.ably.token) {
894
+ await this._initAbly(reply.ably.token, reply.ably.channel);
895
+ }
896
+
897
+ this.setConnectionParams({
898
+ sandboxId: sandboxId,
899
+ persist: persist,
900
+ keepAlive: keepAlive,
901
+ });
902
+ this.instanceSocketConnected = true;
903
+ emitter.emit(events.sandbox.connected);
904
+
905
+ // Prefer runner-provided vncUrl, fall back to ip+port, then bare IP
906
+ const reconnectRunner = reply.runner || {};
907
+ const reconnectVncUrl = reconnectRunner.vncUrl;
908
+ const reconnectNoVncPort = reconnectRunner.noVncPort;
909
+ const reconnectIp = reconnectRunner.ip;
910
+ let reconnectUrl;
911
+ if (reconnectVncUrl) {
912
+ reconnectUrl = reconnectVncUrl;
913
+ } else if (reconnectIp && reconnectNoVncPort) {
914
+ reconnectUrl = `http://${reconnectIp}:${reconnectNoVncPort}/vnc_lite.html`;
915
+ } else if (reconnectIp) {
916
+ reconnectUrl = "http://" + reconnectIp;
917
+ }
918
+
919
+ return {
920
+ success: true,
921
+ url: reconnectUrl,
922
+ sandbox: {
923
+ sandboxId: reply.sandboxId,
924
+ instanceId: reply.sandboxId,
925
+ os: reconnectRunner.os || undefined,
926
+ ip: reconnectIp || undefined,
927
+ url: reconnectUrl,
928
+ vncPort: reconnectNoVncPort || undefined,
929
+ },
930
+ };
931
+ }
932
+
933
+ async boot(apiRoot) {
934
+ if (apiRoot) this.apiRoot = apiRoot;
935
+ return this;
936
+ }
937
+
938
+ async close() {
939
+ if (this.heartbeat) {
940
+ clearInterval(this.heartbeat);
941
+ this.heartbeat = null;
942
+ }
943
+ if (this._statsInterval) {
944
+ clearInterval(this._statsInterval);
945
+ this._statsInterval = null;
946
+ }
947
+
948
+ // Send end-session control message to runner before disconnecting
949
+ if (this._sessionChannel && this._ably?.connection?.state === 'connected') {
950
+ try {
951
+ logger.log('[ably] Publishing control: type=end-session');
952
+ await this._sessionChannel.publish('control', { type: 'end-session' });
953
+ } catch (e) {
954
+ // Ignore - best effort
955
+ }
956
+ }
957
+
958
+ // Leave presence on session channel
959
+ if (this._sessionChannel) {
960
+ try {
961
+ logger.log('[ably] Leaving presence on session channel');
962
+ await this._sessionChannel.presence.leave();
963
+ } catch (e) {
964
+ // ignore - best effort, Ably will auto-leave on disconnect
965
+ }
966
+ }
967
+
968
+ try {
969
+ logger.log('[ably] Detaching session channel');
970
+ if (this._sessionChannel) {
971
+ await this._sessionChannel.detach();
972
+ }
973
+ } catch (e) {
974
+ /* ignore */
975
+ }
976
+
977
+ if (this._ably) {
978
+ try {
979
+ logger.log('[ably] Closing Ably connection');
980
+ this._ably.close();
981
+ } catch (e) {
982
+ /* ignore */
983
+ }
984
+ this._ably = null;
985
+ }
986
+
987
+ this._sessionChannel = null;
988
+ this._channelName = null;
989
+ this.apiSocketConnected = false;
990
+ this.instanceSocketConnected = false;
991
+ this.authenticated = false;
992
+ this.instance = null;
993
+ this._lastConnectParams = null;
994
+ this.ps = {};
995
+ }
996
+
997
+ /**
998
+ * Write the agent config JSON to an EC2 instance via AWS SSM.
999
+ * Runs client-side so the API doesn't need AWS permissions on user infra.
1000
+ */
1001
+ async _provisionAgentConfig(instanceId, agentConfig) {
1002
+ const { execSync } = require('child_process');
1003
+ const { writeFileSync, unlinkSync } = require('fs');
1004
+ const { join } = require('path');
1005
+ const { tmpdir } = require('os');
1006
+
1007
+ const configJson = JSON.stringify(agentConfig);
1008
+ const region = process.env.AWS_REGION || 'us-east-2';
1009
+
1010
+ // Write SSM parameters to a temp file to avoid shell quoting issues
1011
+ // Log key config details for debugging
1012
+ logger.log('Agent config being provisioned:');
1013
+ logger.log(' sandboxId: ' + agentConfig.sandboxId);
1014
+ logger.log(' apiRoot: ' + agentConfig.apiRoot);
1015
+ logger.log(' channel: ' + (agentConfig.ably?.channel || 'N/A'));
1016
+ logger.log(' token length: ' + (agentConfig.ably?.token ? JSON.stringify(agentConfig.ably.token).length : 0));
1017
+
1018
+ const paramsJson = JSON.stringify({
1019
+ commands: [
1020
+ // Debug: show existing state
1021
+ "Write-Host '=== Checking existing state ==='",
1022
+ "$task = Get-ScheduledTask -TaskName RunTestDriverAgent -ErrorAction SilentlyContinue",
1023
+ "if ($task) { Write-Host \"Task exists, state: $($task.State)\" } else { Write-Host 'Task does NOT exist!' }",
1024
+ "if (Test-Path 'C:\\Windows\\Temp\\testdriver-agent.json') { Write-Host 'Old config:'; Get-Content 'C:\\Windows\\Temp\\testdriver-agent.json' | Write-Host } else { Write-Host 'Config file does NOT exist yet' }",
1025
+ // Stop any running runner
1026
+ "Write-Host '=== Stopping runner ==='",
1027
+ "Stop-Process -Name node -Force -ErrorAction SilentlyContinue",
1028
+ "Stop-ScheduledTask -TaskName RunTestDriverAgent -ErrorAction SilentlyContinue",
1029
+ // Write config
1030
+ "Write-Host '=== Writing config ==='",
1031
+ "$config = '" + configJson.replace(/'/g, "''") + "'",
1032
+ "[System.IO.File]::WriteAllText('C:\\Windows\\Temp\\testdriver-agent.json', $config)",
1033
+ "Write-Host 'Config written for sandbox " + agentConfig.sandboxId + "'",
1034
+ // Show what was written (redact token)
1035
+ "Write-Host '=== New config (token redacted) ==='",
1036
+ "$cfg = Get-Content 'C:\\Windows\\Temp\\testdriver-agent.json' | ConvertFrom-Json",
1037
+ "Write-Host \"sandboxId: $($cfg.sandboxId)\"",
1038
+ "Write-Host \"apiRoot: $($cfg.apiRoot)\"",
1039
+ "Write-Host \"channel: $($cfg.ably.channel)\"",
1040
+ "Write-Host \"token type: $($cfg.ably.token.GetType().Name)\"",
1041
+ // Start the runner
1042
+ "Write-Host '=== Starting runner ==='",
1043
+ "Start-Sleep -Seconds 1",
1044
+ "Start-ScheduledTask -TaskName RunTestDriverAgent -ErrorAction Stop",
1045
+ "$task = Get-ScheduledTask -TaskName RunTestDriverAgent",
1046
+ "Write-Host \"Task state after start: $($task.State)\"",
1047
+ // Check if node process started
1048
+ "Start-Sleep -Seconds 3",
1049
+ "Write-Host '=== Checking runner process ==='",
1050
+ "$procs = Get-Process -Name node -ErrorAction SilentlyContinue",
1051
+ "if ($procs) { Write-Host \"Node processes: $($procs.Count)\"; $procs | ForEach-Object { Write-Host \" PID: $($_.Id), StartTime: $($_.StartTime)\" } } else { Write-Host 'No node process found!' }",
1052
+ // Check runner logs
1053
+ "Write-Host '=== Runner log (last 30 lines) ==='",
1054
+ "if (Test-Path 'C:\\testdriver\\logs\\sandbox-agent.log') { Get-Content 'C:\\testdriver\\logs\\sandbox-agent.log' -Tail 30 | Write-Host } else { Write-Host 'No log file found' }",
1055
+ "Write-Host '=== Done ==='",
1056
+ ],
1057
+ });
1058
+ const tmpFile = join(tmpdir(), 'td-provision-' + Date.now() + '.json');
1059
+ writeFileSync(tmpFile, paramsJson);
1060
+
1061
+ try {
1062
+ const output = execSync(
1063
+ 'aws ssm send-command --region "' + region + '" --instance-ids "' + instanceId + '" ' +
1064
+ '--document-name "AWS-RunPowerShellScript" ' +
1065
+ '--parameters file://' + tmpFile + ' --output json',
1066
+ { encoding: 'utf-8', timeout: 30000 }
1067
+ );
1068
+ const cmdId = JSON.parse(output).Command.CommandId;
1069
+ logger.log('SSM command sent: ' + cmdId);
1070
+
1071
+ // Wait for the command to complete
1072
+ execSync(
1073
+ 'aws ssm wait command-executed --region "' + region + '" ' +
1074
+ '--command-id "' + cmdId + '" --instance-id "' + instanceId + '"',
1075
+ { encoding: 'utf-8', timeout: 60000 }
1076
+ );
1077
+
1078
+ // Get the command output for debugging
1079
+ try {
1080
+ const invocationOutput = execSync(
1081
+ 'aws ssm get-command-invocation --region "' + region + '" ' +
1082
+ '--command-id "' + cmdId + '" --instance-id "' + instanceId + '" --output json',
1083
+ { encoding: 'utf-8', timeout: 30000 }
1084
+ );
1085
+ const invocation = JSON.parse(invocationOutput);
1086
+ if (invocation.StandardOutputContent) {
1087
+ logger.log('SSM output:\n' + invocation.StandardOutputContent);
1088
+ }
1089
+ if (invocation.StandardErrorContent) {
1090
+ logger.warn('SSM errors:\n' + invocation.StandardErrorContent);
1091
+ }
1092
+ } catch (e) {
1093
+ logger.warn('Could not retrieve SSM command output: ' + e.message);
1094
+ }
1095
+ } finally {
1096
+ try { unlinkSync(tmpFile); } catch (e) { /* ignore */ }
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ return new Sandbox();
1102
+ };
1103
+
1104
+ module.exports = { createSandbox };