@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
package/agent/index.js ADDED
@@ -0,0 +1,2450 @@
1
+ // disable depreciation warnings
2
+ process.removeAllListeners("warning");
3
+
4
+ // package.json is included to get the version number
5
+ const packageJson = require("../package.json");
6
+
7
+ // nodejs modules
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+
11
+ // third party modules
12
+ const path = require("path");
13
+ const yaml = require("js-yaml");
14
+ const sanitizeFilename = require("sanitize-filename");
15
+ const { EventEmitter2 } = require("eventemitter2");
16
+ const diff = require("diff");
17
+
18
+ // global utilities
19
+ const generator = require("./lib/generator.js");
20
+ const theme = require("./lib/theme.js");
21
+ const SourceMapper = require("./lib/source-mapper.js");
22
+
23
+ // agent modules
24
+ const { createParser } = require("./lib/parser.js");
25
+ const { createSystem } = require("./lib/system.js");
26
+ const { createCommander } = require("./lib/commander.js");
27
+ const { createCommands } = require("./lib/commands.js");
28
+ const { createSandbox } = require("./lib/sandbox.js");
29
+ const { createCommandDefinitions } = require("./interface.js");
30
+ const { createSDK } = require("./lib/sdk.js");
31
+ const { createConfig } = require("./lib/config.js");
32
+ const { createAnalytics } = require("./lib/analytics.js");
33
+ const { createSession } = require("./lib/session.js");
34
+ const { createOutputs } = require("./lib/outputs.js");
35
+
36
+ const isValidVersion = require("./lib/valid-version.js");
37
+ const { events, createEmitter } = require("./events.js");
38
+ const logger = require("./lib/logger.js");
39
+
40
+ class TestDriverAgent extends EventEmitter2 {
41
+ constructor(environment = {}, cliArgs = {}) {
42
+ super({
43
+ wildcard: true,
44
+ delimiter: ":",
45
+ newListener: false,
46
+ removeListener: false,
47
+ maxListeners: 20,
48
+ verboseMemoryLeak: false,
49
+ ignoreErrors: false,
50
+ }); // Create the agent's own emitter for internal events
51
+ this.emitter = createEmitter();
52
+
53
+ // Create config instance for this agent using provided environment
54
+ this.config = createConfig(environment);
55
+
56
+ // Store CLI arguments passed to this agent
57
+ this.cliArgs = cliArgs;
58
+
59
+ // Derive properties from cliArgs
60
+ const flags = cliArgs.options || {};
61
+ const firstArg = cliArgs.args && cliArgs.args[0];
62
+
63
+ // All commands (run, edit, generate) use the same pattern:
64
+ // first argument is the main file to work with
65
+ this.thisFile = firstArg || this.config.TD_DEFAULT_TEST_FILE;
66
+
67
+ this.resultFile = flags.resultFile || null;
68
+ this.newSandbox = flags.newSandbox || false;
69
+ this.healMode = flags.healMode || flags.heal || false;
70
+ this.sandboxId = flags["sandbox-id"] || null;
71
+ this.sandboxAmi = flags["sandbox-ami"] || null;
72
+ this.sandboxInstance = flags["sandbox-instance"] || null;
73
+ this.sandboxOs = flags.os || "linux";
74
+ this.ip = flags.ip || null;
75
+ this.workingDir = flags.workingDir || process.cwd();
76
+
77
+ // Resolve thisFile to absolute path with proper extension
78
+ if (this.thisFile) {
79
+ if (this.thisFile === ".") {
80
+ this.thisFile = path.join(this.workingDir, "testdriver.yaml");
81
+ } else {
82
+ this.thisFile = path.join(this.workingDir, this.thisFile);
83
+ if (
84
+ !this.thisFile.endsWith(".yaml") &&
85
+ !this.thisFile.endsWith(".yml")
86
+ ) {
87
+ this.thisFile += ".yaml";
88
+ }
89
+ }
90
+ }
91
+
92
+ // Create parser instance with this agent's emitter
93
+ this.parser = createParser(this.emitter);
94
+
95
+ // Create session instance for this agent
96
+ this.session = createSession();
97
+
98
+ // Create outputs instance for this agent
99
+ this.outputs = createOutputs();
100
+
101
+ // Create SDK instance with this agent's emitter, config, session, and abort signal
102
+ this.sdk = createSDK(this.emitter, this.config, this.session);
103
+
104
+ // Create analytics instance with this agent's emitter, config, and session
105
+ this.analytics = createAnalytics(this.emitter, this.config, this.session);
106
+
107
+ // Create sandbox instance with this agent's emitter, analytics, and session
108
+ this.sandbox = createSandbox(this.emitter, this.analytics, this.session);
109
+
110
+ // Attach Sentry log listeners to capture CLI logs as breadcrumbs
111
+ const sentry = require("../lib/sentry");
112
+ sentry.attachLogListeners(this.emitter);
113
+
114
+ // Set the OS for the sandbox to use
115
+ this.sandbox.os = this.sandboxOs;
116
+
117
+ // Create system instance with emitter, sandbox and config
118
+ this.system = createSystem(this.emitter, this.sandbox, this.config);
119
+
120
+ // Create commands instance with this agent's emitter and system
121
+ const commandsResult = createCommands(
122
+ this.emitter,
123
+ this.system,
124
+ this.sandbox,
125
+ this.config,
126
+ this.session,
127
+ () => this.sourceMapper.currentFilePath || this.thisFile,
128
+ this.cliArgs.options.redrawThreshold,
129
+ null, // getDashcamElapsedTime - will be set by SDK when dashcam is available
130
+ () => this.softAssertMode, // getter for soft assert mode (used by act())
131
+ );
132
+ this.commands = commandsResult.commands;
133
+ this.redraw = commandsResult.redraw;
134
+
135
+ // Create commander instance with this agent's emitter and commands
136
+ this.commander = createCommander(
137
+ this.emitter,
138
+ this.commands,
139
+ this.analytics,
140
+ this.config,
141
+ this.outputs,
142
+ this.session,
143
+ );
144
+
145
+ // these are "in-memory" globals
146
+ // they represent the current state of the agent
147
+ this.lastPrompt = ""; // the last prompt to be input
148
+ this.executionHistory = []; // a history of commands run in the current session
149
+ this.errorCounts = {}; // counts of different errors encountered in this session
150
+ this.errorLimit = 3; // the max number of times an error can be encountered before exiting
151
+ this.checkCount = 0; // the number of times the AI has checked the task
152
+ this.checkLimit = 7; // the max number of times the AI can check the task before exiting
153
+ this.lastScreenshot = null; // the last screenshot taken by the agent
154
+ this.readlineInterface = null; // the readline interface for interactive mode
155
+ this.tasks = []; // list of prompts that the user has given us
156
+ this.hasRunPostrun = false; // whether the postrun lifecycle has been run. prevents infinite loops
157
+
158
+ this.lastCommand = new Date().getTime();
159
+ this.csv = [["command,time"]];
160
+
161
+ // Source mapping for YAML files
162
+ this.sourceMapper = new SourceMapper();
163
+
164
+ // temporary file for command history
165
+ this.commandHistoryFile = path.join(os.homedir(), ".testdriver_history");
166
+
167
+ // Flag to indicate if the agent should stop execution
168
+ this.stopped = false;
169
+
170
+ // Flag to suppress assertion throws (used by act() to make check-phase assertions non-fatal)
171
+ this.softAssertMode = false;
172
+
173
+ this.emitter.emit(events.log.log, JSON.stringify(environment));
174
+ this.emitter.emit(events.log.log, JSON.stringify(cliArgs));
175
+ }
176
+
177
+ // Stop method to immediately halt execution
178
+ stop() {
179
+ this.stopped = true;
180
+ this.emitter.emit(
181
+ events.log.narration,
182
+ theme.dim("stopping execution..."),
183
+ true,
184
+ );
185
+ }
186
+
187
+ // single function to handle all program exits
188
+ // allows us to save the current state, run lifecycle hooks, and track analytics
189
+ async exit(failed = true, shouldSave = false, shouldRunPostrun = false) {
190
+ const { formatter } = require("../sdk-log-formatter.js");
191
+ this.emitter.emit(
192
+ events.log.narration,
193
+ formatter.getPrefix("disconnect") +
194
+ " " +
195
+ theme.yellow.bold("Exiting") +
196
+ theme.dim("..."),
197
+ true,
198
+ );
199
+
200
+ // Clean up redraw interval
201
+ if (this.redraw && this.redraw.cleanup) {
202
+ this.redraw.cleanup();
203
+ }
204
+
205
+ // Close sandbox connection to release the connection slot
206
+ if (this.sandbox) {
207
+ try {
208
+ this.sandbox.close();
209
+ } catch (err) {
210
+ // Ignore sandbox close errors during exit
211
+ }
212
+ }
213
+
214
+ shouldRunPostrun =
215
+ !this.hasRunPostrun &&
216
+ (shouldRunPostrun || this.cliArgs?.command == "run");
217
+
218
+ if (shouldSave) {
219
+ await this.save();
220
+ }
221
+
222
+ this.analytics.track("exit", { failed });
223
+
224
+ if (shouldRunPostrun) {
225
+ this.hasRunPostrun = true;
226
+ await this.runLifecycle("postrun");
227
+ }
228
+
229
+ // Emit exit event with exit code and close readline interface
230
+ this.readlineInterface?.close();
231
+ this.emitter.emit(events.exit, failed ? 1 : 0);
232
+
233
+ // we purposly never resolve this promise so the process will hang
234
+ return new Promise(() => {
235
+ // The process exit should be handled by the base/entry point listening to the exit event
236
+ });
237
+ }
238
+
239
+ // fatal errors always exit the program
240
+ // this ensure we log the error, summarize it, and exit cleanly
241
+ async dieOnFatal(error, skipPostrun = false) {
242
+ // Show error with source context if available
243
+ const errorContext = this.sourceMapper.getErrorWithSourceContext(error);
244
+ if (errorContext) {
245
+ this.emitter.emit(events.error.fatal, errorContext);
246
+ } else {
247
+ this.emitter.emit(events.error.fatal, error);
248
+ }
249
+
250
+ if (skipPostrun) {
251
+ return await this.exit(true);
252
+ } else {
253
+ try {
254
+ await this.summarize(error.message);
255
+ } catch (summarizeError) {
256
+ // If summarization fails, log it but don't let it prevent postrun from running
257
+ this.emitter.emit(
258
+ events.log.warn,
259
+ theme.yellow(`Failed to summarize: ${summarizeError.message}`),
260
+ );
261
+ }
262
+ // Always run postrun lifecycle script, even for fatal errors
263
+ return await this.exit(true, false, true);
264
+ }
265
+ }
266
+
267
+ // creates a new "thread" in which the AI is given an error
268
+ // and responds. notice `actOnMarkdown` which will continue
269
+ // the thread until there are no more codeblocks to execute
270
+ async haveAIResolveError(
271
+ error,
272
+ markdown,
273
+ depth = 0,
274
+ undo = true,
275
+ shouldSave,
276
+ ) {
277
+ // healMode must be required to attempt to recover from errors
278
+ // otherwise we go directly to fatal
279
+ if (!this.healMode) {
280
+ this.emitter.emit(
281
+ events.error.general,
282
+ theme.red("Error detected, but recovery mode is not enabled."),
283
+ );
284
+ this.emitter.emit(
285
+ events.log.log,
286
+ "To attempt automatic recovery, re-run with the --heal flag.",
287
+ );
288
+ return await this.dieOnFatal(error);
289
+ }
290
+
291
+ if (error.fatal) {
292
+ return await this.dieOnFatal(error);
293
+ }
294
+
295
+ // Get error message
296
+ let eMessage = error.message ? error.message : error;
297
+
298
+ // Truncate error message if too long to prevent 400 errors from API
299
+ // Keep first 5000 characters as a reasonable limit for API payloads
300
+ const MAX_ERROR_LENGTH = 5000;
301
+ if (typeof eMessage === "string" && eMessage.length > MAX_ERROR_LENGTH) {
302
+ eMessage =
303
+ eMessage.substring(0, MAX_ERROR_LENGTH) +
304
+ "\n\n[Error message truncated - message was too long]";
305
+ }
306
+
307
+ // we sanitize the error message to use it as a key in the errorCounts object
308
+ let safeKey = JSON.stringify(error.message ? error.message : error);
309
+ this.errorCounts[safeKey] = this.errorCounts[safeKey]
310
+ ? this.errorCounts[safeKey] + 1
311
+ : 1;
312
+
313
+ this.emitter.emit(
314
+ events.log.warn,
315
+ theme.red("Error detected. Attempting to recover (via --heal)..."),
316
+ );
317
+
318
+ // Show error with source context if available
319
+ const errorContext = this.sourceMapper.getErrorWithSourceContext(error);
320
+ if (errorContext) {
321
+ this.emitter.emit(events.log.warn, errorContext);
322
+ } else {
323
+ this.emitter.emit(events.log.markdown.static, eMessage);
324
+ }
325
+
326
+ this.emitter.emit(events.log.debug, error);
327
+ this.emitter.emit(events.log.debug, error.stack);
328
+
329
+ // if we get the same error 3 times in `run` mode, we exit
330
+ if (this.errorCounts[safeKey] > this.errorLimit - 1) {
331
+ this.emitter.emit(
332
+ events.log.log,
333
+ theme.red("Error loop detected. Exiting."),
334
+ );
335
+ this.emitter.emit(events.log.log, this.getErrorWithPosition(error));
336
+ await this.summarize(eMessage);
337
+ return await this.exit(true);
338
+ }
339
+
340
+ // remove this step from the execution history
341
+ if (undo) {
342
+ await this.popFromHistory();
343
+ }
344
+
345
+ // ask the AI what to do
346
+ let image;
347
+ if (error.attachScreenshot) {
348
+ image = await this.system.captureScreenBase64();
349
+ } else {
350
+ image = null;
351
+ }
352
+
353
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
354
+
355
+ const streamId = `error-${Date.now()}`;
356
+ this.emitter.emit(events.log.markdown.start, streamId);
357
+
358
+ // Truncate markdown if too long to prevent 400 errors
359
+ const MAX_MARKDOWN_LENGTH = 10000;
360
+ let truncatedMarkdown = markdown;
361
+ if (typeof markdown === "string" && markdown.length > MAX_MARKDOWN_LENGTH) {
362
+ truncatedMarkdown =
363
+ markdown.substring(0, MAX_MARKDOWN_LENGTH) +
364
+ "\n\n[Markdown truncated - content was too long]";
365
+ }
366
+
367
+ let response;
368
+ try {
369
+ response = await this.sdk.req(
370
+ "error",
371
+ {
372
+ description: eMessage,
373
+ markdown: truncatedMarkdown,
374
+ image,
375
+ },
376
+ (chunk) => {
377
+ if (chunk.type === "data" && chunk.data) {
378
+ this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
379
+ }
380
+ },
381
+ );
382
+ } catch (apiError) {
383
+ // If the error API call itself fails, prevent infinite loop
384
+ // by not retrying and instead treating as fatal
385
+ this.emitter.emit(
386
+ events.log.error,
387
+ theme.red(`Failed to get AI error resolution: ${apiError.message}`),
388
+ );
389
+ this.emitter.emit(events.log.log, "Original error: " + eMessage);
390
+ return await this.dieOnFatal(error);
391
+ }
392
+
393
+ this.emitter.emit(events.log.markdown.end, streamId);
394
+
395
+ // if the response worked, we try to execute the codeblocks in the response,
396
+ // which begins the recursive process of executing codeblocks
397
+ if (response?.data) {
398
+ return await this.actOnMarkdown(
399
+ response.data,
400
+ depth,
401
+ true,
402
+ false,
403
+ shouldSave,
404
+ );
405
+ }
406
+ }
407
+
408
+ // this is run after all possible codeblocks have been executed, but only at depth 0, which is the top level
409
+ // this checks that the task is "really done" using a screenshot of the desktop state
410
+ // it's likely that the task will not be complete and the AI will respond with more codeblocks to execute
411
+ async check() {
412
+ // Check if execution has been stopped
413
+ if (this.stopped) {
414
+ this.emitter.emit(
415
+ events.log.narration,
416
+ theme.dim("execution stopped"),
417
+ true,
418
+ );
419
+ return;
420
+ }
421
+
422
+ this.checkCount++;
423
+
424
+ if (this.checkCount >= this.checkLimit) {
425
+ this.emitter.emit(
426
+ events.log.narration,
427
+ theme.red("Exploratory loop detected. Exiting."),
428
+ );
429
+ await this.summarize("Check loop detected.");
430
+ return await this.exit(true);
431
+ }
432
+
433
+ this.emitter.emit(events.log.narration, theme.dim("checking..."));
434
+
435
+ // check asks the ai if the task is complete
436
+ // Parallelize system calls for better performance
437
+ const [thisScreenshot, mousePosition, activeWindow] = await Promise.all([
438
+ this.system.captureScreenBase64(1, false, true),
439
+ this.system.getMousePosition(),
440
+ this.system.activeWin(),
441
+ ]);
442
+ let images = [this.lastScreenshot, thisScreenshot];
443
+
444
+ let response = await this.sdk.req("check", {
445
+ tasks: this.tasks,
446
+ images,
447
+ mousePosition,
448
+ activeWindow,
449
+ });
450
+
451
+ // Use log.log (not markdown.static) so output goes through console spy to sandbox
452
+ this.emitter.emit(events.log.log, response.data);
453
+
454
+ this.lastScreenshot = thisScreenshot;
455
+
456
+ return response.data;
457
+ }
458
+
459
+ // command is transformed from a single yml entry generated by the AI into a JSON object
460
+ // it is mapped via `commander` to the `commands` module so the yaml
461
+ // parameters can be mapped to actual functions
462
+ async runCommand(command, depth, shouldSave, pushToHistory) {
463
+ let yml = await yaml.dump(command);
464
+ const commandName = command.command;
465
+ const startTime = Date.now();
466
+
467
+ // Get current source position
468
+ const sourcePosition = this.sourceMapper.getCurrentSourcePosition();
469
+
470
+ // Emit command start event with source mapping
471
+ this.emitter.emit(events.command.start, {
472
+ command: commandName,
473
+ depth,
474
+ data: command,
475
+ timestamp: startTime,
476
+ sourcePosition: sourcePosition,
477
+ });
478
+
479
+ // Log current execution position for debugging
480
+ if (this.sourceMapper.currentFileSourceMap) {
481
+ this.emitter.emit(events.log.log, "");
482
+ this.emitter.emit(
483
+ events.log.log,
484
+ theme.dim(`${this.sourceMapper.getCurrentPositionDescription()}`),
485
+ );
486
+ }
487
+
488
+ try {
489
+ let response;
490
+
491
+ // "run" and "if" commands are special meta commands
492
+ // that change the flow of execution
493
+ if (command.command == "run") {
494
+ response = await this.embed(command.file, depth, pushToHistory);
495
+ } else if (command.command == "if") {
496
+ response = await this.iffy(
497
+ command.condition,
498
+ command.then,
499
+ command.else,
500
+ depth,
501
+ );
502
+ } else {
503
+ response = await this.commander.run(command, depth);
504
+ }
505
+
506
+ const endTime = Date.now();
507
+ const duration = endTime - startTime;
508
+
509
+ // Emit command success event with source mapping
510
+ this.emitter.emit(events.command.success, {
511
+ command: commandName,
512
+ depth,
513
+ data: command,
514
+ duration,
515
+ response,
516
+ timestamp: endTime,
517
+ sourcePosition: sourcePosition,
518
+ });
519
+
520
+ // if the result of a command contains more commands, we perform the process again
521
+ if (response && typeof response === "string") {
522
+ return await this.actOnMarkdown(response, depth, false, false, false);
523
+ }
524
+ } catch (error) {
525
+ const endTime = Date.now();
526
+ const duration = endTime - startTime;
527
+
528
+ // Emit command error event with source mapping
529
+ this.emitter.emit(events.command.error, {
530
+ command: commandName,
531
+ depth,
532
+ data: command,
533
+ error: error.message,
534
+ duration,
535
+ timestamp: endTime,
536
+ sourcePosition: sourcePosition,
537
+ });
538
+
539
+ return await this.haveAIResolveError(
540
+ error,
541
+ yaml.dump({ commands: [yml] }),
542
+ depth,
543
+ true,
544
+ shouldSave,
545
+ );
546
+ }
547
+ }
548
+
549
+ async executeCommands(
550
+ commands,
551
+ depth,
552
+ pushToHistory = false,
553
+ dry = false,
554
+ shouldSave = false,
555
+ ) {
556
+ // Check if execution has been stopped
557
+ if (this.stopped) {
558
+ this.emitter.emit(
559
+ events.log.narration,
560
+ theme.dim("execution stopped"),
561
+ true,
562
+ );
563
+ return;
564
+ }
565
+
566
+ if (commands?.length) {
567
+ for (const command of commands) {
568
+ // Check if execution has been stopped before each command
569
+ if (this.stopped) {
570
+ this.emitter.emit(
571
+ events.log.narration,
572
+ theme.dim("execution stopped"),
573
+ true,
574
+ );
575
+ return;
576
+ }
577
+
578
+ // Update current command tracking
579
+ const commandIndex = commands.indexOf(command);
580
+ this.sourceMapper.setCurrentCommand(commandIndex);
581
+
582
+ if (pushToHistory) {
583
+ this.executionHistory[
584
+ this.executionHistory.length - 1
585
+ ]?.commands.push(command);
586
+ }
587
+
588
+ if (shouldSave) {
589
+ await this.save({ silent: true });
590
+ }
591
+
592
+ if (!dry) {
593
+ await this.runCommand(command, depth, shouldSave);
594
+ }
595
+ let timeToComplete = (new Date().getTime() - this.lastCommand) / 1000;
596
+ // this.emitter.emit(events.log.log, timeToComplete, 'seconds')
597
+
598
+ this.csv.push([command.command, timeToComplete]);
599
+ this.lastCommand = new Date().getTime();
600
+ }
601
+ }
602
+ }
603
+
604
+ // codeblocks are ```yml ... ``` blocks found in ai responses
605
+ // this is similar to "function calling" in other ai frameworks
606
+ // here we parse the codeblocks and execute the commands within them
607
+ async executeCodeBlocks(
608
+ codeblocks,
609
+ depth,
610
+ pushToHistory = false,
611
+ dry = false,
612
+ shouldSave = false,
613
+ ) {
614
+ // Check if execution has been stopped
615
+ if (this.stopped) {
616
+ this.emitter.emit(
617
+ events.log.narration,
618
+ theme.dim("execution stopped"),
619
+ true,
620
+ );
621
+ return;
622
+ }
623
+
624
+ depth = depth + 1;
625
+
626
+ for (const codeblock of codeblocks) {
627
+ // Check if execution has been stopped before each codeblock
628
+ if (this.stopped) {
629
+ this.emitter.emit(
630
+ events.log.narration,
631
+ theme.dim("execution stopped"),
632
+ true,
633
+ );
634
+ return;
635
+ }
636
+
637
+ let commands;
638
+
639
+ try {
640
+ commands = await this.parser.getCommands(codeblock);
641
+ } catch (e) {
642
+ // For parser errors
643
+ return await this.haveAIResolveError(
644
+ e,
645
+ yaml.dump(this.parser.getYAMLFromCodeBlock(codeblock)),
646
+ depth,
647
+ shouldSave,
648
+ );
649
+ }
650
+
651
+ await this.executeCommands(
652
+ commands,
653
+ depth,
654
+ pushToHistory,
655
+ dry,
656
+ shouldSave,
657
+ );
658
+ }
659
+ }
660
+
661
+ // this is the main function that interacts with the ai, runs commands, and checks the results
662
+ // notice that depth is 0 here. when this function resolves, the task is considered complete
663
+ // notice the call to `check()` which validates the prompt is complete
664
+ async aiExecute(
665
+ message,
666
+ validateAndLoop = false,
667
+ dry = false,
668
+ shouldSave = false,
669
+ isLoopContinuation = false,
670
+ ) {
671
+ // Check if execution has been stopped
672
+ if (this.stopped) {
673
+ this.emitter.emit(
674
+ events.log.narration,
675
+ theme.dim("execution stopped"),
676
+ true,
677
+ );
678
+ return;
679
+ }
680
+
681
+ // Only create new execution history entry if this is not a loop continuation
682
+ if (!isLoopContinuation) {
683
+ this.executionHistory.push({ prompt: this.lastPrompt, commands: [] });
684
+ }
685
+
686
+ if (shouldSave) {
687
+ await this.save({ silent: true });
688
+ }
689
+
690
+ this.emitter.emit(events.log.debug, "kicking off exploratory loop");
691
+
692
+ // kick everything off
693
+ await this.actOnMarkdown(message, 0, true, dry, shouldSave);
694
+
695
+ // this calls the "check" function to validate the task is complete"
696
+ // the ai determines if it's complete or not
697
+ // if it is incomplete, the ai will likely return more codeblocks to execute
698
+ if (validateAndLoop) {
699
+ this.emitter.emit(
700
+ events.log.debug,
701
+ "exploratory loop resolved, check your work",
702
+ );
703
+
704
+ let response = await this.check();
705
+
706
+ let checkCodeblocks = [];
707
+ try {
708
+ checkCodeblocks = await this.parser.findCodeBlocks(response);
709
+ } catch (error) {
710
+ return await this.haveAIResolveError(error, response, 0, true, true);
711
+ }
712
+
713
+ this.emitter.emit(
714
+ events.log.debug,
715
+ `found ${checkCodeblocks.length} codeblocks`,
716
+ );
717
+
718
+ if (checkCodeblocks.length > 0) {
719
+ this.emitter.emit(
720
+ events.log.debug,
721
+ "check thinks more needs to be done",
722
+ );
723
+
724
+ return await this.aiExecute(
725
+ response,
726
+ validateAndLoop,
727
+ dry,
728
+ shouldSave,
729
+ true,
730
+ );
731
+ } else {
732
+ this.emitter.emit(events.log.debug, "seems complete, returning");
733
+
734
+ this.emitter.emit(events.log.log, theme.green("success!"));
735
+
736
+ return response;
737
+ }
738
+ }
739
+ }
740
+
741
+ // reads a yaml file and interprets the variables found within it
742
+ async loadYML(file) {
743
+ const startTime = Date.now();
744
+
745
+ // Emit file load start event
746
+ this.emitter.emit(events.file.start, {
747
+ operation: "load",
748
+ filePath: file,
749
+ timestamp: startTime,
750
+ });
751
+
752
+ let yml;
753
+
754
+ //wrap this in try/catch so if the file doesn't exist output an error message to the user
755
+ try {
756
+ yml = fs.readFileSync(file, "utf-8");
757
+
758
+ // Emit file load success event
759
+ this.emitter.emit(events.file.load, {
760
+ filePath: file,
761
+ size: yml.length,
762
+ timestamp: Date.now(),
763
+ });
764
+ } catch (e) {
765
+ // Emit file error event
766
+ this.emitter.emit(events.file.error, {
767
+ operation: "load",
768
+ filePath: file,
769
+ error: e.message,
770
+ timestamp: Date.now(),
771
+ });
772
+
773
+ this.emitter.emit(events.error.fatal, `File not found: ${file}`);
774
+
775
+ await this.summarize("File not found");
776
+ await this.exit(true);
777
+ }
778
+ if (!yml) {
779
+ return {};
780
+ }
781
+
782
+ yml = await this.parser.validateYAML(yml);
783
+
784
+ // Inject environment variables into any ${VAR} strings
785
+ yml = this.parser.interpolate(yml, {
786
+ TD_THIS_FILE: file,
787
+ ...this.config._environment,
788
+ });
789
+
790
+ // Show Unreplaced Variables
791
+ let unreplacedVars = this.parser.collectUnreplacedVariables(yml);
792
+
793
+ // Remove all variables that start with OUTPUT- these are special
794
+ unreplacedVars = unreplacedVars.filter((v) => {
795
+ return !v.startsWith("OUTPUT.");
796
+ });
797
+
798
+ if (unreplacedVars.length > 0) {
799
+ this.emitter.emit(
800
+ events.log.warn,
801
+ theme.yellow(
802
+ `Unreplaced variables in YAML: ${unreplacedVars.join(", ")}`,
803
+ ),
804
+ );
805
+ }
806
+
807
+ let ymlObj = null;
808
+ let sourceMap = null;
809
+ try {
810
+ // Parse YAML with source mapping
811
+ const parseResult = this.sourceMapper.parseYamlWithSourceMap(yml, file);
812
+ ymlObj = parseResult.yamlObj;
813
+ sourceMap = parseResult.sourceMap;
814
+
815
+ const endTime = Date.now();
816
+
817
+ // Emit file load completion event with source mapping
818
+ this.emitter.emit(events.file.stop, {
819
+ operation: "load",
820
+ filePath: file,
821
+ duration: endTime - startTime,
822
+ success: true,
823
+ sourceMap: sourceMap,
824
+ timestamp: endTime,
825
+ });
826
+ } catch (e) {
827
+ const endTime = Date.now();
828
+
829
+ // Emit file error event
830
+ this.emitter.emit(events.file.error, {
831
+ operation: "parse",
832
+ filePath: file,
833
+ error: e.message,
834
+ duration: endTime - startTime,
835
+ timestamp: endTime,
836
+ });
837
+
838
+ this.emitter.emit(events.error.fatal, e.message);
839
+
840
+ await this.summarize("Invalid YAML");
841
+ await this.exit(true);
842
+ }
843
+
844
+ return ymlObj;
845
+ }
846
+
847
+ // this is a rarely used command that likely doesn't need to exist
848
+ // it's used to call /assert in interactive mode
849
+ // @todo remove assert() command from agent.js
850
+ async assert(expect) {
851
+ this.analytics.track("assert");
852
+
853
+ let task = expect;
854
+ if (!task) {
855
+ // set task to last value of tasks
856
+ let task = this.tasks[this.tasks.length - 1];
857
+
858
+ // throw error if no task
859
+ if (!task) {
860
+ throw new Error("No task to assert");
861
+ }
862
+ }
863
+
864
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
865
+
866
+ let response = `\`\`\`yaml
867
+ commands:
868
+ - command: assert
869
+ expect: ${expect}
870
+ \`\`\``;
871
+
872
+ await this.aiExecute(response);
873
+
874
+ await this.save({ silent: true });
875
+ }
876
+
877
+ // this function responds to the result of `promptUser()` which is the user input
878
+ // it kicks off the exploratory loop, which is the main function that interacts with the AI
879
+ async exploratoryLoop(
880
+ currentTask,
881
+ dry = false,
882
+ validateAndLoop = false,
883
+ shouldSave = true,
884
+ ) {
885
+ // Check if execution has been stopped
886
+ if (this.stopped) {
887
+ this.emitter.emit(
888
+ events.log.narration,
889
+ theme.dim("execution stopped"),
890
+ true,
891
+ );
892
+ return;
893
+ }
894
+
895
+ this.lastPrompt = currentTask;
896
+ this.checkCount = 0;
897
+
898
+ this.emitter.emit(events.log.debug, "exploratoryLoop called");
899
+
900
+ this.tasks.push(currentTask);
901
+
902
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
903
+
904
+ // Parallelize system calls for better performance
905
+ const [screenshot, mousePosition, activeWindow] = await Promise.all([
906
+ this.system.captureScreenBase64(),
907
+ this.system.getMousePosition(),
908
+ this.system.activeWin(),
909
+ ]);
910
+ this.lastScreenshot = screenshot;
911
+
912
+ let message = await this.sdk.req("input", {
913
+ input: currentTask,
914
+ mousePosition,
915
+ activeWindow,
916
+ image: this.lastScreenshot,
917
+ });
918
+
919
+ this.emitter.emit(events.log.log, message.data);
920
+
921
+ if (message && message.data) {
922
+ await this.aiExecute(message.data, validateAndLoop, dry, shouldSave);
923
+ this.emitter.emit(
924
+ events.log.debug,
925
+ "showing prompt from exploratoryLoop response check",
926
+ );
927
+ }
928
+
929
+ return;
930
+ }
931
+
932
+ // generate asks the AI to come up with ideas for test files
933
+ // based on the current state of the system (primarily the current screenshot)
934
+ // it will generate files that contain only "prompts"
935
+ // @todo revit the generate command
936
+ async generate(count = 1, prompt = null) {
937
+ this.emitter.emit(
938
+ events.log.debug,
939
+ `generate called with count: ${count}, prompt: ${prompt}`,
940
+ );
941
+
942
+ await this.runLifecycle("prerun");
943
+
944
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
945
+
946
+ const streamId = `generate-${Date.now()}`;
947
+ this.emitter.emit(events.log.markdown.start, streamId);
948
+
949
+ // Parallelize system calls for better performance
950
+ const [image, mouse, activeWindow] = await Promise.all([
951
+ this.system.captureScreenBase64(),
952
+ this.system.getMousePosition(),
953
+ this.system.activeWin(),
954
+ ]);
955
+
956
+ let message = await this.sdk.req(
957
+ "generate",
958
+ {
959
+ prompt: prompt || "make sure to do a spellcheck",
960
+ image,
961
+ mousePosition: mouse,
962
+ activeWindow: activeWindow,
963
+ count,
964
+ stream: false,
965
+ },
966
+ (chunk) => {
967
+ if (chunk.type === "data") {
968
+ this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
969
+ }
970
+ },
971
+ );
972
+
973
+ this.emitter.emit(events.log.markdown.end, streamId);
974
+
975
+ let testPrompts = await this.parser.findGenerativePrompts(message.data);
976
+
977
+ // for each testPrompt
978
+ for (const testPrompt of testPrompts) {
979
+ // with the contents of the testPrompt
980
+ let fileName =
981
+ sanitizeFilename(testPrompt.name)
982
+ .trim()
983
+ .replace(/ /g, "-")
984
+ .replace(/['"`]/g, "")
985
+ .replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
986
+ .toLowerCase() + ".yaml";
987
+
988
+ let path1 = path.join(
989
+ this.workingDir,
990
+ "testdriver",
991
+ "generate",
992
+ fileName,
993
+ );
994
+ // create generate directory if it doesn't exist
995
+ const generateDir = path.join(this.workingDir, "testdriver", "generate");
996
+ if (!fs.existsSync(generateDir)) {
997
+ fs.mkdirSync(generateDir);
998
+ logger.log("Created generate directory:", generateDir);
999
+ } else {
1000
+ logger.log("Generate directory already exists:", generateDir);
1001
+ }
1002
+
1003
+ let list = testPrompt.steps;
1004
+
1005
+ let contents = yaml.dump({
1006
+ version: packageJson.version,
1007
+ steps: list,
1008
+ });
1009
+
1010
+ this.emitter.emit(events.log.debug, `writing file ${path1} ${contents}`);
1011
+
1012
+ fs.writeFileSync(path1, contents);
1013
+ }
1014
+
1015
+ await this.runLifecycle("postrun");
1016
+
1017
+ this.exit(false);
1018
+ }
1019
+
1020
+ // this is the functinoality for "undo"
1021
+ async popFromHistory(fullStep) {
1022
+ this.emitter.emit(events.log.narration, theme.dim("undoing..."), true);
1023
+
1024
+ if (this.executionHistory.length) {
1025
+ if (fullStep) {
1026
+ this.executionHistory.pop();
1027
+ } else {
1028
+ this.executionHistory[this.executionHistory.length - 1].commands.pop();
1029
+ }
1030
+ if (
1031
+ !this.executionHistory[this.executionHistory.length - 1].commands.length
1032
+ ) {
1033
+ this.executionHistory.pop();
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ async undo() {
1039
+ this.analytics.track("undo");
1040
+
1041
+ this.popFromHistory();
1042
+ await this.save();
1043
+ }
1044
+
1045
+ // this allows the user to input "flattened yaml"
1046
+ // like "command='focus-application' name='Google Chrome'"
1047
+ async manualInput(commandString) {
1048
+ this.analytics.track("manual input");
1049
+
1050
+ let yml = await generator.manualToYml(commandString);
1051
+
1052
+ let message = `\`\`\`yaml
1053
+ ${yml}
1054
+ \`\`\``;
1055
+
1056
+ await this.aiExecute(message, false);
1057
+
1058
+ await this.save({ silent: true });
1059
+ }
1060
+
1061
+ // this function is responsible for starting the recursive process of executing codeblocks
1062
+ async actOnMarkdown(
1063
+ content,
1064
+ depth,
1065
+ pushToHistory = false,
1066
+ dry = false,
1067
+ shouldSave = false,
1068
+ ) {
1069
+ let codeblocks = [];
1070
+ try {
1071
+ codeblocks = await this.parser.findCodeBlocks(content);
1072
+ } catch (error) {
1073
+ pushToHistory = false;
1074
+ return await this.haveAIResolveError(
1075
+ error,
1076
+ content,
1077
+ depth,
1078
+ false,
1079
+ shouldSave,
1080
+ );
1081
+ }
1082
+
1083
+ if (codeblocks.length) {
1084
+ let executions = await this.executeCodeBlocks(
1085
+ codeblocks,
1086
+ depth,
1087
+ pushToHistory,
1088
+ dry,
1089
+ shouldSave,
1090
+ );
1091
+ return executions;
1092
+ } else {
1093
+ return true;
1094
+ }
1095
+ }
1096
+
1097
+ // this function is responsible for summarizing the test script that has already executed
1098
+ // it is what is saved to the `/tmp/testdriver-summary.md` file and output to the action as a summary
1099
+ async summarize(error = null) {
1100
+ this.analytics.track("summarize");
1101
+
1102
+ this.emitter.emit(
1103
+ events.log.narration,
1104
+ theme.dim("reviewing test..."),
1105
+ true,
1106
+ );
1107
+
1108
+ // let text = prompts.summarize(tasks, error);
1109
+ let image = await this.system.captureScreenBase64();
1110
+
1111
+ this.emitter.emit(events.log.narration, theme.dim("summarizing..."), true);
1112
+
1113
+ const streamId = `summarize-${Date.now()}`;
1114
+ this.emitter.emit(events.log.markdown.start, streamId);
1115
+
1116
+ let reply = await this.sdk.req(
1117
+ "summarize",
1118
+ {
1119
+ image,
1120
+ error: error?.toString(),
1121
+ tasks: this.tasks,
1122
+ },
1123
+ (chunk) => {
1124
+ if (chunk.type === "data") {
1125
+ this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
1126
+ }
1127
+ },
1128
+ );
1129
+
1130
+ this.emitter.emit(events.log.markdown.end, streamId);
1131
+
1132
+ // Only write summary to file if --summary option was provided
1133
+ if (this.resultFile) {
1134
+ // Ensure the output directory exists
1135
+ const outputDir = path.dirname(this.resultFile);
1136
+ if (!fs.existsSync(outputDir)) {
1137
+ fs.mkdirSync(outputDir, { recursive: true });
1138
+ }
1139
+
1140
+ fs.writeFileSync(this.resultFile, reply.data);
1141
+ this.emitter.emit(
1142
+ events.log.log,
1143
+ theme.dim(`Summary written to: ${this.resultFile}`),
1144
+ );
1145
+ } else {
1146
+ const tmpFile = path.join(os.tmpdir(), "testdriver-summary.md");
1147
+ fs.writeFileSync(tmpFile, reply?.data);
1148
+ this.emitter.emit(
1149
+ events.log.log,
1150
+ theme.dim(`Summary written to: ${tmpFile}`),
1151
+ );
1152
+ }
1153
+ }
1154
+
1155
+ // this function is responsible for saving the regression test script to a file
1156
+ async save({ filepath = this.thisFile, silent = false } = {}) {
1157
+ const startTime = Date.now();
1158
+
1159
+ // Emit file save start event
1160
+ this.emitter.emit(events.file.start, {
1161
+ operation: "save",
1162
+ filePath: filepath,
1163
+ timestamp: startTime,
1164
+ });
1165
+
1166
+ this.analytics.track("save", { silent });
1167
+
1168
+ if (!this.executionHistory.length) {
1169
+ // Emit file save completion event for empty history
1170
+ this.emitter.emit(events.file.stop, {
1171
+ operation: "save",
1172
+ filePath: filepath,
1173
+ duration: Date.now() - startTime,
1174
+ success: true,
1175
+ reason: "empty_history",
1176
+ timestamp: Date.now(),
1177
+ });
1178
+ return;
1179
+ }
1180
+
1181
+ // Read existing file content for diff comparison
1182
+ let existingContent = "";
1183
+ let fileExists = false;
1184
+ try {
1185
+ if (fs.existsSync(filepath)) {
1186
+ existingContent = fs.readFileSync(filepath, "utf8");
1187
+ fileExists = true;
1188
+ }
1189
+ } catch {
1190
+ // File doesn't exist or can't be read, treat as empty
1191
+ existingContent = "";
1192
+ }
1193
+
1194
+ // write reply to /tmp/testdriver-summary.md
1195
+ let regression = await generator.dumpToYML(
1196
+ this.executionHistory,
1197
+ this.session,
1198
+ );
1199
+
1200
+ // Create diff if file exists and content has changed
1201
+ let diffResult = null;
1202
+
1203
+ if (fileExists && existingContent !== regression) {
1204
+ const patches = diff.structuredPatch(
1205
+ filepath,
1206
+ filepath,
1207
+ existingContent,
1208
+ regression,
1209
+ `${new Date().toISOString()} (before)`,
1210
+ `${new Date().toISOString()} (after)`,
1211
+ );
1212
+
1213
+ // Create source map-like information for VS Code
1214
+ const diffLines = diff.diffLines(existingContent, regression);
1215
+ const sourceMaps = [];
1216
+ let oldLineNumber = 1;
1217
+ let newLineNumber = 1;
1218
+
1219
+ diffLines.forEach((part) => {
1220
+ const lineCount = part.value.split("\n").length - 1;
1221
+ if (part.added) {
1222
+ sourceMaps.push({
1223
+ type: "addition",
1224
+ oldStart: oldLineNumber,
1225
+ oldEnd: oldLineNumber,
1226
+ newStart: newLineNumber,
1227
+ newEnd: newLineNumber + lineCount,
1228
+ content: part.value,
1229
+ lines: lineCount,
1230
+ });
1231
+ newLineNumber += lineCount;
1232
+ } else if (part.removed) {
1233
+ sourceMaps.push({
1234
+ type: "deletion",
1235
+ oldStart: oldLineNumber,
1236
+ oldEnd: oldLineNumber + lineCount,
1237
+ newStart: newLineNumber,
1238
+ newEnd: newLineNumber,
1239
+ content: part.value,
1240
+ lines: lineCount,
1241
+ });
1242
+ oldLineNumber += lineCount;
1243
+ } else {
1244
+ // unchanged
1245
+ sourceMaps.push({
1246
+ type: "unchanged",
1247
+ oldStart: oldLineNumber,
1248
+ oldEnd: oldLineNumber + lineCount,
1249
+ newStart: newLineNumber,
1250
+ newEnd: newLineNumber + lineCount,
1251
+ content: part.value,
1252
+ lines: lineCount,
1253
+ });
1254
+ oldLineNumber += lineCount;
1255
+ newLineNumber += lineCount;
1256
+ }
1257
+ });
1258
+
1259
+ diffResult = {
1260
+ patches,
1261
+ sourceMaps,
1262
+ summary: {
1263
+ additions: diffLines.filter((part) => part.added).length,
1264
+ deletions: diffLines.filter((part) => part.removed).length,
1265
+ modifications: diffLines.filter(
1266
+ (part) => !part.added && !part.removed,
1267
+ ).length,
1268
+ },
1269
+ };
1270
+ }
1271
+
1272
+ try {
1273
+ fs.writeFileSync(filepath, regression);
1274
+
1275
+ const endTime = Date.now();
1276
+
1277
+ // Emit file save success event
1278
+ this.emitter.emit(events.file.save, {
1279
+ filePath: filepath,
1280
+ size: regression.length,
1281
+ timestamp: endTime,
1282
+ });
1283
+
1284
+ // Emit diff event if there were changes
1285
+ if (diffResult) {
1286
+ this.emitter.emit(events.file.diff, {
1287
+ filePath: filepath,
1288
+ diff: diffResult,
1289
+ timestamp: endTime,
1290
+ });
1291
+ }
1292
+
1293
+ // Emit file save completion event
1294
+ this.emitter.emit(events.file.stop, {
1295
+ operation: "save",
1296
+ filePath: filepath,
1297
+ duration: endTime - startTime,
1298
+ success: true,
1299
+ timestamp: endTime,
1300
+ });
1301
+ } catch (e) {
1302
+ const endTime = Date.now();
1303
+
1304
+ // Emit file save error event
1305
+ this.emitter.emit(events.file.error, {
1306
+ operation: "save",
1307
+ filePath: filepath,
1308
+ error: e.message,
1309
+ duration: endTime - startTime,
1310
+ timestamp: endTime,
1311
+ });
1312
+
1313
+ this.emitter.emit(events.error.fatal, e.message);
1314
+ }
1315
+
1316
+ if (!silent) {
1317
+ this.emitter.emit(
1318
+ events.log.markdown.static,
1319
+ `Current test script:
1320
+
1321
+ \`\`\`yaml
1322
+ ${regression}
1323
+ \`\`\``,
1324
+ );
1325
+
1326
+ if (!silent) {
1327
+ this.emitter.emit(events.log.log, theme.dim(`saved as ${filepath}`));
1328
+ }
1329
+ }
1330
+
1331
+ return;
1332
+ }
1333
+
1334
+ async runRawYML(yml) {
1335
+ const tmp = require("tmp");
1336
+ let tmpobj = tmp.fileSync();
1337
+
1338
+ let decoded = decodeURIComponent(yml);
1339
+
1340
+ // parse the yaml
1341
+ let ymlObj = null;
1342
+ try {
1343
+ ymlObj = await yaml.load(decoded);
1344
+ } catch (e) {
1345
+ this.emitter.emit(events.error.fatal, e);
1346
+ }
1347
+
1348
+ // add the root key steps: with array of commands:
1349
+ if (ymlObj && !ymlObj.steps) {
1350
+ ymlObj = {
1351
+ version: packageJson.version,
1352
+ steps: [ymlObj],
1353
+ };
1354
+ }
1355
+
1356
+ // write the yaml to a file
1357
+ fs.writeFileSync(tmpobj.name, yaml.dump(ymlObj));
1358
+
1359
+ // and run it with run()
1360
+
1361
+ await this.runLifecycle("prerun");
1362
+ await this.run(tmpobj.name, false, false);
1363
+ await this.runLifecycle("postrun");
1364
+ }
1365
+
1366
+ // this will load a regression test from a file location
1367
+ // it parses the markdown file and executes the codeblocks exactly as if they were
1368
+ // generated by the AI in a single prompt
1369
+ async run(file = this.thisFile, shouldSave = false, shouldExit = true) {
1370
+ const fileStartTime = Date.now();
1371
+
1372
+ // Emit file start event (for individual file execution within a test)
1373
+ this.emitter.emit(events.file.start, {
1374
+ operation: "run",
1375
+ filePath: file,
1376
+ timestamp: fileStartTime,
1377
+ });
1378
+
1379
+ this.emitter.emit(events.log.narration, theme.cyan(`running ${file}...`));
1380
+
1381
+ let ymlObj = await this.loadYML(file);
1382
+
1383
+ // Store source mapping for current file
1384
+ const parseResult = this.sourceMapper.parseYamlWithSourceMap(
1385
+ fs.readFileSync(file, "utf-8"),
1386
+ file,
1387
+ );
1388
+ this.sourceMapper.setCurrentContext(file, parseResult.sourceMap, -1, -1);
1389
+
1390
+ if (ymlObj.version) {
1391
+ let valid = isValidVersion(ymlObj.version);
1392
+ if (!valid) {
1393
+ this.emitter.emit(
1394
+ events.log.warn,
1395
+ theme.yellow(`Version mismatch detected!`),
1396
+ );
1397
+ this.emitter.emit(
1398
+ events.log.warn,
1399
+ theme.yellow(`Running a test created with v${ymlObj.version}.`),
1400
+ );
1401
+ this.emitter.emit(
1402
+ events.log.warn,
1403
+ theme.yellow(
1404
+ `The local testdriverai version is v${packageJson.version}.`,
1405
+ ),
1406
+ );
1407
+ }
1408
+ }
1409
+
1410
+ this.executionHistory = [];
1411
+
1412
+ if (!ymlObj.steps || !ymlObj.steps.length) {
1413
+ this.emitter.emit(
1414
+ events.log.log,
1415
+ theme.red("No steps found in the YAML file"),
1416
+ );
1417
+ await this.exit(true, shouldSave, true);
1418
+ }
1419
+
1420
+ try {
1421
+ for (const step of ymlObj.steps) {
1422
+ const stepIndex = ymlObj.steps.indexOf(step);
1423
+ const stepStartTime = Date.now();
1424
+
1425
+ // Update current step tracking
1426
+ this.sourceMapper.setCurrentStep(stepIndex);
1427
+
1428
+ // Get source position for current step
1429
+ const sourcePosition = this.sourceMapper.getCurrentSourcePosition();
1430
+
1431
+ // Emit step start event with source mapping
1432
+ this.emitter.emit(events.step.start, {
1433
+ stepIndex,
1434
+ prompt: step.prompt,
1435
+ commandCount: step.commands ? step.commands.length : 0,
1436
+ timestamp: stepStartTime,
1437
+ sourcePosition: sourcePosition,
1438
+ });
1439
+
1440
+ this.emitter.emit(events.log.log, ``, null);
1441
+ this.emitter.emit(
1442
+ events.log.log,
1443
+ theme.yellow(`> ${step.prompt || "no prompt"}`),
1444
+ null,
1445
+ );
1446
+
1447
+ try {
1448
+ if (!step.commands && !step.prompt) {
1449
+ this.emitter.emit(
1450
+ events.log.log,
1451
+ theme.red("No commands or prompt found"),
1452
+ );
1453
+
1454
+ this.emitter.emit(events.step.error, {
1455
+ stepIndex,
1456
+ prompt: step.prompt,
1457
+ error: "No commands or prompt found",
1458
+ timestamp: Date.now(),
1459
+ });
1460
+
1461
+ await this.exit(true, shouldSave, true);
1462
+ } else if (!step.commands) {
1463
+ this.emitter.emit(
1464
+ events.log.log,
1465
+ theme.yellow("No commands found, running exploratory"),
1466
+ );
1467
+ await this.exploratoryLoop(step.prompt, false, true, shouldSave);
1468
+ } else {
1469
+ await this.executeCommands(
1470
+ step.commands,
1471
+ 0,
1472
+ true,
1473
+ false,
1474
+ shouldSave,
1475
+ );
1476
+ }
1477
+
1478
+ const stepEndTime = Date.now();
1479
+ const stepDuration = stepEndTime - stepStartTime;
1480
+
1481
+ // Emit step success event with source mapping
1482
+ this.emitter.emit(events.step.success, {
1483
+ stepIndex,
1484
+ prompt: step.prompt,
1485
+ commandCount: step.commands ? step.commands.length : 0,
1486
+ duration: stepDuration,
1487
+ timestamp: stepEndTime,
1488
+ sourcePosition: sourcePosition,
1489
+ });
1490
+
1491
+ if (shouldSave) {
1492
+ await this.save({ silent: true });
1493
+ }
1494
+ } catch (error) {
1495
+ const stepEndTime = Date.now();
1496
+ const stepDuration = stepEndTime - stepStartTime;
1497
+
1498
+ // Emit step error event with source mapping
1499
+ this.emitter.emit(events.step.error, {
1500
+ stepIndex,
1501
+ prompt: step.prompt,
1502
+ error: error.message,
1503
+ duration: stepDuration,
1504
+ timestamp: stepEndTime,
1505
+ sourcePosition: sourcePosition,
1506
+ });
1507
+
1508
+ throw error; // Re-throw to maintain existing error handling
1509
+ }
1510
+ }
1511
+
1512
+ const testEndTime = Date.now();
1513
+ const fileDuration = testEndTime - fileStartTime;
1514
+
1515
+ // Emit file success event
1516
+ this.emitter.emit(events.file.stop, {
1517
+ operation: "run",
1518
+ filePath: file,
1519
+ duration: fileDuration,
1520
+ success: true,
1521
+ timestamp: testEndTime,
1522
+ });
1523
+
1524
+ if (shouldSave) {
1525
+ await this.save({ filepath: file, silent: false });
1526
+ }
1527
+ if (shouldExit) {
1528
+ await this.summarize();
1529
+ await this.exit(false, shouldSave, true);
1530
+ }
1531
+ } catch (error) {
1532
+ const testEndTime = Date.now();
1533
+ const fileDuration = testEndTime - fileStartTime;
1534
+
1535
+ // Emit file error event
1536
+ this.emitter.emit(events.file.error, {
1537
+ operation: "run",
1538
+ filePath: file,
1539
+ error: error.message,
1540
+ duration: fileDuration,
1541
+ timestamp: testEndTime,
1542
+ });
1543
+
1544
+ // Re-throw the error to maintain existing error handling
1545
+ throw error;
1546
+ }
1547
+ }
1548
+
1549
+ async iffy(condition, then, otherwise, depth) {
1550
+ this.analytics.track("if", { condition });
1551
+
1552
+ this.emitter.emit(
1553
+ events.log.log,
1554
+ generator.jsonToManual({ command: "if", condition }),
1555
+ );
1556
+
1557
+ try {
1558
+ await this.commands.assert(condition, false);
1559
+ return await this.executeCommands(then, ++depth);
1560
+ // eslint-disable-next-line no-unused-vars
1561
+ } catch (error) {
1562
+ return await this.executeCommands(otherwise, ++depth);
1563
+ }
1564
+ }
1565
+
1566
+ async embed(file, depth, pushToHistory) {
1567
+ let inputFile = JSON.parse(JSON.stringify(file));
1568
+
1569
+ this.analytics.track("embed", { file });
1570
+
1571
+ this.emitter.emit(
1572
+ events.log.log,
1573
+ generator.jsonToManual({ command: "run", file }),
1574
+ );
1575
+
1576
+ depth = depth + 1;
1577
+
1578
+ this.emitter.emit(events.log.log, `${inputFile} (start)`);
1579
+
1580
+ // Use the new helper method to resolve file paths relative to testdriver directory
1581
+ const currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
1582
+
1583
+ // if the file is not an absolute path, resolve it using the new helper
1584
+ if (!path.isAbsolute(file)) {
1585
+ file = this.resolveTestDriverRelativePath(currentFilePath, file);
1586
+ }
1587
+
1588
+ // check if the file exists
1589
+ if (!fs.existsSync(file)) {
1590
+ throw `Embedded file not found: ${file}`;
1591
+ }
1592
+
1593
+ let ymlObj = await this.loadYML(file);
1594
+
1595
+ // Store current source mapping state
1596
+ const previousContext = this.sourceMapper.saveContext();
1597
+
1598
+ // Set up source mapping for embedded file
1599
+ const parseResult = this.sourceMapper.parseYamlWithSourceMap(
1600
+ fs.readFileSync(file, "utf-8"),
1601
+ file,
1602
+ );
1603
+ this.sourceMapper.setCurrentContext(file, parseResult.sourceMap, -1, -1);
1604
+
1605
+ try {
1606
+ for (const step of ymlObj.steps) {
1607
+ const stepIndex = ymlObj.steps.indexOf(step);
1608
+ this.sourceMapper.setCurrentStep(stepIndex);
1609
+
1610
+ if (!step.commands && !step.prompt) {
1611
+ this.emitter.emit(
1612
+ events.log.log,
1613
+ theme.red("No commands or prompt found"),
1614
+ );
1615
+ await this.exit(true);
1616
+ } else if (!step.commands) {
1617
+ this.emitter.emit(
1618
+ events.log.log,
1619
+ theme.yellow("No commands found, running exploratory"),
1620
+ );
1621
+ await this.exploratoryLoop(step.prompt, false, true, false);
1622
+ } else {
1623
+ await this.executeCommands(step.commands, depth, pushToHistory);
1624
+ }
1625
+ }
1626
+ } finally {
1627
+ // Restore previous source mapping state
1628
+ this.sourceMapper.restoreContext(previousContext);
1629
+ }
1630
+
1631
+ this.emitter.emit(events.log.log, `${inputFile} (end)`);
1632
+ }
1633
+
1634
+ async buildEnv(options = {}) {
1635
+ // If instance already exists, do not build environment again
1636
+ if (this.instance) {
1637
+ this.emitter.emit(
1638
+ events.log.narration,
1639
+ theme.dim("sandbox instance already exists, skipping launch."),
1640
+ );
1641
+ return;
1642
+ }
1643
+
1644
+ let { headless = false, heal, new: createNew = false } = options;
1645
+
1646
+ // Prioritize this.newSandbox flag if it's set
1647
+ if (this.newSandbox) {
1648
+ createNew = true;
1649
+ }
1650
+
1651
+ // If CI environment variable is true, always create a new sandbox
1652
+ if (this.config.CI) {
1653
+ createNew = true;
1654
+ this.emitter.emit(
1655
+ events.log.log,
1656
+ theme.dim("CI environment detected, will create a new sandbox"),
1657
+ );
1658
+ }
1659
+
1660
+ if (heal) this.healMode = heal;
1661
+
1662
+ // If createNew flag is set, clear sandboxId to prevent reconnection attempts
1663
+ if (createNew) {
1664
+ this.sandboxId = null;
1665
+ if (!this.config.CI && !this.newSandbox) {
1666
+ this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
1667
+ } else if (this.newSandbox) {
1668
+ this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
1669
+ }
1670
+ }
1671
+
1672
+ // Create session first so session ID is available for Sentry tracing in WebSocket connection
1673
+ await this.newSession();
1674
+
1675
+ // order is important!
1676
+ await this.connectToSandboxService();
1677
+
1678
+ // Set sandbox ID for reconnection (only if not creating new and recent ID exists)
1679
+ if (this.ip) {
1680
+ let instance = await this.sandbox.send({
1681
+ type: "direct",
1682
+ resolution: this.config.TD_RESOLUTION,
1683
+ ci: this.config.CI,
1684
+ ip: this.ip,
1685
+ instanceId: this.instanceId || undefined,
1686
+ });
1687
+
1688
+ // Store connection params for reconnection
1689
+ // For direct IP connections, store as a direct type so reconnection
1690
+ // sends a 'direct' message instead of 'connect' with an IP as sandboxId
1691
+ this.sandbox.setConnectionParams({
1692
+ type: 'direct',
1693
+ ip: this.ip,
1694
+ sandboxId: instance?.instance?.instanceId || instance?.instance?.sandboxId || null,
1695
+ persist: true,
1696
+ keepAlive: this.keepAlive,
1697
+ });
1698
+
1699
+ // Mark instance socket as connected so console logs are forwarded
1700
+ this.sandbox.instanceSocketConnected = true;
1701
+ this.emitter.emit(events.sandbox.connected);
1702
+
1703
+ this.instance = instance.instance;
1704
+ await this.renderSandbox(this.instance, headless);
1705
+ await this.runLifecycle("provision");
1706
+
1707
+ return;
1708
+ } else if (!createNew && this.sandboxId && !this.config.CI) {
1709
+ // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1710
+ // Attempt to connect to known instance
1711
+ this.emitter.emit(
1712
+ events.log.narration,
1713
+ theme.dim(`connecting to sandbox ${this.sandboxId}...`),
1714
+ );
1715
+
1716
+ try {
1717
+ let instance = await this.connectToSandboxDirect(
1718
+ this.sandboxId,
1719
+ true, // always persist by default
1720
+ this.keepAlive, // pass keepAlive TTL
1721
+ );
1722
+
1723
+ this.instance = instance;
1724
+
1725
+ await this.renderSandbox(instance, headless);
1726
+ return;
1727
+ } catch (error) {
1728
+ // If connection fails, fall through to creating a new sandbox
1729
+ this.emitter.emit(
1730
+ events.log.narration,
1731
+ theme.dim(`failed to connect to recent sandbox, creating new one...`),
1732
+ );
1733
+ console.error("Failed to reconnect to sandbox:", error);
1734
+ }
1735
+ }
1736
+
1737
+ // Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
1738
+ if (!this.instance) {
1739
+ const { formatter } = require("../sdk-log-formatter.js");
1740
+ this.emitter.emit(
1741
+ events.log.narration,
1742
+ formatter.getPrefix("connect") +
1743
+ " " +
1744
+ theme.green.bold("Creating") +
1745
+ " " +
1746
+ theme.cyan(`new sandbox...`),
1747
+ );
1748
+ let newSandbox = await this.createNewSandbox();
1749
+
1750
+ // Extract the sandbox ID from the newly created sandbox
1751
+ this.sandboxId =
1752
+ newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1753
+
1754
+ // E2B sandboxes return a url directly from create — no separate
1755
+ // connect step needed (the API proxies commands via Ably).
1756
+ if (newSandbox?.sandbox?.url) {
1757
+ this.sandbox.setConnectionParams({
1758
+ sandboxId: this.sandboxId,
1759
+ persist: true,
1760
+ keepAlive: this.keepAlive,
1761
+ });
1762
+ this.emitter.emit(events.sandbox.connected);
1763
+ this.instance = newSandbox.sandbox;
1764
+ await this.renderSandbox(this.instance, headless);
1765
+ await this.runLifecycle("provision");
1766
+ } else {
1767
+ let instance = await this.connectToSandboxDirect(
1768
+ this.sandboxId,
1769
+ true, // always persist by default
1770
+ this.keepAlive, // pass keepAlive TTL
1771
+ );
1772
+ this.instance = instance;
1773
+ await this.renderSandbox(instance, headless);
1774
+ await this.runLifecycle("provision");
1775
+ }
1776
+ }
1777
+ }
1778
+
1779
+ async start() {
1780
+ try {
1781
+ this.emitter.emit(
1782
+ events.log.log,
1783
+ theme.green(`Howdy! I'm TestDriver v${packageJson.version}`),
1784
+ );
1785
+
1786
+ // Emit test start event for the entire test execution
1787
+ this.emitter.emit(events.test.start, {
1788
+ filePath: this.thisFile,
1789
+ timestamp: Date.now(),
1790
+ });
1791
+
1792
+ // Debugger UI is hosted on the web app (console.testdriver.ai/debugger/)
1793
+ // No local debugger server needed
1794
+ this.emitter.emit(events.log.log, `This is beta software!`);
1795
+ this.emitter.emit(events.log.log, ``);
1796
+ this.emitter.emit(
1797
+ events.log.log,
1798
+ theme.yellow(`Join our Discord for help`),
1799
+ );
1800
+ this.emitter.emit(
1801
+ events.log.log,
1802
+ `https://discord.com/invite/cWDFW8DzPm`,
1803
+ );
1804
+ this.emitter.emit(events.log.log, ``);
1805
+
1806
+ // make testdriver directory if it doesn't exist
1807
+ let testdriverFolder = path.join(this.workingDir);
1808
+ if (!fs.existsSync(testdriverFolder)) {
1809
+ fs.mkdirSync(testdriverFolder);
1810
+ // log
1811
+ this.emitter.emit(
1812
+ events.log.log,
1813
+ theme.dim(`Created testdriver directory: ${testdriverFolder}`),
1814
+ );
1815
+ }
1816
+
1817
+ // if the directory for thisFile doesn't exist, create it
1818
+ if (
1819
+ this.cliArgs.command !== "sandbox" &&
1820
+ this.cliArgs.command !== "generate"
1821
+ ) {
1822
+ const dir = path.dirname(this.thisFile);
1823
+ if (!fs.existsSync(dir)) {
1824
+ fs.mkdirSync(dir, { recursive: true });
1825
+ this.emitter.emit(
1826
+ events.log.log,
1827
+ theme.dim(`Created directory ${dir}`),
1828
+ );
1829
+ }
1830
+
1831
+ // if thisFile doesn't exist, create it
1832
+ // thisFile def to testdriver/testdriver.yaml, during init, it just creates an empty file
1833
+ if (!fs.existsSync(this.thisFile)) {
1834
+ fs.writeFileSync(this.thisFile, "");
1835
+ this.emitter.emit(
1836
+ events.log.log,
1837
+ theme.dim(`Created ${this.thisFile}`),
1838
+ );
1839
+ }
1840
+ }
1841
+
1842
+ if (this.config.TD_API_KEY) {
1843
+ await this.sdk.auth();
1844
+ }
1845
+
1846
+ if (
1847
+ this.cliArgs.command !== "sandbox" &&
1848
+ this.cliArgs.command !== "generate"
1849
+ ) {
1850
+ this.emitter.emit(
1851
+ events.log.log,
1852
+ theme.dim(`Working on ${this.thisFile}`),
1853
+ );
1854
+
1855
+ this.loadYML(this.thisFile);
1856
+ }
1857
+
1858
+ this.analytics.track("command", {
1859
+ command: this.cliArgs.command,
1860
+ file: this.thisFile,
1861
+ });
1862
+
1863
+ // Dynamically handle all available commands (except edit which is handled by CLI)
1864
+ const availableCommands = Object.keys(this.getCommandDefinitions());
1865
+ if (
1866
+ availableCommands.includes(this.cliArgs.command) &&
1867
+ this.cliArgs.command !== "edit"
1868
+ ) {
1869
+ await this.executeUnifiedCommand(
1870
+ this.cliArgs.command,
1871
+ this.cliArgs.args,
1872
+ this.cliArgs.options,
1873
+ this.cliArgs.options._optionValues,
1874
+ );
1875
+ } else if (this.cliArgs.command !== "edit") {
1876
+ this.emitter.emit(
1877
+ events.error.fatal,
1878
+ `Unknown command: ${this.cliArgs.command}`,
1879
+ );
1880
+ await this.exit(true);
1881
+ }
1882
+ } catch (error) {
1883
+ this.emitter.emit(events.error.fatal, error.message || error);
1884
+ await this.exit(true);
1885
+ }
1886
+ }
1887
+
1888
+ async renderSandbox(instance, headless = false) {
1889
+ if (!headless) {
1890
+ let url;
1891
+
1892
+ // If the instance already has a URL (from reconnection), use it
1893
+ if (instance.url) {
1894
+ url = instance.url;
1895
+ } else if (instance.ip || instance.publicIp) {
1896
+ // Otherwise construct it from IP and port
1897
+ url =
1898
+ "http://" +
1899
+ (instance.ip || instance.publicIp) +
1900
+ ":" +
1901
+ (instance.vncPort || "5800") +
1902
+ "/vnc_lite.html?token=V3b8wG9";
1903
+ } else {
1904
+ // If we don't have URL or IP, we can't render
1905
+ logger.warn("renderSandbox: Missing URL and IP in instance", instance);
1906
+ return;
1907
+ }
1908
+
1909
+ let data = {
1910
+ resolution: this.config.TD_RESOLUTION,
1911
+ url: url,
1912
+ token: "V3b8wG9",
1913
+ testFile: this.testFile || null,
1914
+ os: this.sandboxOs || "linux",
1915
+ };
1916
+
1917
+ // Base64 encode the data (the debugger expects base64, not URL encoding)
1918
+ const encodedData = Buffer.from(JSON.stringify(data)).toString("base64");
1919
+
1920
+ // Build debugger URL — hosted on S3 (v7-vnc bucket)
1921
+ const debuggerBase = process.env.TD_DEBUGGER_BASE_URL || "http://v7-vnc.s3.us-east-2.amazonaws.com";
1922
+ // URL-encode the base64 data to handle +, /, = characters safely
1923
+ const urlToOpen = `${debuggerBase}/index.html?data=${encodeURIComponent(encodedData)}`;
1924
+
1925
+ // Check preview mode from CLI options (SDK passes it directly)
1926
+ const previewMode = (this.cliArgs.options && this.cliArgs.options.preview) || this.config.TD_PREVIEW || "browser";
1927
+
1928
+ if (previewMode === "ide") {
1929
+ // Send session to VS Code extension via HTTP
1930
+ this.writeIdeSessionFile(urlToOpen, data);
1931
+ } else if (previewMode !== "none") {
1932
+ // Open in browser (default behavior)
1933
+ this.emitter.emit(events.showWindow, urlToOpen);
1934
+ }
1935
+ // If preview is "none", don't open anything
1936
+ }
1937
+ }
1938
+
1939
+ // Get the console (web app) URL for the given API root
1940
+ _getConsoleUrl(apiRoot) {
1941
+ // Allow explicit override via env (e.g. VITE_DOMAIN from .env)
1942
+ if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
1943
+
1944
+ const environments = require("../lib/environments.json");
1945
+ const mapping = {
1946
+ "https://v6.testdriver.ai": environments.stable.consoleUrl,
1947
+ };
1948
+ for (const env of Object.values(environments)) {
1949
+ mapping[env.apiRoot] = env.consoleUrl;
1950
+ }
1951
+ if (mapping[apiRoot]) return mapping[apiRoot];
1952
+ // Local dev: API on localhost:1337 -> Web on localhost:3001
1953
+ if (apiRoot.includes("localhost:1337") || apiRoot.includes("127.0.0.1:1337")) {
1954
+ return "http://localhost:3001";
1955
+ }
1956
+ return environments.stable.consoleUrl;
1957
+ }
1958
+
1959
+ // Write session file for IDE preview (VSCode extension watches for these)
1960
+ writeIdeSessionFile(debuggerUrl, data) {
1961
+ const fs = require("fs");
1962
+ const path = require("path");
1963
+
1964
+ const sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
1965
+ const previewsDir = path.join(process.cwd(), ".testdriver", ".previews");
1966
+
1967
+ // Create the previews directory if it doesn't exist
1968
+ if (!fs.existsSync(previewsDir)) {
1969
+ fs.mkdirSync(previewsDir, { recursive: true });
1970
+ }
1971
+
1972
+ const sessionData = {
1973
+ sessionId,
1974
+ debuggerUrl,
1975
+ resolution: Array.isArray(data.resolution) ? data.resolution : (data.resolution ? data.resolution.split("x").map(Number) : [1920, 1080]),
1976
+ testFile: data.testFile || this.testFile || null,
1977
+ os: data.os || this.sandboxOs || "linux",
1978
+ timestamp: Date.now(),
1979
+ };
1980
+
1981
+ const filePath = path.join(previewsDir, `${sessionId}.json`);
1982
+ fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
1983
+
1984
+ logger.log(`IDE preview session written to ${filePath}`);
1985
+ }
1986
+
1987
+ // Find the VS Code instance that contains the test file
1988
+ findTargetIdeInstance(testFilePath) {
1989
+ const fs = require("fs");
1990
+ const os = require("os");
1991
+ const path = require("path");
1992
+
1993
+ const instancesDir = path.join(os.homedir(), ".testdriver", "ide-instances");
1994
+
1995
+ if (!fs.existsSync(instancesDir)) {
1996
+ return null;
1997
+ }
1998
+
1999
+ const files = fs.readdirSync(instancesDir);
2000
+ const normalizedTestPath = testFilePath ? path.normalize(testFilePath) : null;
2001
+
2002
+ let matchingInstance = null;
2003
+ let longestMatchLength = 0;
2004
+
2005
+ for (const file of files) {
2006
+ if (!file.endsWith('.json')) continue;
2007
+
2008
+ try {
2009
+ const registrationPath = path.join(instancesDir, file);
2010
+ const registration = JSON.parse(fs.readFileSync(registrationPath, 'utf-8'));
2011
+
2012
+ // Check if this instance is still alive (registration within last 60 seconds or process exists)
2013
+ const isRecent = Date.now() - registration.timestamp < 60000;
2014
+
2015
+ // Skip stale registrations
2016
+ if (!isRecent) {
2017
+ // Try to clean up stale file
2018
+ try { fs.unlinkSync(registrationPath); } catch {}
2019
+ continue;
2020
+ }
2021
+
2022
+ // If we have a test file path, find the best matching workspace
2023
+ if (normalizedTestPath && registration.workspacePaths) {
2024
+ for (const workspacePath of registration.workspacePaths) {
2025
+ const normalizedWorkspace = path.normalize(workspacePath);
2026
+ if (normalizedTestPath.startsWith(normalizedWorkspace + path.sep) ||
2027
+ normalizedTestPath === normalizedWorkspace) {
2028
+ // Prefer longest match (most specific workspace)
2029
+ if (normalizedWorkspace.length > longestMatchLength) {
2030
+ longestMatchLength = normalizedWorkspace.length;
2031
+ matchingInstance = registration;
2032
+ }
2033
+ }
2034
+ }
2035
+ } else if (!matchingInstance) {
2036
+ // If no test file path, just use the first available instance
2037
+ matchingInstance = registration;
2038
+ }
2039
+ } catch (error) {
2040
+ // Ignore malformed registration files
2041
+ }
2042
+ }
2043
+
2044
+ return matchingInstance;
2045
+ }
2046
+
2047
+ // Send session notification to VS Code extension via HTTP
2048
+ sendIdeSessionNotification(debuggerUrl, data) {
2049
+ const http = require("http");
2050
+ const path = require("path");
2051
+
2052
+ const testFilePath = data.testFile || this.thisFile;
2053
+ const targetInstance = this.findTargetIdeInstance(testFilePath);
2054
+
2055
+ if (!targetInstance) {
2056
+ logger.warn("No VS Code instance found for IDE preview. Make sure VS Code with TestDriver extension is open.");
2057
+ // Fall back to browser
2058
+ this.emitter.emit(events.showWindow, debuggerUrl);
2059
+ return;
2060
+ }
2061
+
2062
+ // Generate a unique session ID
2063
+ const testFileName = (testFilePath || "test")
2064
+ .split(path.sep).pop()
2065
+ .replace(/\.[^/.]+$/, "");
2066
+ const sessionId = `${testFileName}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
2067
+
2068
+ const sessionData = {
2069
+ sessionId: sessionId,
2070
+ debuggerUrl: debuggerUrl,
2071
+ resolution: data.resolution || this.config.TD_RESOLUTION,
2072
+ testFile: testFilePath,
2073
+ os: data.os || this.sandboxOs || "linux",
2074
+ timestamp: Date.now(),
2075
+ };
2076
+
2077
+ const postData = JSON.stringify(sessionData);
2078
+
2079
+ const options = {
2080
+ hostname: '127.0.0.1',
2081
+ port: targetInstance.port,
2082
+ path: '/session',
2083
+ method: 'POST',
2084
+ headers: {
2085
+ 'Content-Type': 'application/json',
2086
+ 'Content-Length': Buffer.byteLength(postData)
2087
+ },
2088
+ timeout: 5000
2089
+ };
2090
+
2091
+ const req = http.request(options, (res) => {
2092
+ if (res.statusCode === 200) {
2093
+ logger.log(`IDE session notification sent to port ${targetInstance.port}`);
2094
+ } else {
2095
+ logger.warn(`IDE session notification failed with status ${res.statusCode}`);
2096
+ // Fall back to browser on failure
2097
+ this.emitter.emit(events.showWindow, debuggerUrl);
2098
+ }
2099
+ });
2100
+
2101
+ req.on('error', (error) => {
2102
+ logger.warn(`Failed to send IDE session notification: ${error.message}`);
2103
+ // Fall back to browser on error
2104
+ this.emitter.emit(events.showWindow, debuggerUrl);
2105
+ });
2106
+
2107
+ req.on('timeout', () => {
2108
+ req.destroy();
2109
+ logger.warn('IDE session notification timed out');
2110
+ // Fall back to browser on timeout
2111
+ this.emitter.emit(events.showWindow, debuggerUrl);
2112
+ });
2113
+
2114
+ req.write(postData);
2115
+ req.end();
2116
+ }
2117
+
2118
+ async connectToSandboxService() {
2119
+ this.emitter.emit(
2120
+ events.log.narration,
2121
+ theme.dim(`establishing connection...`),
2122
+ );
2123
+ let ableToBoot = await this.sandbox.boot(this.config.TD_API_ROOT);
2124
+
2125
+ if (!ableToBoot) {
2126
+ return await this.dieOnFatal(
2127
+ `Unable to connect to TestDriver sandbox service at ${this.config.TD_API_ROOT}.
2128
+ Please check your network connection, TD_API_KEY, or the service status.`,
2129
+ true,
2130
+ );
2131
+ }
2132
+
2133
+ const { formatter } = require("../sdk-log-formatter.js");
2134
+ this.emitter.emit(
2135
+ events.log.narration,
2136
+ formatter.getPrefix("connect") +
2137
+ " " +
2138
+ theme.green.bold("Authenticating") +
2139
+ theme.dim("..."),
2140
+ );
2141
+ let ableToAuth = await this.sandbox.auth(this.config.TD_API_KEY);
2142
+
2143
+ if (!ableToAuth) {
2144
+ return await this.dieOnFatal(
2145
+ `Unable to authorize with TestDriver sandbox service at ${this.config.TD_API_ROOT}.
2146
+ Please check your network connection, TD_API_KEY, or the service status.`,
2147
+ true,
2148
+ );
2149
+ }
2150
+ }
2151
+
2152
+ async connectToSandboxDirect(sandboxId, persist = false, keepAlive = null) {
2153
+ const { formatter } = require("../sdk-log-formatter.js");
2154
+ this.emitter.emit(
2155
+ events.log.narration,
2156
+ formatter.getPrefix("connect") +
2157
+ " " +
2158
+ theme.green.bold("Connecting") +
2159
+ " " +
2160
+ theme.cyan(`to sandbox...`),
2161
+ );
2162
+ let reply = await this.sandbox.connect(sandboxId, persist, keepAlive);
2163
+
2164
+ // reply includes { success, url, sandbox: {...} }
2165
+ // For renderSandbox, we need the sandbox object with url merged in
2166
+ const sandbox = reply.sandbox || {};
2167
+
2168
+ // If reply has a URL at top level, merge it into the sandbox object
2169
+ if (reply.url && !sandbox.url) {
2170
+ sandbox.url = reply.url;
2171
+ }
2172
+
2173
+ return sandbox;
2174
+ }
2175
+
2176
+ async createNewSandbox() {
2177
+ const sandboxConfig = {
2178
+ type: "create",
2179
+ resolution: this.config.TD_RESOLUTION,
2180
+ ci: this.config.CI,
2181
+ os: this.sandboxOs || "linux",
2182
+ };
2183
+
2184
+ // Add AMI and instance type if specified
2185
+ if (this.sandboxAmi) {
2186
+ sandboxConfig.ami = this.sandboxAmi;
2187
+ }
2188
+ if (this.sandboxInstance) {
2189
+ sandboxConfig.instanceType = this.sandboxInstance;
2190
+ }
2191
+ // Add keepAlive TTL if specified
2192
+ if (this.keepAlive !== undefined && this.keepAlive !== null) {
2193
+ sandboxConfig.keepAlive = this.keepAlive;
2194
+ }
2195
+
2196
+ const { formatter } = require("../sdk-log-formatter.js");
2197
+ const retryDelay = 15000; // 15 seconds between retries
2198
+
2199
+ while (true) {
2200
+ let response = await this.sandbox.send(sandboxConfig, 60000 * 8);
2201
+
2202
+ // Check if queued (all slots in use)
2203
+ if (response.type === "create.queued") {
2204
+ this.emitter.emit(
2205
+ events.log.narration,
2206
+ formatter.getPrefix("queue") +
2207
+ " " +
2208
+ theme.yellow.bold("Waiting") +
2209
+ " " +
2210
+ theme.dim(response.message),
2211
+ );
2212
+
2213
+ // Wait then retry
2214
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
2215
+ continue;
2216
+ }
2217
+
2218
+ // Success - got a sandbox
2219
+ return response;
2220
+ }
2221
+ }
2222
+
2223
+ async newSession() {
2224
+ // should be start of new session
2225
+ // If sandbox is connected, get system info; otherwise pass empty objects
2226
+ const isSandboxConnected = this.sandbox.apiSocketConnected;
2227
+
2228
+ const sessionRes = await this.sdk.req("session/start", {
2229
+ systemInformationOsInfo: isSandboxConnected
2230
+ ? await this.system.getSystemInformationOsInfo()
2231
+ : {},
2232
+ mousePosition: isSandboxConnected
2233
+ ? await this.system.getMousePosition()
2234
+ : {},
2235
+ activeWindow: isSandboxConnected ? await this.system.activeWin() : {},
2236
+ });
2237
+
2238
+ if (!sessionRes) {
2239
+ throw new Error(
2240
+ "Unable to start TestDriver session. Check your network connection or restart the CLI.",
2241
+ );
2242
+ }
2243
+
2244
+ this.session.set(sessionRes.data.id);
2245
+
2246
+ // Set Sentry session trace context for distributed tracing
2247
+ // This links CLI errors/logs to the same trace as API calls
2248
+ try {
2249
+ const sentry = require("../lib/sentry");
2250
+ sentry.setSessionTraceContext(sessionRes.data.id);
2251
+ } catch (e) {
2252
+ // Sentry module may not be available, ignore
2253
+ }
2254
+ }
2255
+
2256
+ // Helper method to find testdriver directory by traversing up from a file path
2257
+ findTestDriverDirectory(filePath) {
2258
+ // Start from the directory containing the file, or use workingDir as fallback
2259
+ let currentDir = filePath
2260
+ ? path.dirname(path.resolve(filePath))
2261
+ : this.workingDir;
2262
+
2263
+ while (currentDir !== path.dirname(currentDir)) {
2264
+ // Continue until we reach the root
2265
+ const testdriverPath = path.join(currentDir, "testdriver");
2266
+ if (
2267
+ fs.existsSync(testdriverPath) &&
2268
+ fs.statSync(testdriverPath).isDirectory()
2269
+ ) {
2270
+ return testdriverPath;
2271
+ }
2272
+ currentDir = path.dirname(currentDir);
2273
+ }
2274
+
2275
+ // Fallback to workingDir/testdriver if not found
2276
+ return path.join(this.workingDir, "testdriver");
2277
+ }
2278
+
2279
+ // Helper method to resolve file paths relative to the testdriver directory
2280
+ // This handles both snippets and other relative files that should be resolved
2281
+ // relative to the nearest testdriver directory
2282
+ resolveTestDriverRelativePath(filePath, relativePath) {
2283
+ // If it's already an absolute path, return as-is
2284
+ if (path.isAbsolute(relativePath)) {
2285
+ return relativePath;
2286
+ }
2287
+
2288
+ // Check if this looks like a snippet or lifecycle reference
2289
+ if (
2290
+ relativePath.startsWith("snippets/") ||
2291
+ relativePath.startsWith("lifecycle/")
2292
+ ) {
2293
+ // First, check if there's a local directory in the same directory as the current file
2294
+ if (filePath) {
2295
+ const currentFileDir = path.dirname(path.resolve(filePath));
2296
+ const localPath = path.join(currentFileDir, relativePath);
2297
+
2298
+ if (fs.existsSync(localPath)) {
2299
+ return localPath;
2300
+ }
2301
+ }
2302
+
2303
+ // If no local file found, fall back to the testdriver directory
2304
+ const testdriverDir = this.findTestDriverDirectory(filePath);
2305
+ return path.join(testdriverDir, relativePath);
2306
+ }
2307
+
2308
+ // For other relative paths, resolve relative to the current file's directory
2309
+ if (filePath) {
2310
+ return path.resolve(path.dirname(filePath), relativePath);
2311
+ }
2312
+
2313
+ // Fallback to workingDir
2314
+ return path.resolve(this.workingDir, relativePath);
2315
+ }
2316
+
2317
+ async runLifecycle(lifecycleName) {
2318
+ // Use the current file path from sourceMapper to find the lifecycle directory
2319
+ // If sourceMapper doesn't have a current file, use thisFile which should be the file being run
2320
+ let currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
2321
+
2322
+ // If we still don't have a currentFilePath, fall back to the default testdriver directory
2323
+ if (!currentFilePath) {
2324
+ currentFilePath = path.join(
2325
+ this.workingDir,
2326
+ "testdriver",
2327
+ "testdriver.yaml",
2328
+ );
2329
+ }
2330
+
2331
+ // Ensure we have an absolute path
2332
+ if (currentFilePath && !path.isAbsolute(currentFilePath)) {
2333
+ currentFilePath = path.resolve(this.workingDir, currentFilePath);
2334
+ }
2335
+ let lifecycleFile = null;
2336
+
2337
+ // First, check if there's a local lifecycle directory in the same directory as the current file
2338
+ if (currentFilePath) {
2339
+ const currentFileDir = path.dirname(currentFilePath);
2340
+ const localLifecycleDir = path.join(currentFileDir, "lifecycle");
2341
+ const localLifecycleFile = path.join(
2342
+ localLifecycleDir,
2343
+ `${lifecycleName}.yaml`,
2344
+ );
2345
+ // If there's a local lifecycle directory, only look there (don't fall back to global)
2346
+ if (
2347
+ fs.existsSync(localLifecycleDir) &&
2348
+ fs.statSync(localLifecycleDir).isDirectory()
2349
+ ) {
2350
+ if (fs.existsSync(localLifecycleFile)) {
2351
+ lifecycleFile = localLifecycleFile;
2352
+ }
2353
+ // Stop here - don't fall back to global if local lifecycle directory exists
2354
+ } else {
2355
+ // Only fall back to global if there's no local lifecycle directory
2356
+ const testdriverDir = this.findTestDriverDirectory(currentFilePath);
2357
+ const globalLifecycleFile = path.join(
2358
+ testdriverDir,
2359
+ "lifecycle",
2360
+ `${lifecycleName}.yaml`,
2361
+ );
2362
+ if (fs.existsSync(globalLifecycleFile)) {
2363
+ lifecycleFile = globalLifecycleFile;
2364
+ }
2365
+ }
2366
+ }
2367
+
2368
+ this.emitter.emit(events.log.log, lifecycleFile);
2369
+
2370
+ if (lifecycleFile) {
2371
+ // Store current source mapping state before running lifecycle file
2372
+ const previousContext = this.sourceMapper.saveContext();
2373
+
2374
+ try {
2375
+ await this.run(lifecycleFile, false, false);
2376
+ } finally {
2377
+ // Restore previous source mapping state after lifecycle file execution
2378
+ this.sourceMapper.restoreContext(previousContext);
2379
+ }
2380
+ }
2381
+ } // Unified command definitions that work for both CLI and interactive modes
2382
+ getCommandDefinitions() {
2383
+ return createCommandDefinitions(this);
2384
+ }
2385
+
2386
+ // Execute a unified command
2387
+ async executeUnifiedCommand(commandName, args = {}, options = {}) {
2388
+ const commands = this.getCommandDefinitions();
2389
+ const command = commands[commandName];
2390
+
2391
+ if (!command) {
2392
+ throw new Error(`Unknown command: ${commandName}`);
2393
+ }
2394
+
2395
+ // Convert args array to object if needed
2396
+ const argsObj = {};
2397
+ if (Array.isArray(args)) {
2398
+ // Get argument definitions from the command
2399
+ const argDefs = command.args ? Object.values(command.args) : [];
2400
+ const argNames = command.args ? Object.keys(command.args) : [];
2401
+
2402
+ // Handle both positional args (/run myfile) and named args (/run file=myfile)
2403
+ args.forEach((arg, index) => {
2404
+ if (typeof arg === "string" && arg.includes("=")) {
2405
+ // Named argument: file=myfile or path=myfile
2406
+ const [key, value] = arg.split("=", 2);
2407
+ // Support both 'file' and 'path' for the run command
2408
+ if (commandName === "run" && key === "path") {
2409
+ argsObj["file"] = value;
2410
+ } else {
2411
+ argsObj[key] = value;
2412
+ }
2413
+ } else {
2414
+ // Positional argument: myfile
2415
+ const argName = argNames[index];
2416
+ if (argName) {
2417
+ const argDef = argDefs[index];
2418
+ if (argDef && argDef.variadic) {
2419
+ argsObj[argName] = args.slice(index);
2420
+ } else {
2421
+ argsObj[argName] = arg;
2422
+ }
2423
+ }
2424
+ }
2425
+ });
2426
+
2427
+ // Apply defaults for any missing arguments
2428
+ argNames.forEach((argName, index) => {
2429
+ const argDef = argDefs[index];
2430
+ if (argsObj[argName] === undefined && argDef && argDef.default) {
2431
+ argsObj[argName] = argDef.default;
2432
+ }
2433
+ });
2434
+ } else {
2435
+ Object.assign(argsObj, args);
2436
+ }
2437
+
2438
+ // Move environment setup and special handling here
2439
+ if (["edit", "run", "generate"].includes(commandName)) {
2440
+ await this.buildEnv(options);
2441
+ }
2442
+
2443
+ if (commandName === "run") {
2444
+ this.errorLimit = 100;
2445
+ }
2446
+ await command.handler(argsObj, options);
2447
+ }
2448
+ }
2449
+
2450
+ module.exports = TestDriverAgent;