testdriverai 6.2.2 → 7.1.0

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 (300) hide show
  1. package/.github/workflows/acceptance-linux.yml +75 -0
  2. package/.github/workflows/acceptance-sdk-tests.yml +133 -0
  3. package/.vscode/settings.json +5 -1
  4. package/AGENTS.md +550 -0
  5. package/CODEOWNERS +0 -1
  6. package/README.md +126 -0
  7. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  8. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  9. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  10. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  11. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  12. package/_testdriver/lifecycle/prerun.yaml +15 -0
  13. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  14. package/agent/index.js +300 -85
  15. package/agent/interface.js +15 -0
  16. package/agent/lib/cache.js +142 -0
  17. package/agent/lib/commander.js +1 -39
  18. package/agent/lib/commands.js +910 -296
  19. package/agent/lib/redraw.js +129 -41
  20. package/agent/lib/sandbox.js +29 -6
  21. package/agent/lib/sdk.js +22 -0
  22. package/agent/lib/system.js +0 -3
  23. package/agent/lib/validation.js +1 -7
  24. package/debug-locate-response.js +82 -0
  25. package/debugger/index.html +15 -4
  26. package/docs/ARCHITECTURE.md +424 -0
  27. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  28. package/docs/MIGRATION.md +425 -0
  29. package/docs/PRESETS.md +210 -0
  30. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  31. package/docs/SDK_AWESOME_LOGS.md +468 -0
  32. package/docs/TEST_RECORDING.md +388 -0
  33. package/docs/docs.json +286 -152
  34. package/docs/guide/best-practices-polling.mdx +154 -0
  35. package/docs/sdk-browser-rendering.md +167 -0
  36. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  37. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  38. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  39. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  40. package/docs/v6/overview/comparison.mdx +101 -0
  41. package/docs/v7/README.md +135 -0
  42. package/docs/v7/api/ai.mdx +205 -0
  43. package/docs/v7/api/assert.mdx +285 -0
  44. package/docs/v7/api/assertions.mdx +403 -0
  45. package/docs/v7/api/click.mdx +287 -0
  46. package/docs/v7/api/client.mdx +322 -0
  47. package/docs/v7/api/dashcam.mdx +497 -0
  48. package/docs/v7/api/doubleClick.mdx +102 -0
  49. package/docs/v7/api/elements.mdx +479 -0
  50. package/docs/v7/api/exec.mdx +346 -0
  51. package/docs/v7/api/find.mdx +316 -0
  52. package/docs/v7/api/focusApplication.mdx +294 -0
  53. package/docs/v7/api/hover.mdx +279 -0
  54. package/docs/v7/api/mouseDown.mdx +161 -0
  55. package/docs/v7/api/mouseUp.mdx +164 -0
  56. package/docs/v7/api/pressKeys.mdx +349 -0
  57. package/docs/v7/api/rightClick.mdx +123 -0
  58. package/docs/v7/api/sandbox.mdx +404 -0
  59. package/docs/v7/api/scroll.mdx +300 -0
  60. package/docs/v7/api/type.mdx +314 -0
  61. package/docs/v7/commands/assert.mdx +45 -0
  62. package/docs/v7/commands/exec.mdx +282 -0
  63. package/docs/v7/commands/focus-application.mdx +44 -0
  64. package/docs/v7/commands/hover-image.mdx +69 -0
  65. package/docs/v7/commands/hover-text.mdx +47 -0
  66. package/docs/v7/commands/if.mdx +53 -0
  67. package/docs/v7/commands/match-image.mdx +67 -0
  68. package/docs/v7/commands/press-keys.mdx +87 -0
  69. package/docs/v7/commands/remember.mdx +49 -0
  70. package/docs/v7/commands/run.mdx +44 -0
  71. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  72. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  73. package/docs/v7/commands/scroll.mdx +69 -0
  74. package/docs/v7/commands/type.mdx +45 -0
  75. package/docs/v7/commands/wait-for-image.mdx +54 -0
  76. package/docs/v7/commands/wait-for-text.mdx +48 -0
  77. package/docs/v7/commands/wait.mdx +45 -0
  78. package/docs/v7/getting-started/configuration.mdx +380 -0
  79. package/docs/v7/getting-started/quickstart.mdx +332 -0
  80. package/docs/v7/guides/best-practices.mdx +486 -0
  81. package/docs/v7/guides/caching-ai.mdx +215 -0
  82. package/docs/v7/guides/caching-selectors.mdx +292 -0
  83. package/docs/v7/guides/caching.mdx +366 -0
  84. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  85. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  86. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  87. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  88. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  89. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  90. package/docs/v7/guides/debugging.mdx +349 -0
  91. package/docs/v7/guides/faq.mdx +393 -0
  92. package/docs/v7/guides/migration.mdx +562 -0
  93. package/docs/v7/guides/performance.mdx +517 -0
  94. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  95. package/docs/v7/guides/troubleshooting.mdx +526 -0
  96. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  97. package/docs/v7/guides/vitest.mdx +535 -0
  98. package/docs/v7/platforms/linux.mdx +308 -0
  99. package/docs/v7/platforms/macos.mdx +433 -0
  100. package/docs/v7/platforms/windows.mdx +430 -0
  101. package/docs/v7/playwright.mdx +342 -0
  102. package/docs/v7/presets/chrome-extension.mdx +223 -0
  103. package/docs/v7/presets/chrome.mdx +287 -0
  104. package/docs/v7/presets/electron.mdx +435 -0
  105. package/docs/v7/presets/vscode.mdx +398 -0
  106. package/docs/v7/presets/webapp.mdx +396 -0
  107. package/docs/v7/progressive-apis/CORE.md +459 -0
  108. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  109. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  110. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  111. package/eslint.config.js +19 -1
  112. package/interfaces/cli/lib/base.js +10 -4
  113. package/interfaces/logger.js +2 -1
  114. package/interfaces/shared-test-state.mjs +69 -0
  115. package/interfaces/vitest-plugin.mjs +830 -0
  116. package/package.json +29 -5
  117. package/schema.json +8 -29
  118. package/scripts/view-test-results.mjs +96 -0
  119. package/sdk-log-formatter.js +714 -0
  120. package/sdk.d.ts +1028 -0
  121. package/sdk.js +2567 -0
  122. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  123. package/setup/aws/cloudformation.yaml +9 -2
  124. package/src/core/Dashcam.js +469 -0
  125. package/src/core/index.d.ts +150 -0
  126. package/src/core/index.js +12 -0
  127. package/src/presets/index.mjs +331 -0
  128. package/src/vitest/extended.mjs +108 -0
  129. package/src/vitest/hooks.d.ts +119 -0
  130. package/src/vitest/hooks.mjs +298 -0
  131. package/src/vitest/index.mjs +64 -0
  132. package/src/vitest/lifecycle.mjs +277 -0
  133. package/src/vitest/utils.mjs +150 -0
  134. package/test/dashcam.test.js +137 -0
  135. package/test/mcp-example-test.yaml +27 -0
  136. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  137. package/testdriver/acceptance-sdk/README.md +128 -0
  138. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  139. package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
  140. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  141. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  142. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
  143. package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
  144. package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
  145. package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
  146. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
  147. package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
  148. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
  149. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  150. package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
  151. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
  152. package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
  153. package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
  154. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  155. package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
  156. package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
  157. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
  158. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
  159. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
  160. package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
  161. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  162. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
  163. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  164. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  165. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  166. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  167. package/testdriver/acceptance-sdk/type.test.mjs +45 -0
  168. package/verify-element-api.js +89 -0
  169. package/verify-types.js +0 -0
  170. package/vitest.config.example.js +19 -0
  171. package/vitest.config.mjs +66 -0
  172. package/vitest.config.mjs.bak +44 -0
  173. package/.github/workflows/acceptance-v6.yml +0 -169
  174. package/.vscode/mcp.json +0 -9
  175. package/docs/overview/comparison.mdx +0 -82
  176. package/testdriver/lifecycle/prerun.yaml +0 -17
  177. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  178. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  179. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  180. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  181. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  182. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  183. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  184. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  185. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  186. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  187. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  188. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  189. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  190. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  191. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  192. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  193. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  194. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  195. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  196. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  197. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  198. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  199. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  200. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  201. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  202. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  203. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  204. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  205. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  206. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  207. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  208. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  209. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  210. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  211. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  212. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  213. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  214. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  215. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  216. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  217. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  218. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  219. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  220. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  221. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  222. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  223. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  224. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  225. /package/docs/{account → v6/account}/projects.mdx +0 -0
  226. /package/docs/{account → v6/account}/team.mdx +0 -0
  227. /package/docs/{action → v6/action}/ami.mdx +0 -0
  228. /package/docs/{action → v6/action}/performance.mdx +0 -0
  229. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  230. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  231. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  232. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  233. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  234. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  235. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  236. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  237. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  238. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  239. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  240. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  241. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  242. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  243. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  244. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  245. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  246. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  247. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  248. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  249. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  250. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  251. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  252. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  253. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  254. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  255. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  256. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  257. /package/docs/{features → v6/features}/generation.mdx +0 -0
  258. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  259. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  260. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  261. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  262. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  263. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  264. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  265. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  266. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  267. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  268. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  269. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  270. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  271. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  272. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  273. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  274. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  275. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  276. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  277. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  278. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  279. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  280. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  281. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  282. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  283. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  284. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  285. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  286. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  287. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  288. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  289. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  290. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  291. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  292. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  293. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  294. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  295. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  296. /package/docs/{security → v6/security}/action.mdx +0 -0
  297. /package/docs/{security → v6/security}/agent.mdx +0 -0
  298. /package/docs/{security → v6/security}/platform.mdx +0 -0
  299. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  300. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -4,7 +4,12 @@ steps:
4
4
  commands:
5
5
  - command: exec
6
6
  lang: pwsh
7
- code: dashcam track --name=TestDriver --type=application --pattern="C:\Users\testdriver\Documents\testdriver.log"
7
+ code: |
8
+ npm install dashcam@beta -g
9
+ npm list -g dashcam
10
+ - command: exec
11
+ lang: pwsh
12
+ code: dashcam track --name=TestDriver --type=app --pattern="C:\Users\testdriver\Documents\testdriver.log"
8
13
  - command: exec
9
14
  lang: pwsh
10
15
  code: dashcam start
@@ -3,5 +3,6 @@ steps:
3
3
  - prompt: stop dashcam
4
4
  commands:
5
5
  - command: exec
6
- lang: pwsh
7
- code: dashcam -t 'Web Test Recording' -p
6
+ lang: sh
7
+ timeout: 120000
8
+ code: dashcam stop
@@ -0,0 +1,15 @@
1
+ version: 6.0.0
2
+ steps:
3
+ - prompt: dashcam
4
+ commands:
5
+ - command: exec
6
+ lang: sh
7
+ code: |
8
+ dashcam auth 4e93d8bf-3886-4d26-a144-116c4063522d
9
+ dashcam logs --add --name="TestDriver" --type=file --file="/tmp/testdriver.log"
10
+ - command: exec
11
+ lang: sh
12
+ code: dashcam record > /dev/null 2>&1 &
13
+ - command: exec
14
+ lang: sh
15
+ code: dashcam logs --add --name=TestDriver --type=file --file="/tmp/testdriver.log"
@@ -3,6 +3,11 @@ session: 67f00511acbd9ccac373edf7
3
3
  steps:
