testdriverai 6.2.2 → 7.0.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 (264) 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/MIGRATION.md +389 -0
  5. package/PLUGIN_MIGRATION.md +222 -0
  6. package/PROMPT_CACHE.md +200 -0
  7. package/SDK_LOGGING.md +222 -0
  8. package/SDK_MIGRATION.md +474 -0
  9. package/SDK_README.md +1122 -0
  10. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  11. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  12. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  13. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  14. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  15. package/_testdriver/lifecycle/prerun.yaml +15 -0
  16. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  17. package/agent/index.js +258 -68
  18. package/agent/interface.js +15 -0
  19. package/agent/lib/cache.js +142 -0
  20. package/agent/lib/commander.js +1 -39
  21. package/agent/lib/commands.js +143 -188
  22. package/agent/lib/redraw.js +6 -3
  23. package/agent/lib/sandbox.js +19 -5
  24. package/agent/lib/sdk.js +1 -0
  25. package/agent/lib/system.js +0 -3
  26. package/agent/lib/validation.js +1 -7
  27. package/debug-locate-response.js +82 -0
  28. package/debug-screenshot-1763401388589.png +0 -0
  29. package/debugger/index.html +15 -4
  30. package/docs/ARCHITECTURE.md +424 -0
  31. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  32. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  33. package/docs/SDK_AWESOME_LOGS.md +468 -0
  34. package/docs/TEST_RECORDING.md +388 -0
  35. package/docs/docs.json +232 -152
  36. package/docs/sdk-browser-rendering.md +167 -0
  37. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  38. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  39. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  40. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  41. package/docs/v6/overview/comparison.mdx +101 -0
  42. package/docs/v7/README.md +135 -0
  43. package/docs/v7/api/ai.mdx +205 -0
  44. package/docs/v7/api/assert.mdx +285 -0
  45. package/docs/v7/api/assertions.mdx +403 -0
  46. package/docs/v7/api/click.mdx +287 -0
  47. package/docs/v7/api/client.mdx +322 -0
  48. package/docs/v7/api/elements.mdx +479 -0
  49. package/docs/v7/api/exec.mdx +346 -0
  50. package/docs/v7/api/find.mdx +316 -0
  51. package/docs/v7/api/focusApplication.mdx +294 -0
  52. package/docs/v7/api/hover.mdx +279 -0
  53. package/docs/v7/api/pressKeys.mdx +349 -0
  54. package/docs/v7/api/sandbox.mdx +404 -0
  55. package/docs/v7/api/scroll.mdx +300 -0
  56. package/docs/v7/api/type.mdx +314 -0
  57. package/docs/v7/commands/assert.mdx +45 -0
  58. package/docs/v7/commands/exec.mdx +282 -0
  59. package/docs/v7/commands/focus-application.mdx +44 -0
  60. package/docs/v7/commands/hover-image.mdx +69 -0
  61. package/docs/v7/commands/hover-text.mdx +47 -0
  62. package/docs/v7/commands/if.mdx +53 -0
  63. package/docs/v7/commands/match-image.mdx +67 -0
  64. package/docs/v7/commands/press-keys.mdx +87 -0
  65. package/docs/v7/commands/remember.mdx +49 -0
  66. package/docs/v7/commands/run.mdx +44 -0
  67. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  68. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  69. package/docs/v7/commands/scroll.mdx +69 -0
  70. package/docs/v7/commands/type.mdx +45 -0
  71. package/docs/v7/commands/wait-for-image.mdx +54 -0
  72. package/docs/v7/commands/wait-for-text.mdx +48 -0
  73. package/docs/v7/commands/wait.mdx +45 -0
  74. package/docs/v7/getting-started/quickstart.mdx +199 -0
  75. package/docs/v7/guides/migration.mdx +562 -0
  76. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  77. package/docs/v7/playwright.mdx +342 -0
  78. package/eslint.config.js +19 -1
  79. package/examples/run-tests-with-recording.sh +70 -0
  80. package/examples/screenshot-example.js +63 -0
  81. package/examples/sdk-awesome-logs-demo.js +177 -0
  82. package/examples/sdk-cache-thresholds.js +96 -0
  83. package/examples/sdk-element-properties.js +155 -0
  84. package/examples/sdk-simple-example.js +65 -0
  85. package/examples/test-recording-example.test.js +166 -0
  86. package/interfaces/cli/lib/base.js +10 -4
  87. package/interfaces/logger.js +2 -1
  88. package/interfaces/shared-test-state.mjs +69 -0
  89. package/interfaces/vitest-plugin.mjs +744 -0
  90. package/mcp-server/AI_GUIDELINES.md +57 -0
  91. package/package.json +18 -5
  92. package/schema.json +8 -29
  93. package/scripts/view-test-results.mjs +96 -0
  94. package/sdk-log-formatter.js +714 -0
  95. package/sdk.d.ts +735 -0
  96. package/sdk.js +1906 -0
  97. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  98. package/setup/aws/cloudformation.yaml +9 -2
  99. package/test/mcp-example-test.yaml +27 -0
  100. package/test-find-api.js +73 -0
  101. package/test-prompt-cache.js +96 -0
  102. package/test-sandbox-render.js +28 -0
  103. package/test-sdk-methods.js +15 -0
  104. package/test-sdk-refactor.js +53 -0
  105. package/test-stack-trace.mjs +57 -0
  106. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  107. package/testdriver/acceptance-sdk/README.md +128 -0
  108. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  109. package/testdriver/acceptance-sdk/assert.test.mjs +44 -0
  110. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +70 -0
  111. package/testdriver/acceptance-sdk/element-not-found.test.mjs +38 -0
  112. package/testdriver/acceptance-sdk/exec-js.test.mjs +55 -0
  113. package/testdriver/acceptance-sdk/exec-output.test.mjs +71 -0
  114. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +69 -0
  115. package/testdriver/acceptance-sdk/focus-window.test.mjs +48 -0
  116. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +41 -0
  117. package/testdriver/acceptance-sdk/hover-image.test.mjs +43 -0
  118. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +50 -0
  119. package/testdriver/acceptance-sdk/hover-text.test.mjs +41 -0
  120. package/testdriver/acceptance-sdk/match-image.test.mjs +48 -0
  121. package/testdriver/acceptance-sdk/press-keys.test.mjs +64 -0
  122. package/testdriver/acceptance-sdk/prompt.test.mjs +45 -0
  123. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +52 -0
  124. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +51 -0
  125. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +42 -0
  126. package/testdriver/acceptance-sdk/scroll.test.mjs +50 -0
  127. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  128. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +239 -0
  129. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +648 -0
  130. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  131. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  132. package/testdriver/acceptance-sdk/type.test.mjs +84 -0
  133. package/verify-element-api.js +89 -0
  134. package/verify-types.js +0 -0
  135. package/vitest.config.example.js +19 -0
  136. package/vitest.config.mjs +65 -0
  137. package/vitest.config.mjs.bak +44 -0
  138. package/.github/workflows/acceptance-v6.yml +0 -169
  139. package/docs/overview/comparison.mdx +0 -82
  140. package/testdriver/lifecycle/prerun.yaml +0 -17
  141. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  142. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  143. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  144. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  145. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  146. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  147. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  148. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  149. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  150. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  151. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  152. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  153. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  154. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  155. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  156. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  157. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  158. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  159. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  160. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  161. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  162. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  163. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  164. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  165. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  166. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  167. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  168. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  169. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  170. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  171. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  172. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  173. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  174. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  175. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  176. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  177. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  178. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  179. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  180. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  181. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  182. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  183. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  184. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  185. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  186. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  187. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  188. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  189. /package/docs/{account → v6/account}/projects.mdx +0 -0
  190. /package/docs/{account → v6/account}/team.mdx +0 -0
  191. /package/docs/{action → v6/action}/ami.mdx +0 -0
  192. /package/docs/{action → v6/action}/performance.mdx +0 -0
  193. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  194. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  195. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  196. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  197. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  198. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  199. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  200. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  201. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  202. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  203. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  204. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  205. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  206. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  207. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  208. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  209. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  210. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  211. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  212. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  213. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  214. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  215. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  216. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  217. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  218. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  219. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  220. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  221. /package/docs/{features → v6/features}/generation.mdx +0 -0
  222. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  223. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  224. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  225. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  226. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  227. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  228. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  229. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  230. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  231. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  232. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  233. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  234. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  235. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  236. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  237. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  238. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  239. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  240. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  241. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  242. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  243. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  244. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  245. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  246. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  247. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  248. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  249. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  250. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  251. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  252. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  253. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  254. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  255. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  256. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  257. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  258. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  259. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  260. /package/docs/{security → v6/security}/action.mdx +0 -0
  261. /package/docs/{security → v6/security}/agent.mdx +0 -0
  262. /package/docs/{security → v6/security}/platform.mdx +0 -0
  263. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  264. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -37,11 +37,11 @@ steps:
37
37
  - command: hover-text
38
38
  text: New Text Document
39
39
  description: new text document icon in the center of the desktop
40
- action: drag-start
40
+ action: mouseDown
41
41
  - command: hover-text
42
42
  text: Recycle Bin
43
43
  description: recycle bin icon in the top left corner of the desktop
44
- action: drag-end
44
+ action: mouseUp
45
45
 
46
46
  - prompt: '"New Text Document" icon is not on the Desktop'
47
47
  commands:
@@ -1,4 +1,4 @@
1
- version: 5.7.7
1
+ perversion: 5.7.7
2
2
  session: 682f6071811bd5a322c0e6dd
3
3
  steps:
4
4
  - prompt: focus chrome
@@ -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,7 +103,7 @@ 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
@@ -110,6 +112,9 @@ class TestDriverAgent extends EventEmitter2 {
110
112
  // Create sandbox instance with this agent's emitter and analytics
111
113
  this.sandbox = createSandbox(this.emitter, this.analytics);
112
114
 
115
+ // Set the OS for the sandbox to use
116
+ this.sandbox.os = this.sandboxOs;
117
+
113
118
  // Create system instance with emitter, sandbox and config
114
119
  this.system = createSystem(this.emitter, this.sandbox, this.config);
115
120
 
@@ -121,6 +126,7 @@ class TestDriverAgent extends EventEmitter2 {
121
126
  this.config,
122
127
  this.session,
123
128
  () => this.sourceMapper.currentFilePath || this.thisFile,
129
+ this.cliArgs.options.redrawThreshold,
124
130
  );
125
131
  this.commands = commandsResult.commands;
126
132
  this.redraw = commandsResult.redraw;
@@ -272,6 +278,15 @@ class TestDriverAgent extends EventEmitter2 {
272
278
  // Get error message
273
279
  let eMessage = error.message ? error.message : error;
274
280
 
281
+ // Truncate error message if too long to prevent 400 errors from API
282
+ // Keep first 5000 characters as a reasonable limit for API payloads
283
+ const MAX_ERROR_LENGTH = 5000;
284
+ if (typeof eMessage === "string" && eMessage.length > MAX_ERROR_LENGTH) {
285
+ eMessage =
286
+ eMessage.substring(0, MAX_ERROR_LENGTH) +
287
+ "\n\n[Error message truncated - message was too long]";
288
+ }
289
+
275
290
  // we sanitize the error message to use it as a key in the errorCounts object
276
291
  let safeKey = JSON.stringify(error.message ? error.message : error);
277
292
  this.errorCounts[safeKey] = this.errorCounts[safeKey]
@@ -323,19 +338,40 @@ class TestDriverAgent extends EventEmitter2 {
323
338
  const streamId = `error-${Date.now()}`;
324
339
  this.emitter.emit(events.log.markdown.start, streamId);
325
340
 
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
- );
341
+ // Truncate markdown if too long to prevent 400 errors
342
+ const MAX_MARKDOWN_LENGTH = 10000;
343
+ let truncatedMarkdown = markdown;
344
+ if (typeof markdown === "string" && markdown.length > MAX_MARKDOWN_LENGTH) {
345
+ truncatedMarkdown =
346
+ markdown.substring(0, MAX_MARKDOWN_LENGTH) +
347
+ "\n\n[Markdown truncated - content was too long]";
348
+ }
349
+
350
+ let response;
351
+ try {
352
+ response = await this.sdk.req(
353
+ "error",
354
+ {
355
+ description: eMessage,
356
+ markdown: truncatedMarkdown,
357
+ image,
358
+ },
359
+ (chunk) => {
360
+ if (chunk.type === "data") {
361
+ this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
362
+ }
363
+ },
364
+ );
365
+ } catch (apiError) {
366
+ // If the error API call itself fails, prevent infinite loop
367
+ // by not retrying and instead treating as fatal
368
+ this.emitter.emit(
369
+ events.log.error,
370
+ theme.red(`Failed to get AI error resolution: ${apiError.message}`),
371
+ );
372
+ this.emitter.emit(events.log.log, "Original error: " + eMessage);
373
+ return await this.dieOnFatal(error);
374
+ }
339
375
 
340
376
  this.emitter.emit(events.log.markdown.end, streamId);
341
377
 
@@ -835,6 +871,7 @@ commands:
835
871
  dry = false,
836
872
  validateAndLoop = false,
837
873
  shouldSave = true,
874
+ useCache = true,
838
875
  ) {
839
876
  // Check if execution has been stopped
840
877
  if (this.stopped) {
@@ -853,6 +890,49 @@ commands:
853
890
 
854
891
  this.tasks.push(currentTask);
855
892
 
893
+ // Check cache first (if enabled via parameter)
894
+ const cachedYaml = useCache ? promptCache.readCache(currentTask) : null;
895
+
896
+ if (cachedYaml) {
897
+ // Cache hit - load and execute the cached YAML file
898
+ this.emitter.emit(
899
+ events.log.debug,
900
+ `Using cached response for prompt: "${currentTask}"`,
901
+ );
902
+ this.emitter.emit(events.log.log, theme.dim("(using cached response)"));
903
+
904
+ try {
905
+ // Load the YAML using hydrateFromYML
906
+ const parsed = await generator.hydrateFromYML(
907
+ cachedYaml,
908
+ this.sessionInstance,
909
+ );
910
+
911
+ // Execute the commands from the first step
912
+ if (parsed.steps && parsed.steps.length > 0) {
913
+ const step = parsed.steps[0];
914
+ if (step.commands) {
915
+ await this.executeCommands(
916
+ step.commands,
917
+ 0,
918
+ false,
919
+ dry,
920
+ shouldSave,
921
+ );
922
+ }
923
+ }
924
+ } catch (err) {
925
+ this.emitter.emit(
926
+ events.log.debug,
927
+ `Error loading cached YAML: ${err.message}, falling back to API`,
928
+ );
929
+ // Fall through to make API call if cache is invalid
930
+ }
931
+
932
+ return;
933
+ }
934
+
935
+ // Cache miss - call the API
856
936
  this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
857
937
 
858
938
  this.lastScreenshot = await this.system.captureScreenBase64();
@@ -877,7 +957,49 @@ commands:
877
957
 
878
958
  this.emitter.emit(events.log.markdown.end, streamId);
879
959
 
880
- if (message) {
960
+ if (message && message.data) {
961
+ // Save the YAML to cache (if enabled)
962
+ if (useCache) {
963
+ try {
964
+ // Extract YAML code blocks from the markdown response
965
+ const codeblocks = await this.parser.findCodeBlocks(message.data);
966
+ if (codeblocks && codeblocks.length > 0) {
967
+ // Parse commands from all code blocks
968
+ const allCommands = [];
969
+ for (const block of codeblocks) {
970
+ const commands = await this.parser.getCommands(block);
971
+ allCommands.push(...commands);
972
+ }
973
+
974
+ // Create a proper step with prompt
975
+ const step = {
976
+ prompt: currentTask,
977
+ commands: allCommands,
978
+ };
979
+
980
+ // Use dumpToYML to create a valid testdriver yaml file
981
+ const yamlContent = await generator.dumpToYML(
982
+ [step],
983
+ this.sessionInstance,
984
+ );
985
+
986
+ const cachePath = promptCache.writeCache(currentTask, yamlContent);
987
+ if (cachePath) {
988
+ this.emitter.emit(
989
+ events.log.debug,
990
+ `Cached YAML saved to: ${cachePath}`,
991
+ );
992
+ }
993
+ }
994
+ } catch (err) {
995
+ // If we can't extract YAML, just skip caching
996
+ this.emitter.emit(
997
+ events.log.debug,
998
+ `Could not cache response: ${err.message}`,
999
+ );
1000
+ }
1001
+ }
1002
+
881
1003
  await this.aiExecute(message.data, validateAndLoop, dry, shouldSave);
882
1004
  this.emitter.emit(
883
1005
  events.log.debug,
@@ -1619,7 +1741,8 @@ ${regression}
1619
1741
  const storedInstance = sandboxInfo.instanceType || null;
1620
1742
 
1621
1743
  if (currentAmi === storedAmi && currentInstance === storedInstance) {
1622
- return sandboxInfo.instanceId;
1744
+ // Return sandboxId (new format) or instanceId (old format for backwards compatibility)
1745
+ return sandboxInfo.sandboxId || sandboxInfo.instanceId;
1623
1746
  } else {
1624
1747
  this.emitter.emit(
1625
1748
  events.log.log,
@@ -1637,14 +1760,15 @@ ${regression}
1637
1760
  return null;
1638
1761
  }
1639
1762
 
1640
- saveLastSandboxId(instanceId) {
1763
+ saveLastSandboxId(sandboxId, osType = "linux") {
1641
1764
  const lastSandboxFile = path.join(
1642
1765
  os.homedir(),
1643
1766
  ".testdriverai-last-sandbox",
1644
1767
  );
1645
1768
  try {
1646
1769
  const sandboxInfo = {
1647
- instanceId: instanceId,
1770
+ sandboxId: sandboxId,
1771
+ os: osType,
1648
1772
  ami: this.sandboxAmi || null,
1649
1773
  instanceType: this.sandboxInstance || null,
1650
1774
  timestamp: new Date().toISOString(),
@@ -1682,6 +1806,11 @@ ${regression}
1682
1806
 
1683
1807
  let { headless = false, heal, new: createNew = false } = options;
1684
1808
 
1809
+ // Prioritize this.newSandbox flag if it's set
1810
+ if (this.newSandbox) {
1811
+ createNew = true;
1812
+ }
1813
+
1685
1814
  // If CI environment variable is true, always create a new sandbox
1686
1815
  if (this.config.CI) {
1687
1816
  createNew = true;
@@ -1696,11 +1825,18 @@ ${regression}
1696
1825
  // If createNew flag is set, clear the recent sandbox file to force creating a new sandbox
1697
1826
  if (createNew) {
1698
1827
  this.clearRecentSandboxId();
1699
- if (!this.config.CI) {
1828
+ // Also clear this.sandboxId to prevent reconnection attempts
1829
+ this.sandboxId = null;
1830
+ if (!this.config.CI && !this.newSandbox) {
1700
1831
  this.emitter.emit(
1701
1832
  events.log.log,
1702
1833
  theme.dim("--`new` flag detected, will create a new sandbox"),
1703
1834
  );
1835
+ } else if (this.newSandbox) {
1836
+ this.emitter.emit(
1837
+ events.log.log,
1838
+ theme.dim("--new-sandbox flag detected, will create a new sandbox"),
1839
+ );
1704
1840
  }
1705
1841
  }
1706
1842
 
@@ -1718,23 +1854,47 @@ ${regression}
1718
1854
  ip: this.ip,
1719
1855
  });
1720
1856
 
1721
- await this.renderSandbox(instance.instance, headless);
1857
+ this.emitter.emit(events.sandbox.connected);
1858
+
1859
+ this.instance = instance.instance;
1860
+ await this.renderSandbox(this.instance, headless);
1722
1861
  await this.newSession();
1723
1862
  await this.runLifecycle("provision");
1724
1863
 
1725
1864
  return;
1726
1865
  } else if (!createNew && recentId) {
1866
+ // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1727
1867
  this.emitter.emit(
1728
1868
  events.log.narration,
1729
1869
  theme.dim(`using recent sandbox: ${recentId}`),
1730
1870
  );
1731
1871
  this.sandboxId = recentId;
1732
- } else if (!createNew) {
1872
+
1873
+ try {
1874
+ let instance = await this.connectToSandboxDirect(
1875
+ this.sandboxId,
1876
+ true, // always persist by default
1877
+ );
1878
+
1879
+ this.instance = instance;
1880
+
1881
+ await this.renderSandbox(instance, headless);
1882
+ await this.newSession();
1883
+ return;
1884
+ } catch (error) {
1885
+ // If connection fails, fall through to creating a new sandbox
1886
+ this.emitter.emit(
1887
+ events.log.narration,
1888
+ theme.dim(`failed to connect to recent sandbox, creating new one...`),
1889
+ );
1890
+ console.error("Failed to reconnect to sandbox:", error);
1891
+ }
1892
+ } else if (!createNew && !recentId) {
1733
1893
  this.emitter.emit(
1734
1894
  events.log.narration,
1735
1895
  theme.dim(`no recent sandbox found, creating a new one.`),
1736
1896
  );
1737
- } else if (this.sandboxId && !this.config.CI) {
1897
+ } else if (!createNew && this.sandboxId && !this.config.CI) {
1738
1898
  // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1739
1899
  // Attempt to connect to known instance
1740
1900
  this.emitter.emit(
@@ -1754,46 +1914,48 @@ ${regression}
1754
1914
  await this.newSession();
1755
1915
  return;
1756
1916
  } 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
- }
1917
+ // If connection fails, fall through to creating a new sandbox
1918
+ this.emitter.emit(
1919
+ events.log.narration,
1920
+ theme.dim(`failed to connect to recent sandbox, creating new one...`),
1921
+ );
1922
+ console.error("Failed to reconnect to sandbox:", error);
1761
1923
  }
1762
1924
  }
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(() => {
1925
+
1926
+ // Create new sandbox (either because createNew is true, or no existing sandbox to connect to)
1927
+ if (!this.instance) {
1771
1928
  this.emitter.emit(
1772
1929
  events.log.narration,
1773
- theme.dim(`double-checking sandbox availability`),
1930
+ theme.dim(`creating new sandbox (can take up to 2 minutes)...`),
1774
1931
  );
1932
+ // We don't have resiliency/retries baked in, so let's at least give it 1 attempt
1933
+ // to see if that fixes the issue.
1934
+ let newSandbox = await this.createNewSandbox().catch(() => {
1935
+ this.emitter.emit(
1936
+ events.log.narration,
1937
+ theme.dim(`double-checking sandbox availability`),
1938
+ );
1939
+ return this.createNewSandbox();
1940
+ });
1775
1941
 
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");
1942
+ // Extract the sandbox ID from the newly created sandbox
1943
+ this.sandboxId = newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1944
+
1945
+ // Use the configured sandbox OS type
1946
+ this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
1947
+
1948
+ let instance = await this.connectToSandboxDirect(
1949
+ this.sandboxId,
1950
+ true, // always persist by default
1951
+ );
1952
+ this.instance = instance;
1953
+ await this.renderSandbox(instance, headless);
1954
+ await this.newSession();
1955
+ await this.runLifecycle("provision");
1795
1956
 
1796
- console.log("provision run");
1957
+ console.log("provision run");
1958
+ }
1797
1959
  }
1798
1960
 
1799
1961
  async start() {
@@ -1913,13 +2075,27 @@ ${regression}
1913
2075
  }
1914
2076
 
1915
2077
  async renderSandbox(instance, headless = false) {
2078
+ console.log("renderSandbox", instance);
2079
+
1916
2080
  if (!headless) {
1917
- let url =
1918
- "http://" +
1919
- instance.ip +
1920
- ":" +
1921
- instance.vncPort +
1922
- "/vnc_lite.html?token=V3b8wG9";
2081
+ let url;
2082
+
2083
+ // If the instance already has a URL (from reconnection), use it
2084
+ if (instance.url) {
2085
+ url = instance.url;
2086
+ } else if (instance.ip || instance.publicIp) {
2087
+ // Otherwise construct it from IP and port
2088
+ url =
2089
+ "http://" +
2090
+ (instance.ip || instance.publicIp) +
2091
+ ":" +
2092
+ (instance.vncPort || "5800") +
2093
+ "/vnc_lite.html?token=V3b8wG9";
2094
+ } else {
2095
+ // If we don't have URL or IP, we can't render
2096
+ console.warn("renderSandbox: Missing URL and IP in instance", instance);
2097
+ return;
2098
+ }
1923
2099
 
1924
2100
  let data = {
1925
2101
  resolution: this.config.TD_RESOLUTION,
@@ -1927,7 +2103,8 @@ ${regression}
1927
2103
  token: "V3b8wG9",
1928
2104
  };
1929
2105
 
1930
- const encodedData = encodeURIComponent(JSON.stringify(data));
2106
+ // Base64 encode the data (the debugger expects base64, not URL encoding)
2107
+ const encodedData = Buffer.from(JSON.stringify(data)).toString("base64");
1931
2108
 
1932
2109
  // Use the debugger URL instead of the VNC URL
1933
2110
  const urlToOpen = `${this.debuggerUrl}?data=${encodedData}`;
@@ -1965,16 +2142,26 @@ Please check your network connection, TD_API_KEY, or the service status.`,
1965
2142
 
1966
2143
  async connectToSandboxDirect(sandboxId, persist = false) {
1967
2144
  this.emitter.emit(events.log.narration, theme.dim(`connecting...`));
1968
- let instance = await this.sandbox.connect(sandboxId, persist);
1969
- return instance;
2145
+ let reply = await this.sandbox.connect(sandboxId, persist);
2146
+
2147
+ // reply includes { success, url, sandbox: {...} }
2148
+ // For renderSandbox, we need the sandbox object with url merged in
2149
+ const sandbox = reply.sandbox || {};
2150
+
2151
+ // If reply has a URL at top level, merge it into the sandbox object
2152
+ if (reply.url && !sandbox.url) {
2153
+ sandbox.url = reply.url;
2154
+ }
2155
+
2156
+ return sandbox;
1970
2157
  }
1971
2158
 
1972
2159
  async createNewSandbox() {
1973
2160
  const sandboxConfig = {
1974
2161
  type: "create",
1975
- os: "linux",
1976
2162
  resolution: this.config.TD_RESOLUTION,
1977
2163
  ci: this.config.CI,
2164
+ os: this.sandboxOs || "windows",
1978
2165
  };
1979
2166
 
1980
2167
  // Add AMI and instance type if specified
@@ -1985,11 +2172,14 @@ Please check your network connection, TD_API_KEY, or the service status.`,
1985
2172
  sandboxConfig.instanceType = this.sandboxInstance;
1986
2173
  }
1987
2174
 
1988
- console.log("sending create");
1989
-
1990
2175
  let instance = await this.sandbox.send(sandboxConfig);
1991
2176
 
1992
- console.log("instance created", instance);
2177
+ // Save the sandbox ID for reconnection with the correct OS type
2178
+ if (instance.sandbox && instance.sandbox.sandboxId) {
2179
+ this.saveLastSandboxId(instance.sandbox.sandboxId, this.sandboxOs);
2180
+ } else if (instance.sandbox && instance.sandbox.instanceId) {
2181
+ this.saveLastSandboxId(instance.sandbox.instanceId, this.sandboxOs);
2182
+ }
1993
2183
 
1994
2184
  return instance;
1995
2185
  }
@@ -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