4
4
  - prompt: launch chrome
5
5
  commands:
6
+ - command: exec
7
+ lang: sh
8
+ code: |
9
+ npm install dashcam@beta -g
10
+ npm list -g dashcam
6
11
  - command: exec
7
12
  lang: sh
8
13
  code: |
@@ -13,8 +18,8 @@ steps:
13
18
  --no-default-browser-check \
14
19
  --no-first-run \
15
20
  --guest \
16
- "${TD_WEBSITE}" \
21
+ "https://testdriver-sandbox.vercel.app/" \
17
22
  >/dev/null 2>&1 &
18
23
  - command: wait-for-text
19
- text: ${TD_WEBSITE}
24
+ text: testdriver-sandbox
20
25
  timeout: 60000
package/agent/index.js CHANGED
@@ -19,6 +19,7 @@ const diff = require("diff");
19
19
 
20
20
  // global utilities
21
21
  const generator = require("./lib/generator.js");
22
+ const promptCache = require("./lib/cache.js");
22
23
  const theme = require("./lib/theme.js");
23
24
  const SourceMapper = require("./lib/source-mapper.js");
24
25
 
@@ -74,6 +75,7 @@ class TestDriverAgent extends EventEmitter2 {
74
75
  this.sandboxId = flags["sandbox-id"] || null;
75
76
  this.sandboxAmi = flags["sandbox-ami"] || null;
76
77
  this.sandboxInstance = flags["sandbox-instance"] || null;
78
+ this.sandboxOs = flags.os || "linux";
77
79
  this.ip = flags.ip || null;
78
80
  this.workingDir = flags.workingDir || process.cwd();
79
81
 
@@ -101,14 +103,17 @@ class TestDriverAgent extends EventEmitter2 {
101
103
  // Create outputs instance for this agent
102
104
  this.outputs = createOutputs();
103
105
 
104
- // Create SDK instance with this agent's emitter, config, and session
106
+ // Create SDK instance with this agent's emitter, config, session, and abort signal
105
107
  this.sdk = createSDK(this.emitter, this.config, this.session);
106
108
 
107
109
  // Create analytics instance with this agent's emitter, config, and session
108
110
  this.analytics = createAnalytics(this.emitter, this.config, this.session);
109
111
 
110
- // Create sandbox instance with this agent's emitter and analytics
111
- this.sandbox = createSandbox(this.emitter, this.analytics);
112
+ // Create sandbox instance with this agent's emitter, analytics, and session
113
+ this.sandbox = createSandbox(this.emitter, this.analytics, this.session);
114
+
115
+ // Set the OS for the sandbox to use
116
+ this.sandbox.os = this.sandboxOs;
112
117
 
113
118
  // Create system instance with emitter, sandbox and config
114
119
  this.system = createSystem(this.emitter, this.sandbox, this.config);
@@ -121,6 +126,8 @@ class TestDriverAgent extends EventEmitter2 {
121
126
  this.config,
122
127
  this.session,
123
128
  () => this.sourceMapper.currentFilePath || this.thisFile,
129
+ this.cliArgs.options.redrawThreshold,
130
+ null, // getDashcamElapsedTime - will be set by SDK when dashcam is available
124
131
  );
125
132
  this.commands = commandsResult.commands;
126
133
  this.redraw = commandsResult.redraw;
@@ -272,6 +279,15 @@ class TestDriverAgent extends EventEmitter2 {
272
279
  // Get error message
273
280
  let eMessage = error.message ? error.message : error;
274
281
 
282
+ // Truncate error message if too long to prevent 400 errors from API
283
+ // Keep first 5000 characters as a reasonable limit for API payloads
284
+ const MAX_ERROR_LENGTH = 5000;
285
+ if (typeof eMessage === "string" && eMessage.length > MAX_ERROR_LENGTH) {
286
+ eMessage =
287
+ eMessage.substring(0, MAX_ERROR_LENGTH) +
288
+ "\n\n[Error message truncated - message was too long]";
289
+ }
290
+
275
291
  // we sanitize the error message to use it as a key in the errorCounts object
276
292
  let safeKey = JSON.stringify(error.message ? error.message : error);
277
293
  this.errorCounts[safeKey] = this.errorCounts[safeKey]
@@ -323,19 +339,40 @@ class TestDriverAgent extends EventEmitter2 {
323
339
  const streamId = `error-${Date.now()}`;
324
340
  this.emitter.emit(events.log.markdown.start, streamId);
325
341
 
326
- let response = await this.sdk.req(
327
- "error",
328
- {
329
- description: eMessage,
330
- markdown,
331
- image,
332
- },
333
- (chunk) => {
334
- if (chunk.type === "data") {
335
- this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
336
- }
337
- },
338
- );
342
+ // Truncate markdown if too long to prevent 400 errors
343
+ const MAX_MARKDOWN_LENGTH = 10000;
344
+ let truncatedMarkdown = markdown;
345
+ if (typeof markdown === "string" && markdown.length > MAX_MARKDOWN_LENGTH) {
346
+ truncatedMarkdown =
347
+ markdown.substring(0, MAX_MARKDOWN_LENGTH) +
348
+ "\n\n[Markdown truncated - content was too long]";
349
+ }
350
+
351
+ let response;
352
+ try {
353
+ response = await this.sdk.req(
354
+ "error",
355
+ {
356
+ description: eMessage,
357
+ markdown: truncatedMarkdown,
358
+ image,
359
+ },
360
+ (chunk) => {
361
+ if (chunk.type === "data") {
362
+ this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
363
+ }
364
+ },
365
+ );
366
+ } catch (apiError) {
367
+ // If the error API call itself fails, prevent infinite loop
368
+ // by not retrying and instead treating as fatal
369
+ this.emitter.emit(
370
+ events.log.error,
371
+ theme.red(`Failed to get AI error resolution: ${apiError.message}`),
372
+ );
373
+ this.emitter.emit(events.log.log, "Original error: " + eMessage);
374
+ return await this.dieOnFatal(error);
375
+ }
339
376
 
340
377
  this.emitter.emit(events.log.markdown.end, streamId);
341
378
 
@@ -835,6 +872,7 @@ commands:
835
872
  dry = false,
836
873
  validateAndLoop = false,
837
874
  shouldSave = true,
875
+ useCache = true,
838
876
  ) {
839
877
  // Check if execution has been stopped
840
878
  if (this.stopped) {
@@ -853,6 +891,49 @@ commands:
853
891
 
854
892
  this.tasks.push(currentTask);
855
893
 
894
+ // Check cache first (if enabled via parameter)
895
+ const cachedYaml = useCache ? promptCache.readCache(currentTask) : null;
896
+
897
+ if (cachedYaml) {
898
+ // Cache hit - load and execute the cached YAML file
899
+ this.emitter.emit(
900
+ events.log.debug,
901
+ `Using cached response for prompt: "${currentTask}"`,
902
+ );
903
+ this.emitter.emit(events.log.log, theme.dim("(using cached response)"));
904
+
905
+ try {
906
+ // Load the YAML using hydrateFromYML
907
+ const parsed = await generator.hydrateFromYML(
908
+ cachedYaml,
909
+ this.sessionInstance,
910
+ );
911
+
912
+ // Execute the commands from the first step
913
+ if (parsed.steps && parsed.steps.length > 0) {
914
+ const step = parsed.steps[0];
915
+ if (step.commands) {
916
+ await this.executeCommands(
917
+ step.commands,
918
+ 0,
919
+ false,
920
+ dry,
921
+ shouldSave,
922
+ );
923
+ }
924
+ }
925
+ } catch (err) {
926
+ this.emitter.emit(
927
+ events.log.debug,
928
+ `Error loading cached YAML: ${err.message}, falling back to API`,
929
+ );
930
+ // Fall through to make API call if cache is invalid
931
+ }
932
+
933
+ return;
934
+ }
935
+
936
+ // Cache miss - call the API
856
937
  this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
857
938
 
858
939
  this.lastScreenshot = await this.system.captureScreenBase64();
@@ -877,7 +958,49 @@ commands:
877
958
 
878
959
  this.emitter.emit(events.log.markdown.end, streamId);
879
960
 
880
- if (message) {
961
+ if (message && message.data) {
962
+ // Save the YAML to cache (if enabled)
963
+ if (useCache) {
964
+ try {
965
+ // Extract YAML code blocks from the markdown response
966
+ const codeblocks = await this.parser.findCodeBlocks(message.data);
967
+ if (codeblocks && codeblocks.length > 0) {
968
+ // Parse commands from all code blocks
969
+ const allCommands = [];
970
+ for (const block of codeblocks) {
971
+ const commands = await this.parser.getCommands(block);
972
+ allCommands.push(...commands);
973
+ }
974
+
975
+ // Create a proper step with prompt
976
+ const step = {
977
+ prompt: currentTask,
978
+ commands: allCommands,
979
+ };
980
+
981
+ // Use dumpToYML to create a valid testdriver yaml file
982
+ const yamlContent = await generator.dumpToYML(
983
+ [step],
984
+ this.sessionInstance,
985
+ );
986
+
987
+ const cachePath = promptCache.writeCache(currentTask, yamlContent);
988
+ if (cachePath) {
989
+ this.emitter.emit(
990
+ events.log.debug,
991
+ `Cached YAML saved to: ${cachePath}`,
992
+ );
993
+ }
994
+ }
995
+ } catch (err) {
996
+ // If we can't extract YAML, just skip caching
997
+ this.emitter.emit(
998
+ events.log.debug,
999
+ `Could not cache response: ${err.message}`,
1000
+ );
1001
+ }
1002
+ }
1003
+
881
1004
  await this.aiExecute(message.data, validateAndLoop, dry, shouldSave);
882
1005
  this.emitter.emit(
883
1006
  events.log.debug,
@@ -1588,12 +1711,28 @@ ${regression}
1588
1711
  this.emitter.emit(events.log.log, `${inputFile} (end)`);
1589
1712
  }
1590
1713
 
1714
+ // Returns the path to the sandbox file (project-local first, then global)
1715
+ getSandboxFilePath(preferProjectLocal = true) {
1716
+ const projectLocalFile = path.join(process.cwd(), ".testdriver-sandbox.json");
1717
+ const globalFile = path.join(os.homedir(), ".testdriverai-last-sandbox");
1718
+
1719
+ if (preferProjectLocal) {
1720
+ // For reading: check project-local first, then global
1721
+ if (fs.existsSync(projectLocalFile)) {
1722
+ return projectLocalFile;
1723
+ }
1724
+ if (fs.existsSync(globalFile)) {
1725
+ return globalFile;
1726
+ }
1727
+ // For writing: default to project-local
1728
+ return projectLocalFile;
1729
+ }
1730
+ return globalFile;
1731
+ }
1732
+
1591
1733
  // Returns sandboxId to use (either from file if recent, or null)
1592
1734
  getRecentSandboxId() {
1593
- const lastSandboxFile = path.join(
1594
- os.homedir(),
1595
- ".testdriverai-last-sandbox",
1596
- );
1735
+ const lastSandboxFile = this.getSandboxFilePath(true);
1597
1736
 
1598
1737
  if (fs.existsSync(lastSandboxFile)) {
1599
1738
  try {
@@ -1619,7 +1758,8 @@ ${regression}
1619
1758
  const storedInstance = sandboxInfo.instanceType || null;
1620
1759
 
1621
1760
  if (currentAmi === storedAmi && currentInstance === storedInstance) {
1622
- return sandboxInfo.instanceId;
1761
+ // Return sandboxId (new format) or instanceId (old format for backwards compatibility)
1762
+ return sandboxInfo.sandboxId || sandboxInfo.instanceId;
1623
1763
  } else {
1624
1764
  this.emitter.emit(
1625
1765
  events.log.log,
@@ -1637,19 +1777,18 @@ ${regression}
1637
1777
  return null;
1638
1778
  }
1639
1779
 
1640
- saveLastSandboxId(instanceId) {
1641
- const lastSandboxFile = path.join(
1642
- os.homedir(),
1643
- ".testdriverai-last-sandbox",
1644
- );
1780
+ saveLastSandboxId(sandboxId, osType = "linux") {
1781
+ // Save to project-local .testdriver-sandbox.json
1782
+ const projectLocalFile = path.join(process.cwd(), ".testdriver-sandbox.json");
1645
1783
  try {
1646
1784
  const sandboxInfo = {
1647
- instanceId: instanceId,
1785
+ sandboxId: sandboxId,
1786
+ os: osType,
1648
1787
  ami: this.sandboxAmi || null,
1649
1788
  instanceType: this.sandboxInstance || null,
1650
1789
  timestamp: new Date().toISOString(),
1651
1790
  };
1652
- fs.writeFileSync(lastSandboxFile, JSON.stringify(sandboxInfo), {
1791
+ fs.writeFileSync(projectLocalFile, JSON.stringify(sandboxInfo, null, 2), {
1653
1792
  encoding: "utf-8",
1654
1793
  });
1655
1794
  } catch {
@@ -1658,13 +1797,16 @@ ${regression}
1658
1797
  }
1659
1798
 
1660
1799
  clearRecentSandboxId() {
1661
- const lastSandboxFile = path.join(
1662
- os.homedir(),
1663
- ".testdriverai-last-sandbox",
1664
- );
1800
+ // Clear project-local file first, then global
1801
+ const projectLocalFile = path.join(process.cwd(), ".testdriver-sandbox.json");
1802
+ const globalFile = path.join(os.homedir(), ".testdriverai-last-sandbox");
1803
+
1665
1804
  try {
1666
- if (fs.existsSync(lastSandboxFile)) {
1667
- fs.unlinkSync(lastSandboxFile);
1805
+ if (fs.existsSync(projectLocalFile)) {
1806
+ fs.unlinkSync(projectLocalFile);
1807
+ }
1808
+ if (fs.existsSync(globalFile)) {
1809
+ fs.unlinkSync(globalFile);
1668
1810
  }
1669
1811
  } catch {
1670
1812
  // ignore errors
@@ -1682,6 +1824,11 @@ ${regression}
1682
1824
 
1683
1825
  let { headless = false, heal, new: createNew = false } = options;
1684
1826
 
1827
+ // Prioritize this.newSandbox flag if it's set
1828
+ if (this.newSandbox) {
1829
+ createNew = true;
1830
+ }
1831
+
1685
1832
  // If CI environment variable is true, always create a new sandbox
1686
1833
  if (this.config.CI) {
1687
1834
  createNew = true;
@@ -1696,11 +1843,18 @@ ${regression}
1696
1843
  // If createNew flag is set, clear the recent sandbox file to force creating a new sandbox
1697
1844
  if (createNew) {
1698
1845
  this.clearRecentSandboxId();
1699
- if (!this.config.CI) {
1846
+ // Also clear this.sandboxId to prevent reconnection attempts
1847
+ this.sandboxId = null;
1848
+ if (!this.config.CI && !this.newSandbox) {
1700
1849
  this.emitter.emit(
1701
1850
  events.log.log,
1702
1851
  theme.dim("--`new` flag detected, will create a new sandbox"),
1703
1852
  );
1853
+ } else if (this.newSandbox) {
1854
+ this.emitter.emit(
1855
+ events.log.log,
1856
+ theme.dim("--new-sandbox flag detected, will create a new sandbox"),
1857
+ );
1704
1858
  }
1705
1859
  }
1706
1860
 
@@ -1708,6 +1862,9 @@ ${regression}
1708
1862
  await this.connectToSandboxService();
1709
1863
 
1710
1864
  const recentId = createNew ? null : this.getRecentSandboxId();
1865
+
1866
+ // Track whether we reconnected to an existing sandbox or created a new one
1867
+ this.isReconnected = false;
1711
1868
 
1712
1869
  // Set sandbox ID for reconnection (only if not creating new and recent ID exists)
1713
1870
  if (this.ip) {
@@ -1718,23 +1875,49 @@ ${regression}
1718
1875
  ip: this.ip,
1719
1876
  });
1720
1877
 
1721
- await this.renderSandbox(instance.instance, headless);
1878
+ this.emitter.emit(events.sandbox.connected);
1879
+
1880
+ this.instance = instance.instance;
1881
+ this.isReconnected = false; // Direct IP is considered new
1882
+ await this.renderSandbox(this.instance, headless);
1722
1883
  await this.newSession();
1723
1884
  await this.runLifecycle("provision");
1724
1885
 
1725
1886
  return;
1726
1887
  } else if (!createNew && recentId) {
1888
+ // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1727
1889
  this.emitter.emit(
1728
1890
  events.log.narration,
1729
1891
  theme.dim(`using recent sandbox: ${recentId}`),
1730
1892
  );
1731
1893
  this.sandboxId = recentId;
1732
- } else if (!createNew) {
1894
+
1895
+ try {
1896
+ let instance = await this.connectToSandboxDirect(
1897
+ this.sandboxId,
1898
+ true, // always persist by default
1899
+ );
1900
+
1901
+ this.instance = instance;
1902
+ this.isReconnected = true; // Successfully reconnected to existing sandbox
1903
+
1904
+ await this.renderSandbox(instance, headless);
1905
+ await this.newSession();
1906
+ return;
1907
+ } catch (error) {
1908
+ // If connection fails, fall through to creating a new sandbox
1909
+ this.emitter.emit(
1910
+ events.log.narration,
1911
+ theme.dim(`failed to connect to recent sandbox, creating new one...`),
1912
+ );
1913
+ console.error("Failed to reconnect to sandbox:", error);
1914
+ }
1915
+ } else if (!createNew && !recentId) {
1733
1916
  this.emitter.emit(
1734
1917
  events.log.narration,
1735
1918
  theme.dim(`no recent sandbox found, creating a new one.`),
1736
1919
  );
1737
- } else if (this.sandboxId && !this.config.CI) {
1920
+ } else if (!createNew && this.sandboxId && !this.config.CI) {
1738
1921
  // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1739
1922
  // Attempt to connect to known instance
1740
1923
  this.emitter.emit(
@@ -1749,51 +1932,55 @@ ${regression}
1749
1932
  );
1750
1933
 
1751
1934
  this.instance = instance;
1935
+ this.isReconnected = true; // Successfully reconnected to existing sandbox
1752
1936
 
1753
1937
  await this.renderSandbox(instance, headless);
1754
1938
  await this.newSession();
1755
1939
  return;
1756
1940
  } catch (error) {
1757
- // But if it fails because the machine 404s, fall-through to `createNewSandbox()`
1758
- if (error?.name !== "InvalidInstanceID.NotFound") {
1759
- throw error;
1760
- }
1941
+ // If connection fails, fall through to creating a new sandbox
1942
+ this.emitter.emit(
1943
+ events.log.narration,
1944
+ theme.dim(`failed to connect to recent sandbox, creating new one...`),
1945
+ );
1946
+ console.error("Failed to reconnect to sandbox:", error);
1761
1947
  }
1762
1948
  }
1763
-
1764
- this.emitter.emit(
1765
- events.log.narration,
1766
- theme.dim(`creating new sandbox (can take up to 2 minutes)...`),
1767
- );
1768
- // We don't have resiliency/retries baked in, so let's at least give it 1 attempt
1769
- // to see if that fixes the issue.
1770
- let newSandbox = await this.createNewSandbox().catch(() => {
1949
+
1950
+ // Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
1951
+ if (!this.instance) {
1952
+ this.isReconnected = false; // Creating new sandbox
1771
1953
  this.emitter.emit(
1772
1954
  events.log.narration,
1773
- theme.dim(`double-checking sandbox availability`),
1955
+ theme.dim(`creating new sandbox...`),
1774
1956
  );
1957
+ // We don't have resiliency/retries baked in, so let's at least give it 1 attempt
1958
+ // to see if that fixes the issue.
1959
+ let newSandbox = await this.createNewSandbox().catch(() => {
1960
+ this.emitter.emit(
1961
+ events.log.narration,
1962
+ theme.dim(`double-checking sandbox availability`),
1963
+ );
1964
+ return this.createNewSandbox();
1965
+ });
1775
1966
 
1776
- return this.createNewSandbox();
1777
- });
1778
-
1779
- console.log("New sandbox created:", newSandbox);
1780
-
1781
- let data = {
1782
- resolution: this.config.TD_RESOLUTION,
1783
- url: newSandbox.url,
1784
- };
1785
-
1786
- const encodedData = Buffer.from(JSON.stringify(data)).toString('base64');
1787
-
1788
- // Use the debugger URL instead of the VNC URL
1789
- const urlToOpen = `${this.debuggerUrl}?data=${encodedData}`;
1790
-
1791
- this.emitter.emit(events.showWindow, urlToOpen);
1792
-
1793
- await this.newSession();
1794
- await this.runLifecycle("provision");
1967
+ // Extract the sandbox ID from the newly created sandbox
1968
+ this.sandboxId = newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1969
+
1970
+ // Use the configured sandbox OS type
1971
+ this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
1972
+
1973
+ let instance = await this.connectToSandboxDirect(
1974
+ this.sandboxId,
1975
+ true, // always persist by default
1976
+ );
1977
+ this.instance = instance;
1978
+ await this.renderSandbox(instance, headless);
1979
+ await this.newSession();
1980
+ await this.runLifecycle("provision");
1795
1981
 
1796
- console.log("provision run");
1982
+ console.log("provision run");
1983
+ }
1797
1984
  }
1798
1985
 
1799
1986
  async start() {
@@ -1913,13 +2100,27 @@ ${regression}
1913
2100
  }
1914
2101
 
1915
2102
  async renderSandbox(instance, headless = false) {
2103
+ console.log("renderSandbox", instance);
2104
+
1916
2105
  if (!headless) {
1917
- let url =
1918
- "http://" +
1919
- instance.ip +
1920
- ":" +
1921
- instance.vncPort +
1922
- "/vnc_lite.html?token=V3b8wG9";
2106
+ let url;
2107
+
2108
+ // If the instance already has a URL (from reconnection), use it
2109
+ if (instance.url) {
2110
+ url = instance.url;
2111
+ } else if (instance.ip || instance.publicIp) {
2112
+ // Otherwise construct it from IP and port
2113
+ url =
2114
+ "http://" +
2115
+ (instance.ip || instance.publicIp) +
2116
+ ":" +
2117
+ (instance.vncPort || "5800") +
2118
+ "/vnc_lite.html?token=V3b8wG9";
2119
+ } else {
2120
+ // If we don't have URL or IP, we can't render
2121
+ console.warn("renderSandbox: Missing URL and IP in instance", instance);
2122
+ return;
2123
+ }
1923
2124
 
1924
2125
  let data = {
1925
2126
  resolution: this.config.TD_RESOLUTION,
@@ -1927,7 +2128,8 @@ ${regression}
1927
2128
  token: "V3b8wG9",
1928
2129
  };
1929
2130
 
1930
- const encodedData = encodeURIComponent(JSON.stringify(data));
2131
+ // Base64 encode the data (the debugger expects base64, not URL encoding)
2132
+ const encodedData = Buffer.from(JSON.stringify(data)).toString("base64");
1931
2133
 
1932
2134
  // Use the debugger URL instead of the VNC URL
1933
2135
  const urlToOpen = `${this.debuggerUrl}?data=${encodedData}`;
@@ -1965,16 +2167,26 @@ Please check your network connection, TD_API_KEY, or the service status.`,
1965
2167
 
1966
2168
  async connectToSandboxDirect(sandboxId, persist = false) {
1967
2169
  this.emitter.emit(events.log.narration, theme.dim(`connecting...`));
1968
- let instance = await this.sandbox.connect(sandboxId, persist);
1969
- return instance;
2170
+ let reply = await this.sandbox.connect(sandboxId, persist);
2171
+
2172
+ // reply includes { success, url, sandbox: {...} }
2173
+ // For renderSandbox, we need the sandbox object with url merged in
2174
+ const sandbox = reply.sandbox || {};
2175
+
2176
+ // If reply has a URL at top level, merge it into the sandbox object
2177
+ if (reply.url && !sandbox.url) {
2178
+ sandbox.url = reply.url;
2179
+ }
2180
+
2181
+ return sandbox;
1970
2182
  }
1971
2183
 
1972
2184
  async createNewSandbox() {
1973
2185
  const sandboxConfig = {
1974
2186
  type: "create",
1975
- os: "linux",
1976
2187
  resolution: this.config.TD_RESOLUTION,
1977
2188
  ci: this.config.CI,
2189
+ os: this.sandboxOs || "windows",
1978
2190
  };
1979
2191
 
1980
2192
  // Add AMI and instance type if specified
@@ -1985,11 +2197,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
1985
2197
  sandboxConfig.instanceType = this.sandboxInstance;
1986
2198
  }
1987
2199
 
1988
- console.log("sending create");
1989
-
1990
2200
  let instance = await this.sandbox.send(sandboxConfig);
1991
2201
 
1992
- console.log("instance created", instance);
2202
+ // Save the sandbox ID for reconnection with the correct OS type
2203
+ if (instance.sandbox && instance.sandbox.sandboxId) {
2204
+ this.saveLastSandboxId(instance.sandbox.sandboxId, this.sandboxOs);
2205
+ } else if (instance.sandbox && instance.sandbox.instanceId) {
2206
+ this.saveLastSandboxId(instance.sandbox.instanceId, this.sandboxOs);
2207
+ }
1993
2208
 
1994
2209
  return instance;
1995
2210
  }
@@ -66,6 +66,11 @@ function createCommandDefinitions(agent) {
66
66
  description: "Generate JUnit XML test report to specified file",
67
67
  default: false,
68
68
  }),
69
+ os: Flags.string({
70
+ description: "Operating system for the sandbox (windows or linux)",
71
+ options: ["windows", "linux"],
72
+ default: "windows",
73
+ }),
69
74
  },
70
75
  handler: async (args, flags) => {
71
76
  // Use --path flag if provided, otherwise fall back to args.file
@@ -140,6 +145,11 @@ function createCommandDefinitions(agent) {
140
145
  summary: Flags.string({
141
146
  description: "Specify output file for summarize results",
142
147
  }),
148
+ os: Flags.string({
149
+ description: "Operating system for the sandbox (windows or linux)",
150
+ options: ["windows", "linux"],
151
+ default: "windows",
152
+ }),
143
153
  },
144
154
  handler: async () => {
145
155
  // Edit mode is handled by the CLI interface via factory.js
@@ -233,6 +243,11 @@ function createCommandDefinitions(agent) {
233
243
  "sandbox-instance": Flags.string({
234
244
  description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
235
245
  }),
246
+ os: Flags.string({
247
+ description: "Operating system for the sandbox (windows or linux)",
248
+ options: ["windows", "linux"],
249
+ default: "windows",
250
+ }),
236
251
  },
237
252
  handler: async (args, flags) => {
238
253
  // Call generate with the count and prompt