testdriverai 7.0.0 → 7.1.1

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 (324) hide show
  1. package/.env.example +2 -0
  2. package/.github/workflows/linux-tests.yml +28 -0
  3. package/README.md +126 -0
  4. package/agent/index.js +7 -9
  5. package/agent/interface.js +13 -2
  6. package/agent/lib/commands.js +795 -136
  7. package/agent/lib/redraw.js +124 -39
  8. package/agent/lib/sandbox.js +40 -3
  9. package/agent/lib/sdk.js +21 -0
  10. package/agent/lib/valid-version.js +2 -2
  11. package/debugger/index.html +1 -1
  12. package/docs/docs.json +86 -71
  13. package/docs/guide/best-practices-polling.mdx +154 -0
  14. package/docs/v6/getting-started/self-hosting.mdx +3 -2
  15. package/docs/v7/_drafts/agents.mdx +852 -0
  16. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  17. package/docs/v7/_drafts/best-practices.mdx +486 -0
  18. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  19. package/docs/v7/_drafts/caching-selectors.mdx +400 -0
  20. package/docs/v7/_drafts/caching.mdx +366 -0
  21. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  22. package/docs/v7/_drafts/core.mdx +459 -0
  23. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  24. package/docs/v7/_drafts/debugging.mdx +349 -0
  25. package/docs/v7/_drafts/error-handling.mdx +501 -0
  26. package/docs/v7/_drafts/faq.mdx +393 -0
  27. package/docs/v7/_drafts/hooks.mdx +360 -0
  28. package/docs/v7/_drafts/implementation-plan.mdx +994 -0
  29. package/docs/v7/_drafts/init-command.mdx +95 -0
  30. package/docs/v7/_drafts/optimal-sdk-design.mdx +1348 -0
  31. package/docs/v7/_drafts/performance.mdx +517 -0
  32. package/docs/v7/_drafts/presets.mdx +210 -0
  33. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  34. package/docs/v7/_drafts/provision.mdx +266 -0
  35. package/docs/{QUICK_START_TEST_RECORDING.md → v7/_drafts/quick-start-test-recording.mdx} +3 -3
  36. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  37. package/docs/v7/{guides → _drafts}/self-hosting.mdx +1 -1
  38. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  39. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  40. package/docs/v7/_drafts/vitest.mdx +535 -0
  41. package/docs/v7/api/{ai.mdx → act.mdx} +24 -24
  42. package/docs/v7/api/client.mdx +1 -1
  43. package/docs/v7/api/dashcam.mdx +497 -0
  44. package/docs/v7/api/doubleClick.mdx +102 -0
  45. package/docs/v7/api/elements.mdx +143 -41
  46. package/docs/v7/api/find.mdx +258 -0
  47. package/docs/v7/api/mouseDown.mdx +161 -0
  48. package/docs/v7/api/mouseUp.mdx +164 -0
  49. package/docs/v7/api/rightClick.mdx +123 -0
  50. package/docs/v7/api/type.mdx +51 -7
  51. package/docs/v7/features/ai-native.mdx +427 -0
  52. package/docs/v7/features/easy-to-write.mdx +351 -0
  53. package/docs/v7/features/enterprise.mdx +540 -0
  54. package/docs/v7/features/fast.mdx +424 -0
  55. package/docs/v7/features/observable.mdx +623 -0
  56. package/docs/v7/features/powerful.mdx +531 -0
  57. package/docs/v7/features/scalable.mdx +417 -0
  58. package/docs/v7/features/stable.mdx +514 -0
  59. package/docs/v7/getting-started/configuration.mdx +380 -0
  60. package/docs/v7/getting-started/generating-tests.mdx +525 -0
  61. package/docs/v7/getting-started/installation.mdx +486 -0
  62. package/docs/v7/getting-started/quickstart.mdx +320 -141
  63. package/docs/v7/getting-started/running-and-debugging.mdx +511 -0
  64. package/docs/v7/getting-started/setting-up-in-ci.mdx +612 -0
  65. package/docs/v7/getting-started/writing-tests.mdx +535 -0
  66. package/docs/v7/overview/what-is-testdriver.mdx +398 -0
  67. package/docs/v7/platforms/linux.mdx +308 -0
  68. package/docs/v7/platforms/macos.mdx +433 -0
  69. package/docs/v7/platforms/windows.mdx +430 -0
  70. package/docs/v7/playwright.mdx +3 -3
  71. package/docs/v7/presets/chrome-extension.mdx +223 -0
  72. package/docs/v7/presets/chrome.mdx +303 -0
  73. package/docs/v7/presets/electron.mdx +453 -0
  74. package/docs/v7/presets/vscode.mdx +417 -0
  75. package/docs/v7/presets/webapp.mdx +396 -0
  76. package/examples/run-tests-with-recording.sh +2 -2
  77. package/interfaces/cli/commands/init.js +358 -0
  78. package/interfaces/vitest-plugin.mjs +393 -103
  79. package/lib/core/Dashcam.js +506 -0
  80. package/lib/core/index.d.ts +150 -0
  81. package/lib/core/index.js +12 -0
  82. package/lib/presets/index.mjs +331 -0
  83. package/lib/vitest/hooks.d.ts +119 -0
  84. package/lib/vitest/hooks.mjs +316 -0
  85. package/lib/vitest/setup.mjs +44 -0
  86. package/package.json +13 -3
  87. package/sdk.d.ts +350 -44
  88. package/sdk.js +818 -105
  89. package/{self-hosted.yml → setup/aws/self-hosted.yml} +1 -1
  90. package/test/manual/test-console-logs.test.mjs +42 -0
  91. package/test/manual/test-init.sh +54 -0
  92. package/test/manual/test-provision-auth.mjs +22 -0
  93. package/test/testdriver/assert.test.mjs +41 -0
  94. package/test/testdriver/auto-cache-key-demo.test.mjs +56 -0
  95. package/test/testdriver/chrome-extension.test.mjs +89 -0
  96. package/{testdriver/acceptance-sdk → test/testdriver}/drag-and-drop.test.mjs +7 -19
  97. package/{testdriver/acceptance-sdk → test/testdriver}/element-not-found.test.mjs +6 -19
  98. package/{testdriver/acceptance-sdk → test/testdriver}/exec-js.test.mjs +6 -18
  99. package/{testdriver/acceptance-sdk → test/testdriver}/exec-output.test.mjs +9 -21
  100. package/{testdriver/acceptance-sdk → test/testdriver}/exec-pwsh.test.mjs +14 -26
  101. package/{testdriver/acceptance-sdk → test/testdriver}/focus-window.test.mjs +8 -20
  102. package/{testdriver/acceptance-sdk → test/testdriver}/formatted-logging.test.mjs +5 -20
  103. package/{testdriver/acceptance-sdk → test/testdriver}/hover-image.test.mjs +10 -19
  104. package/{testdriver/acceptance-sdk → test/testdriver}/hover-text-with-description.test.mjs +7 -19
  105. package/{testdriver/acceptance-sdk → test/testdriver}/hover-text.test.mjs +5 -19
  106. package/{testdriver/acceptance-sdk → test/testdriver}/match-image.test.mjs +7 -19
  107. package/{testdriver/acceptance-sdk → test/testdriver}/press-keys.test.mjs +5 -19
  108. package/{testdriver/acceptance-sdk → test/testdriver}/prompt.test.mjs +7 -19
  109. package/{testdriver/acceptance-sdk → test/testdriver}/scroll-keyboard.test.mjs +6 -20
  110. package/{testdriver/acceptance-sdk → test/testdriver}/scroll-until-image.test.mjs +6 -18
  111. package/test/testdriver/scroll-until-text.test.mjs +28 -0
  112. package/{testdriver/acceptance-sdk → test/testdriver}/scroll.test.mjs +12 -21
  113. package/test/testdriver/setup/lifecycleHelpers.mjs +262 -0
  114. package/{testdriver/acceptance-sdk → test/testdriver}/setup/testHelpers.mjs +25 -20
  115. package/test/testdriver/type.test.mjs +45 -0
  116. package/vitest.config.mjs +11 -56
  117. package/.github/dependabot.yml +0 -11
  118. package/.github/workflows/acceptance-linux.yml +0 -75
  119. package/.github/workflows/acceptance-sdk-tests.yml +0 -133
  120. package/.github/workflows/acceptance-tests.yml +0 -130
  121. package/.github/workflows/lint.yml +0 -27
  122. package/.github/workflows/publish-canary.yml +0 -40
  123. package/.github/workflows/publish-latest.yml +0 -61
  124. package/.github/workflows/test-install.yml +0 -29
  125. package/.vscode/extensions.json +0 -3
  126. package/.vscode/launch.json +0 -22
  127. package/.vscode/mcp.json +0 -9
  128. package/.vscode/settings.json +0 -14
  129. package/CODEOWNERS +0 -3
  130. package/MIGRATION.md +0 -389
  131. package/SDK_README.md +0 -1122
  132. package/_testdriver/acceptance/assert.yaml +0 -7
  133. package/_testdriver/acceptance/dashcam.yaml +0 -9
  134. package/_testdriver/acceptance/drag-and-drop.yaml +0 -49
  135. package/_testdriver/acceptance/embed.yaml +0 -9
  136. package/_testdriver/acceptance/exec-js.yaml +0 -29
  137. package/_testdriver/acceptance/exec-output.yaml +0 -43
  138. package/_testdriver/acceptance/exec-shell.yaml +0 -40
  139. package/_testdriver/acceptance/focus-window.yaml +0 -16
  140. package/_testdriver/acceptance/hover-image.yaml +0 -18
  141. package/_testdriver/acceptance/hover-text-with-description.yaml +0 -29
  142. package/_testdriver/acceptance/hover-text.yaml +0 -14
  143. package/_testdriver/acceptance/if-else.yaml +0 -31
  144. package/_testdriver/acceptance/match-image.yaml +0 -15
  145. package/_testdriver/acceptance/press-keys.yaml +0 -35
  146. package/_testdriver/acceptance/prompt.yaml +0 -11
  147. package/_testdriver/acceptance/remember.yaml +0 -27
  148. package/_testdriver/acceptance/screenshots/cart.png +0 -0
  149. package/_testdriver/acceptance/scroll-keyboard.yaml +0 -34
  150. package/_testdriver/acceptance/scroll-until-image.yaml +0 -26
  151. package/_testdriver/acceptance/scroll-until-text.yaml +0 -20
  152. package/_testdriver/acceptance/scroll.yaml +0 -33
  153. package/_testdriver/acceptance/snippets/login.yaml +0 -29
  154. package/_testdriver/acceptance/snippets/match-cart.yaml +0 -8
  155. package/_testdriver/acceptance/type.yaml +0 -29
  156. package/_testdriver/behavior/failure.yaml +0 -7
  157. package/_testdriver/behavior/hover-text.yaml +0 -13
  158. package/_testdriver/behavior/lifecycle/postrun.yaml +0 -10
  159. package/_testdriver/behavior/lifecycle/prerun.yaml +0 -8
  160. package/_testdriver/behavior/lifecycle/provision.yaml +0 -8
  161. package/_testdriver/behavior/secrets.yaml +0 -7
  162. package/_testdriver/edge-cases/dashcam-chrome.yaml +0 -8
  163. package/_testdriver/edge-cases/exec-pwsh-multiline.yaml +0 -10
  164. package/_testdriver/edge-cases/js-exception.yaml +0 -8
  165. package/_testdriver/edge-cases/js-promise.yaml +0 -19
  166. package/_testdriver/edge-cases/lifecycle/postrun.yaml +0 -10
  167. package/_testdriver/edge-cases/prompt-in-middle.yaml +0 -23
  168. package/_testdriver/edge-cases/prompt-nested.yaml +0 -7
  169. package/_testdriver/edge-cases/success-test.yaml +0 -9
  170. package/_testdriver/examples/android/example.yaml +0 -12
  171. package/_testdriver/examples/android/lifecycle/postrun.yaml +0 -11
  172. package/_testdriver/examples/android/lifecycle/provision.yaml +0 -47
  173. package/_testdriver/examples/android/readme.md +0 -7
  174. package/_testdriver/examples/chrome-extension/lifecycle/provision.yaml +0 -74
  175. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  176. package/_testdriver/examples/desktop/lifecycle/provision.yaml +0 -64
  177. package/_testdriver/examples/vscode-extension/lifecycle/provision.yaml +0 -73
  178. package/_testdriver/examples/web/lifecycle/postrun.yaml +0 -7
  179. package/_testdriver/examples/web/lifecycle/prerun.yaml +0 -22
  180. package/_testdriver/lifecycle/postrun.yaml +0 -8
  181. package/_testdriver/lifecycle/prerun.yaml +0 -15
  182. package/_testdriver/lifecycle/provision.yaml +0 -25
  183. package/debug-screenshot-1763401388589.png +0 -0
  184. package/mcp-server/AI_GUIDELINES.md +0 -57
  185. package/scripts/view-test-results.mjs +0 -96
  186. package/styles/.vale-config/2-MDX.ini +0 -5
  187. package/styles/Microsoft/AMPM.yml +0 -9
  188. package/styles/Microsoft/Accessibility.yml +0 -30
  189. package/styles/Microsoft/Acronyms.yml +0 -64
  190. package/styles/Microsoft/Adverbs.yml +0 -272
  191. package/styles/Microsoft/Auto.yml +0 -11
  192. package/styles/Microsoft/Avoid.yml +0 -14
  193. package/styles/Microsoft/Contractions.yml +0 -50
  194. package/styles/Microsoft/Dashes.yml +0 -13
  195. package/styles/Microsoft/DateFormat.yml +0 -8
  196. package/styles/Microsoft/DateNumbers.yml +0 -40
  197. package/styles/Microsoft/DateOrder.yml +0 -8
  198. package/styles/Microsoft/Ellipses.yml +0 -9
  199. package/styles/Microsoft/FirstPerson.yml +0 -16
  200. package/styles/Microsoft/Foreign.yml +0 -13
  201. package/styles/Microsoft/Gender.yml +0 -8
  202. package/styles/Microsoft/GenderBias.yml +0 -42
  203. package/styles/Microsoft/GeneralURL.yml +0 -11
  204. package/styles/Microsoft/HeadingAcronyms.yml +0 -7
  205. package/styles/Microsoft/HeadingColons.yml +0 -8
  206. package/styles/Microsoft/HeadingPunctuation.yml +0 -13
  207. package/styles/Microsoft/Headings.yml +0 -28
  208. package/styles/Microsoft/Hyphens.yml +0 -14
  209. package/styles/Microsoft/Negative.yml +0 -13
  210. package/styles/Microsoft/Ordinal.yml +0 -13
  211. package/styles/Microsoft/OxfordComma.yml +0 -8
  212. package/styles/Microsoft/Passive.yml +0 -183
  213. package/styles/Microsoft/Percentages.yml +0 -7
  214. package/styles/Microsoft/Plurals.yml +0 -7
  215. package/styles/Microsoft/Quotes.yml +0 -7
  216. package/styles/Microsoft/RangeTime.yml +0 -13
  217. package/styles/Microsoft/Semicolon.yml +0 -8
  218. package/styles/Microsoft/SentenceLength.yml +0 -6
  219. package/styles/Microsoft/Spacing.yml +0 -8
  220. package/styles/Microsoft/Suspended.yml +0 -7
  221. package/styles/Microsoft/Terms.yml +0 -42
  222. package/styles/Microsoft/URLFormat.yml +0 -9
  223. package/styles/Microsoft/Units.yml +0 -16
  224. package/styles/Microsoft/Vocab.yml +0 -25
  225. package/styles/Microsoft/We.yml +0 -11
  226. package/styles/Microsoft/Wordiness.yml +0 -127
  227. package/styles/Microsoft/meta.json +0 -4
  228. package/styles/alex/Ablist.yml +0 -274
  229. package/styles/alex/Condescending.yml +0 -16
  230. package/styles/alex/Gendered.yml +0 -110
  231. package/styles/alex/LGBTQ.yml +0 -55
  232. package/styles/alex/OCD.yml +0 -10
  233. package/styles/alex/Press.yml +0 -12
  234. package/styles/alex/ProfanityLikely.yml +0 -1289
  235. package/styles/alex/ProfanityMaybe.yml +0 -282
  236. package/styles/alex/ProfanityUnlikely.yml +0 -251
  237. package/styles/alex/README.md +0 -27
  238. package/styles/alex/Race.yml +0 -85
  239. package/styles/alex/Suicide.yml +0 -26
  240. package/styles/alex/meta.json +0 -4
  241. package/styles/config/vocabularies/Docs/accept.txt +0 -47
  242. package/styles/config/vocabularies/Docs/reject.txt +0 -4
  243. package/styles/proselint/Airlinese.yml +0 -8
  244. package/styles/proselint/AnimalLabels.yml +0 -48
  245. package/styles/proselint/Annotations.yml +0 -9
  246. package/styles/proselint/Apologizing.yml +0 -8
  247. package/styles/proselint/Archaisms.yml +0 -52
  248. package/styles/proselint/But.yml +0 -8
  249. package/styles/proselint/Cliches.yml +0 -782
  250. package/styles/proselint/CorporateSpeak.yml +0 -30
  251. package/styles/proselint/Currency.yml +0 -5
  252. package/styles/proselint/Cursing.yml +0 -15
  253. package/styles/proselint/DateCase.yml +0 -7
  254. package/styles/proselint/DateMidnight.yml +0 -7
  255. package/styles/proselint/DateRedundancy.yml +0 -10
  256. package/styles/proselint/DateSpacing.yml +0 -7
  257. package/styles/proselint/DenizenLabels.yml +0 -52
  258. package/styles/proselint/Diacritical.yml +0 -95
  259. package/styles/proselint/GenderBias.yml +0 -45
  260. package/styles/proselint/GroupTerms.yml +0 -39
  261. package/styles/proselint/Hedging.yml +0 -8
  262. package/styles/proselint/Hyperbole.yml +0 -6
  263. package/styles/proselint/Jargon.yml +0 -11
  264. package/styles/proselint/LGBTOffensive.yml +0 -13
  265. package/styles/proselint/LGBTTerms.yml +0 -15
  266. package/styles/proselint/Malapropisms.yml +0 -8
  267. package/styles/proselint/Needless.yml +0 -358
  268. package/styles/proselint/Nonwords.yml +0 -38
  269. package/styles/proselint/Oxymorons.yml +0 -22
  270. package/styles/proselint/P-Value.yml +0 -6
  271. package/styles/proselint/RASSyndrome.yml +0 -30
  272. package/styles/proselint/README.md +0 -12
  273. package/styles/proselint/Skunked.yml +0 -13
  274. package/styles/proselint/Spelling.yml +0 -17
  275. package/styles/proselint/Typography.yml +0 -11
  276. package/styles/proselint/Uncomparables.yml +0 -50
  277. package/styles/proselint/Very.yml +0 -6
  278. package/styles/proselint/meta.json +0 -15
  279. package/styles/write-good/Cliches.yml +0 -702
  280. package/styles/write-good/E-Prime.yml +0 -32
  281. package/styles/write-good/Illusions.yml +0 -11
  282. package/styles/write-good/Passive.yml +0 -183
  283. package/styles/write-good/README.md +0 -27
  284. package/styles/write-good/So.yml +0 -5
  285. package/styles/write-good/ThereIs.yml +0 -6
  286. package/styles/write-good/TooWordy.yml +0 -221
  287. package/styles/write-good/Weasel.yml +0 -29
  288. package/styles/write-good/meta.json +0 -4
  289. package/test/mcp-example-test.yaml +0 -27
  290. package/test/test_parser.js +0 -47
  291. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +0 -61
  292. package/testdriver/acceptance-sdk/README.md +0 -128
  293. package/testdriver/acceptance-sdk/TEST_REPORTING.md +0 -245
  294. package/testdriver/acceptance-sdk/assert.test.mjs +0 -44
  295. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +0 -42
  296. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +0 -239
  297. package/testdriver/acceptance-sdk/type-checking-demo.js +0 -49
  298. package/testdriver/acceptance-sdk/type.test.mjs +0 -84
  299. package/vale.ini +0 -18
  300. package/vitest.config.example.js +0 -19
  301. package/vitest.config.mjs.bak +0 -44
  302. /package/docs/{ARCHITECTURE.md → v7/_drafts/architecture.mdx} +0 -0
  303. /package/docs/{AWESOME_LOGS_QUICK_REF.md → v7/_drafts/awesome-logs-quick-ref.mdx} +0 -0
  304. /package/{CONTRIBUTING.md → docs/v7/_drafts/contributing.mdx} +0 -0
  305. /package/docs/v7/{guides → _drafts}/migration.mdx +0 -0
  306. /package/{PLUGIN_MIGRATION.md → docs/v7/_drafts/plugin-migration.mdx} +0 -0
  307. /package/{PROMPT_CACHE.md → docs/v7/_drafts/prompt-cache.mdx} +0 -0
  308. /package/docs/{SDK_AWESOME_LOGS.md → v7/_drafts/sdk-awesome-logs.mdx} +0 -0
  309. /package/docs/{sdk-browser-rendering.md → v7/_drafts/sdk-browser-rendering.mdx} +0 -0
  310. /package/{SDK_LOGGING.md → docs/v7/_drafts/sdk-logging.mdx} +0 -0
  311. /package/{SDK_MIGRATION.md → docs/v7/_drafts/sdk-migration.mdx} +0 -0
  312. /package/docs/{TEST_RECORDING.md → v7/_drafts/test-recording.mdx} +0 -0
  313. /package/docs/v7/{README.md → overview/readme.mdx} +0 -0
  314. /package/{debug-locate-response.js → test/manual/debug-locate-response.js} +0 -0
  315. /package/{test-find-api.js → test/manual/test-find-api.js} +0 -0
  316. /package/{test-prompt-cache.js → test/manual/test-prompt-cache.js} +0 -0
  317. /package/{test-sandbox-render.js → test/manual/test-sandbox-render.js} +0 -0
  318. /package/{test-sdk-methods.js → test/manual/test-sdk-methods.js} +0 -0
  319. /package/{test-sdk-refactor.js → test/manual/test-sdk-refactor.js} +0 -0
  320. /package/{test-stack-trace.mjs → test/manual/test-stack-trace.mjs} +0 -0
  321. /package/{verify-element-api.js → test/manual/verify-element-api.js} +0 -0
  322. /package/{verify-types.js → test/manual/verify-types.js} +0 -0
  323. /package/{testdriver/acceptance-sdk → test/testdriver}/setup/globalTeardown.mjs +0 -0
  324. /package/{testdriver/acceptance-sdk → test/testdriver}/setup/vitestSetup.mjs +0 -0
package/sdk.js CHANGED
@@ -1,10 +1,69 @@
1
- #!/usr/bin/env node
2
-
3
1
  const fs = require("fs");
4
2
  const path = require("path");
5
3
  const os = require("os");
4
+ const crypto = require("crypto");
6
5
  const { formatter } = require("./sdk-log-formatter");
7
6
 
7
+ /**
8
+ * Get the file path of the caller (the file that called TestDriver)
9
+ * @returns {string|null} File path or null if not found
10
+ */
11
+ function getCallerFilePath() {
12
+ const originalPrepareStackTrace = Error.prepareStackTrace;
13
+ try {
14
+ const err = new Error();
15
+ Error.prepareStackTrace = (_, stack) => stack;
16
+ const stack = err.stack;
17
+ Error.prepareStackTrace = originalPrepareStackTrace;
18
+
19
+ // Look for the first file that's not sdk.js, hooks.mjs, or node internals
20
+ for (const callSite of stack) {
21
+ const fileName = callSite.getFileName();
22
+ if (fileName &&
23
+ !fileName.includes('sdk.js') &&
24
+ !fileName.includes('hooks.mjs') &&
25
+ !fileName.includes('hooks.js') &&
26
+ !fileName.includes('node_modules') &&
27
+ !fileName.includes('node:internal') &&
28
+ fileName !== 'evalmachine.<anonymous>') {
29
+ return fileName;
30
+ }
31
+ }
32
+ } catch (error) {
33
+ // Silently fail and return null
34
+ } finally {
35
+ Error.prepareStackTrace = originalPrepareStackTrace;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Generate a hash of the caller file for use as a cache key
42
+ * @returns {string|null} Hash of the file or null if file not found
43
+ */
44
+ function getCallerFileHash() {
45
+ const filePath = getCallerFilePath();
46
+ if (!filePath) {
47
+ return null;
48
+ }
49
+
50
+ try {
51
+ // Handle file:// URLs by converting to file system path
52
+ let fsPath = filePath;
53
+ if (filePath.startsWith('file://')) {
54
+ fsPath = filePath.replace('file://', '');
55
+ }
56
+
57
+ const fileContent = fs.readFileSync(fsPath, 'utf-8');
58
+ const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
59
+ // Return first 16 chars of hash for brevity
60
+ return hash.substring(0, 16);
61
+ } catch (error) {
62
+ // If we can't read the file, return null
63
+ return null;
64
+ }
65
+ }
66
+
8
67
  /**
9
68
  * Custom error class for element operation failures
10
69
  * Includes debugging information like screenshots and AI responses
@@ -13,8 +72,8 @@ class ElementNotFoundError extends Error {
13
72
  constructor(message, debugInfo = {}) {
14
73
  super(message);
15
74
  this.name = "ElementNotFoundError";
16
- this.screenshot = debugInfo.screenshot;
17
- this.aiResponse = debugInfo.aiResponse;
75
+ // Sanitize aiResponse to remove base64 images before storing
76
+ this.aiResponse = this._sanitizeAiResponse(debugInfo.aiResponse);
18
77
  this.description = debugInfo.description;
19
78
  this.timestamp = new Date().toISOString();
20
79
  this.screenshotPath = null;
@@ -24,8 +83,9 @@ class ElementNotFoundError extends Error {
24
83
  Error.captureStackTrace(this, ElementNotFoundError);
25
84
  }
26
85
 
27
- // Write screenshot to temp directory
28
- if (this.screenshot) {
86
+ // Write screenshot to temp directory immediately (don't store on error object)
87
+ // This prevents vitest from serializing huge base64 strings
88
+ if (debugInfo.screenshot) {
29
89
  try {
30
90
  const tempDir = path.join(os.tmpdir(), "testdriver-debug");
31
91
  if (!fs.existsSync(tempDir)) {
@@ -36,7 +96,7 @@ class ElementNotFoundError extends Error {
36
96
  this.screenshotPath = path.join(tempDir, filename);
37
97
 
38
98
  // Remove data:image/png;base64, prefix if present
39
- const base64Data = this.screenshot.replace(
99
+ const base64Data = debugInfo.screenshot.replace(
40
100
  /^data:image\/\w+;base64,/,
41
101
  "",
42
102
  );
@@ -182,6 +242,25 @@ class ElementNotFoundError extends Error {
182
242
  this.stack = filteredLines.join("\n");
183
243
  }
184
244
  }
245
+
246
+ /**
247
+ * Sanitize AI response by removing large base64 data to prevent serialization issues
248
+ * @private
249
+ * @param {Object} response - AI response
250
+ * @returns {Object} Sanitized response
251
+ */
252
+ _sanitizeAiResponse(response) {
253
+ if (!response) return null;
254
+
255
+ // Create shallow copy and remove large base64 fields
256
+ const sanitized = { ...response };
257
+ delete sanitized.croppedImage;
258
+ delete sanitized.screenshot;
259
+ delete sanitized.pixelDiffImage;
260
+ // Keep cachedImageUrl as it's just a URL string, not base64 data
261
+
262
+ return sanitized;
263
+ }
185
264
  }
186
265
 
187
266
  /**
@@ -213,16 +292,18 @@ class Element {
213
292
  /**
214
293
  * Find the element on screen
215
294
  * @param {string} [newDescription] - Optional new description to search for
216
- * @param {number} [cacheThreshold] - Cache threshold for this specific find (overrides global setting)
295
+ * @param {Object} [options] - Optional options object with cacheThreshold and/or cacheKey
217
296
  * @returns {Promise<Element>} This element instance
218
297
  */
219
- async find(newDescription, cacheThreshold) {
298
+ async find(newDescription, options) {
220
299
  const description = newDescription || this.description;
221
300
  if (newDescription) {
222
301
  this.description = newDescription;
223
302
  }
224
303
 
225
304
  const startTime = Date.now();
305
+ let response = null;
306
+ let findError = null;
226
307
 
227
308
  const debugMode =
228
309
  process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
@@ -239,9 +320,38 @@ class Element {
239
320
  this._screenshot = screenshot;
240
321
  }
241
322
 
242
- // Use per-command threshold if provided, otherwise fall back to global threshold
243
- const threshold =
244
- cacheThreshold ?? this.sdk.cacheThresholds?.find ?? 0.05;
323
+ // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
324
+ let cacheKey = null;
325
+ let cacheThreshold = null;
326
+
327
+ if (typeof options === 'number') {
328
+ // Legacy: options is just a number threshold
329
+ cacheThreshold = options;
330
+ } else if (typeof options === 'object' && options !== null) {
331
+ // New: options is an object with cacheKey and/or cacheThreshold
332
+ cacheKey = options.cacheKey || null;
333
+ cacheThreshold = options.cacheThreshold ?? null;
334
+ }
335
+
336
+ // Use default cacheKey from SDK constructor if not provided in find() options
337
+ if (!cacheKey && this.sdk.options?.cacheKey) {
338
+ cacheKey = this.sdk.options.cacheKey;
339
+ }
340
+
341
+ // Determine threshold:
342
+ // - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
343
+ // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
344
+ let threshold;
345
+ if (cacheKey) {
346
+ // cacheKey provided - enable cache with threshold
347
+ threshold = cacheThreshold ?? 0.05;
348
+ } else if (cacheThreshold !== null) {
349
+ // Explicit threshold provided without cacheKey
350
+ threshold = cacheThreshold;
351
+ } else {
352
+ // No cacheKey, no explicit threshold - use global default (which is -1 now)
353
+ threshold = this.sdk.cacheThresholds?.find ?? -1;
354
+ }
245
355
 
246
356
  // Store the threshold for debugging
247
357
  this._threshold = threshold;
@@ -249,16 +359,21 @@ class Element {
249
359
  // Debug log threshold
250
360
  if (debugMode) {
251
361
  const { events } = require("./agent/events.js");
362
+ const autoGenMsg = (this.sdk._autoGeneratedCacheKey && cacheKey === this.sdk.options.cacheKey)
363
+ ? ' (auto-generated from file hash)'
364
+ : '';
252
365
  this.sdk.emitter.emit(
253
366
  events.log.debug,
254
- `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"})`,
367
+ `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
255
368
  );
256
369
  }
257
370
 
258
- const response = await this.sdk.apiClient.req("find", {
371
+ response = await this.sdk.apiClient.req("find", {
372
+ session: this.sdk.getSessionId(),
259
373
  element: description,
260
374
  image: screenshot,
261
375
  threshold: threshold,
376
+ cacheKey: cacheKey,
262
377
  os: this.sdk.os,
263
378
  resolution: this.sdk.resolution,
264
379
  });
@@ -278,12 +393,36 @@ class Element {
278
393
  } else {
279
394
  this._response = this._sanitizeResponse(response);
280
395
  this._found = false;
396
+ findError = "Element not found";
281
397
  }
282
398
  } catch (error) {
283
399
  this._response = error.response
284
400
  ? this._sanitizeResponse(error.response)
285
401
  : null;
286
402
  this._found = false;
403
+ findError = error.message;
404
+ response = error.response;
405
+ }
406
+
407
+ // Track find interaction once at the end
408
+ const sessionId = this.sdk.getSessionId();
409
+ if (sessionId && this.sdk.sandbox?.send) {
410
+ try {
411
+ await this.sdk.sandbox.send({
412
+ type: "trackInteraction",
413
+ interactionType: "find",
414
+ session: sessionId,
415
+ prompt: description,
416
+ timestamp: startTime,
417
+ success: this._found,
418
+ error: findError,
419
+ cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
420
+ selector: response?.selector,
421
+ selectorUsed: !!response?.selector,
422
+ });
423
+ } catch (err) {
424
+ console.warn("Failed to track find interaction:", err.message);
425
+ }
287
426
  }
288
427
 
289
428
  return this;
@@ -544,11 +683,8 @@ class Element {
544
683
  `Element "${this.description}" not found.`,
545
684
  {
546
685
  description: this.description,
547
- screenshot: this._screenshot,
548
686
  aiResponse: this._response,
549
687
  threshold: this._threshold,
550
- cachedImageUrl: this._response?.cachedImageUrl,
551
- pixelDiffImage: this._response?.pixelDiffImage,
552
688
  },
553
689
  );
554
690
  }
@@ -562,10 +698,22 @@ class Element {
562
698
  );
563
699
  this.sdk.emitter.emit(events.log.log, formattedMessage);
564
700
 
701
+ // Prepare element metadata for interaction tracking
702
+ const elementData = {
703
+ prompt: this.description,
704
+ elementType: this._response?.elementType,
705
+ elementBounds: this._response?.elementBounds,
706
+ croppedImageUrl: this._response?.savedImagePath,
707
+ edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
708
+ cacheHit: this._response?.cacheHit,
709
+ selectorUsed: !!this._response?.selector,
710
+ selector: this._response?.selector
711
+ };
712
+
565
713
  if (action === "hover") {
566
- await this.commands.hover(this.coordinates.x, this.coordinates.y);
714
+ await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
567
715
  } else {
568
- await this.commands.click(this.coordinates.x, this.coordinates.y, action);
716
+ await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
569
717
  }
570
718
  }
571
719
 
@@ -579,11 +727,8 @@ class Element {
579
727
  `Element "${this.description}" not found.`,
580
728
  {
581
729
  description: this.description,
582
- screenshot: this._screenshot,
583
730
  aiResponse: this._response,
584
731
  threshold: this._threshold,
585
- cachedImageUrl: this._response?.cachedImageUrl,
586
- pixelDiffImage: this._response?.pixelDiffImage,
587
732
  },
588
733
  );
589
734
  }
@@ -593,7 +738,19 @@ class Element {
593
738
  const formattedMessage = formatter.formatAction("hover", this.description);
594
739
  this.sdk.emitter.emit(events.log.log, formattedMessage);
595
740
 
596
- await this.commands.hover(this.coordinates.x, this.coordinates.y);
741
+ // Prepare element metadata for interaction tracking
742
+ const elementData = {
743
+ prompt: this.description,
744
+ elementType: this._response?.elementType,
745
+ elementBounds: this._response?.elementBounds,
746
+ croppedImageUrl: this._response?.savedImagePath,
747
+ edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
748
+ cacheHit: this._response?.cacheHit,
749
+ selectorUsed: !!this._response?.selector,
750
+ selector: this._response?.selector
751
+ };
752
+
753
+ await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
597
754
  }
598
755
 
599
756
  /**
@@ -785,6 +942,60 @@ class Element {
785
942
  }
786
943
  }
787
944
 
945
+ /**
946
+ * Creates a chainable promise that allows method chaining on find() results
947
+ * This enables syntax like: await testdriver.find("button").click()
948
+ *
949
+ * @param {Promise<Element>} promise - The promise that resolves to an Element
950
+ * @returns {Promise<Element> & ChainableElement} A promise with chainable element methods
951
+ */
952
+ function createChainablePromise(promise) {
953
+ // Define the chainable methods that should be available
954
+ const chainableMethods = ['click', 'hover', 'doubleClick', 'rightClick', 'mouseDown', 'mouseUp'];
955
+
956
+ // Create a new promise that wraps the original
957
+ const chainablePromise = promise.then(element => element);
958
+
959
+ // Add chainable methods to the promise
960
+ for (const method of chainableMethods) {
961
+ chainablePromise[method] = function(...args) {
962
+ // Return a promise that waits for the element, then calls the method
963
+ return promise.then(element => element[method](...args));
964
+ };
965
+ }
966
+
967
+ // Add getters for element properties (these return promises)
968
+ Object.defineProperty(chainablePromise, 'x', {
969
+ get() { return promise.then(el => el.x); }
970
+ });
971
+ Object.defineProperty(chainablePromise, 'y', {
972
+ get() { return promise.then(el => el.y); }
973
+ });
974
+ Object.defineProperty(chainablePromise, 'centerX', {
975
+ get() { return promise.then(el => el.centerX); }
976
+ });
977
+ Object.defineProperty(chainablePromise, 'centerY', {
978
+ get() { return promise.then(el => el.centerY); }
979
+ });
980
+
981
+ // Add found() method
982
+ chainablePromise.found = function() {
983
+ return promise.then(el => el.found());
984
+ };
985
+
986
+ // Add getCoordinates() method
987
+ chainablePromise.getCoordinates = function() {
988
+ return promise.then(el => el.getCoordinates());
989
+ };
990
+
991
+ // Add getResponse() method
992
+ chainablePromise.getResponse = function() {
993
+ return promise.then(el => el.getResponse());
994
+ };
995
+
996
+ return chainablePromise;
997
+ }
998
+
788
999
  /**
789
1000
  * TestDriver SDK
790
1001
  *
@@ -838,6 +1049,17 @@ class TestDriverSDK {
838
1049
  },
839
1050
  });
840
1051
 
1052
+ // Auto-generate cache key from caller file hash if not explicitly provided
1053
+ // This allows caching to be tied to the specific test file
1054
+ if (!options.cacheKey) {
1055
+ const autoGeneratedKey = getCallerFileHash();
1056
+ if (autoGeneratedKey) {
1057
+ options.cacheKey = autoGeneratedKey;
1058
+ // Store flag to indicate this was auto-generated
1059
+ this._autoGeneratedCacheKey = true;
1060
+ }
1061
+ }
1062
+
841
1063
  // Store options for later use
842
1064
  this.options = options;
843
1065
 
@@ -847,7 +1069,7 @@ class TestDriverSDK {
847
1069
 
848
1070
  // Store newSandbox preference from options
849
1071
  this.newSandbox =
850
- options.newSandbox !== undefined ? options.newSandbox : false;
1072
+ options.newSandbox !== undefined ? options.newSandbox : true;
851
1073
 
852
1074
  // Store headless preference from options
853
1075
  this.headless = options.headless !== undefined ? options.headless : false;
@@ -862,31 +1084,48 @@ class TestDriverSDK {
862
1084
 
863
1085
  // Cache threshold configuration
864
1086
  // threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
865
- // cache: false option disables cache completely by setting threshold to -1
866
- // Also support TD_NO_CACHE environment variable
867
- const useCache =
868
- options.cache !== false && process.env.TD_NO_CACHE !== "true";
869
-
870
- // Note: Cannot emit events here as emitter is not yet available
871
- // Logging will be done after connection
872
-
873
- if (!useCache) {
874
- // If cache is disabled, use -1 to bypass cache entirely
1087
+ // By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
1088
+ // To enable cache, provide a cacheKey when calling find() or findAll()
1089
+ // Also support TD_NO_CACHE environment variable and cache: false option for backwards compatibility
1090
+ const cacheDisabled =
1091
+ options.cache === false || process.env.TD_NO_CACHE === "true";
1092
+
1093
+ if (cacheDisabled) {
1094
+ // Explicit cache disabled via option or env var
875
1095
  this.cacheThresholds = {
876
1096
  find: -1,
877
1097
  findAll: -1,
878
1098
  };
879
1099
  } else {
880
- // Use configured thresholds or defaults
1100
+ // Cache disabled by default, enabled only when cacheKey is provided
1101
+ // Note: The threshold value here is the fallback when cacheKey is NOT provided
881
1102
  this.cacheThresholds = {
882
- find: options.cacheThreshold?.find ?? 0.05,
883
- findAll: options.cacheThreshold?.findAll ?? 0.05,
1103
+ find: options.cacheThreshold?.find ?? -1, // Default: cache disabled
1104
+ findAll: options.cacheThreshold?.findAll ?? -1, // Default: cache disabled
884
1105
  };
885
1106
  }
886
1107
 
887
- // Redraw threshold configuration
888
- // threshold = percentage of pixels that must change to consider screen redrawn (0.1 = 0.1%)
889
- this.redrawThreshold = options.redrawThreshold ?? 0.1;
1108
+ // Redraw configuration
1109
+ // Supports both:
1110
+ // - redraw: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
1111
+ // - redrawThreshold: 0.1 (legacy, sets diffThreshold)
1112
+ // The `redraw` option takes precedence and matches the per-command API
1113
+ if (options.redraw !== undefined) {
1114
+ // New unified API: redraw object (matches per-command options)
1115
+ this.redrawOptions = typeof options.redraw === 'object'
1116
+ ? options.redraw
1117
+ : { enabled: options.redraw }; // Support redraw: false as shorthand
1118
+ } else if (options.redrawThreshold !== undefined) {
1119
+ // Legacy API: redrawThreshold number or object
1120
+ this.redrawOptions = typeof options.redrawThreshold === 'object'
1121
+ ? options.redrawThreshold
1122
+ : { diffThreshold: options.redrawThreshold };
1123
+ } else {
1124
+ // Default: enabled (as of v7.2)
1125
+ this.redrawOptions = { enabled: true };
1126
+ }
1127
+ // Keep redrawThreshold for backwards compatibility in connect()
1128
+ this.redrawThreshold = this.redrawOptions;
890
1129
 
891
1130
  // Track connection state
892
1131
  this.connected = false;
@@ -910,6 +1149,258 @@ class TestDriverSDK {
910
1149
 
911
1150
  // Set up event listeners once (they live for the lifetime of the SDK instance)
912
1151
  this._setupLogging();
1152
+
1153
+ // Set up provision API
1154
+ this.provision = this._createProvisionAPI();
1155
+
1156
+ // Set up dashcam API lazily
1157
+ this._dashcam = null;
1158
+ }
1159
+
1160
+ /**
1161
+ * Wait for the sandbox connection to complete
1162
+ * @returns {Promise<void>}
1163
+ */
1164
+ async ready() {
1165
+ if (this.__connectionPromise) {
1166
+ await this.__connectionPromise;
1167
+ }
1168
+ if (!this.connected) {
1169
+ throw new Error('Not connected to sandbox. Call connect() first or use autoConnect option.');
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Get or create the Dashcam instance
1175
+ * @returns {Dashcam} Dashcam instance
1176
+ */
1177
+ get dashcam() {
1178
+ if (!this._dashcam) {
1179
+ const { Dashcam } = require("./lib/core/index.js");
1180
+ // Don't pass apiKey - let Dashcam use its default key
1181
+ this._dashcam = new Dashcam(this);
1182
+ }
1183
+ return this._dashcam;
1184
+ }
1185
+
1186
+ /**
1187
+ * Get milliseconds elapsed since dashcam started recording
1188
+ * @returns {number|null} Milliseconds since dashcam start, or null if not recording
1189
+ */
1190
+ getDashcamElapsedTime() {
1191
+ if (this._dashcam) {
1192
+ return this._dashcam.getElapsedTime();
1193
+ }
1194
+ return null;
1195
+ }
1196
+
1197
+ /**
1198
+ * Create the provision API with methods for launching applications
1199
+ * @private
1200
+ */
1201
+ _createProvisionAPI() {
1202
+ return {
1203
+ /**
1204
+ * Launch Chrome browser
1205
+ * @param {Object} options - Chrome launch options
1206
+ * @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
1207
+ * @param {boolean} [options.maximized=true] - Start maximized
1208
+ * @param {boolean} [options.guest=false] - Use guest mode
1209
+ * @returns {Promise<void>}
1210
+ */
1211
+ chrome: async (options = {}) => {
1212
+ // Automatically wait for connection to be ready
1213
+ await this.ready();
1214
+
1215
+ const {
1216
+ url = 'http://testdriver-sandbox.vercel.app/',
1217
+ maximized = true,
1218
+ guest = false,
1219
+ } = options;
1220
+
1221
+ // If dashcam is available and recording, add web logs for this domain
1222
+ if (this._dashcam) {
1223
+
1224
+ // Create the log file on the remote machine
1225
+ const shell = this.os === "windows" ? "pwsh" : "sh";
1226
+ const logPath = this.os === "windows"
1227
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1228
+ : "/tmp/testdriver.log";
1229
+
1230
+ const createLogCmd = this.os === "windows"
1231
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1232
+ : `touch ${logPath}`;
1233
+
1234
+ await this.exec(shell, createLogCmd, 10000, true);
1235
+
1236
+ console.log('[provision.chrome] Adding web logs to dashcam...');
1237
+ try {
1238
+ const urlObj = new URL(url);
1239
+ const domain = urlObj.hostname;
1240
+ const pattern = `*${domain}*`;
1241
+ await this._dashcam.addWebLog(pattern, 'Web Logs');
1242
+ console.log(`[provision.chrome] ✅ Web logs added to dashcam (pattern: ${pattern})`);
1243
+
1244
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1245
+
1246
+ } catch (error) {
1247
+ console.warn('[provision.chrome] ⚠️ Failed to add web logs:', error.message);
1248
+ }
1249
+ }
1250
+
1251
+ // Automatically start dashcam if not already recording
1252
+ if (!this._dashcam || !this._dashcam.recording) {
1253
+ console.log('[provision.chrome] Starting dashcam...');
1254
+ await this.dashcam.start();
1255
+ console.log('[provision.chrome] ✅ Dashcam started');
1256
+ }
1257
+
1258
+ // Build Chrome launch command
1259
+ const chromeArgs = [];
1260
+ if (maximized) chromeArgs.push('--start-maximized');
1261
+ if (guest) chromeArgs.push('--guest');
1262
+ chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run');
1263
+
1264
+ // Add dashcam-chrome extension on Linux
1265
+ if (this.os === 'linux') {
1266
+ chromeArgs.push('--load-extension=/usr/lib/node_modules/dashcam-chrome/build');
1267
+ }
1268
+
1269
+ // Launch Chrome
1270
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1271
+
1272
+ if (this.os === 'windows') {
1273
+ const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
1274
+ await this.exec(
1275
+ shell,
1276
+ `Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1277
+ 30000
1278
+ );
1279
+ } else {
1280
+ const argsString = chromeArgs.join(' ');
1281
+ await this.exec(
1282
+ shell,
1283
+ `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1284
+ 30000
1285
+ );
1286
+ }
1287
+
1288
+ // Wait for Chrome to be ready
1289
+ await this.focusApplication('Google Chrome');
1290
+
1291
+
1292
+ // Wait for URL to load
1293
+ try {
1294
+ const urlObj = new URL(url);
1295
+ const domain = urlObj.hostname;
1296
+
1297
+ console.log(`[provision.chrome] Waiting for domain "${domain}" to appear in URL bar...`);
1298
+
1299
+ for (let attempt = 0; attempt < 30; attempt++) {
1300
+ try {
1301
+ const result = await this.find(`${domain}`);
1302
+ if (result.found()) {
1303
+ console.log(`[provision.chrome] ✅ Chrome ready at ${url}`);
1304
+ break;
1305
+ }
1306
+ } catch (e) {
1307
+ // Not found yet, continue polling
1308
+ }
1309
+ await new Promise(resolve => setTimeout(resolve, 1000));
1310
+ }
1311
+
1312
+ await this.focusApplication('Google Chrome');
1313
+ } catch (e) {
1314
+ console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
1315
+ }
1316
+ },
1317
+
1318
+ /**
1319
+ * Launch VS Code
1320
+ * @param {Object} options - VS Code launch options
1321
+ * @param {string} [options.workspace] - Workspace/folder to open
1322
+ * @param {string[]} [options.extensions=[]] - Extensions to install
1323
+ * @returns {Promise<void>}
1324
+ */
1325
+ vscode: async (options = {}) => {
1326
+ this._ensureConnected();
1327
+
1328
+ const {
1329
+ workspace = null,
1330
+ extensions = [],
1331
+ } = options;
1332
+
1333
+ // Install extensions if provided
1334
+ for (const extension of extensions) {
1335
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1336
+ await this.exec(
1337
+ shell,
1338
+ `code --install-extension ${extension}`,
1339
+ 60000,
1340
+ true
1341
+ );
1342
+ }
1343
+
1344
+ // Launch VS Code
1345
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1346
+ const workspaceArg = workspace ? `"${workspace}"` : '';
1347
+
1348
+ if (this.os === 'windows') {
1349
+ await this.exec(
1350
+ shell,
1351
+ `Start-Process code -ArgumentList ${workspaceArg}`,
1352
+ 30000
1353
+ );
1354
+ } else {
1355
+ await this.exec(
1356
+ shell,
1357
+ `code ${workspaceArg} >/dev/null 2>&1 &`,
1358
+ 30000
1359
+ );
1360
+ }
1361
+
1362
+ // Wait for VS Code to be ready
1363
+ await this.focusApplication('Visual Studio Code');
1364
+ console.log('[provision.vscode] ✅ VS Code ready');
1365
+ },
1366
+
1367
+ /**
1368
+ * Launch Electron app
1369
+ * @param {Object} options - Electron launch options
1370
+ * @param {string} options.appPath - Path to Electron app (required)
1371
+ * @param {string[]} [options.args=[]] - Additional electron args
1372
+ * @returns {Promise<void>}
1373
+ */
1374
+ electron: async (options = {}) => {
1375
+ this._ensureConnected();
1376
+
1377
+ const { appPath, args = [] } = options;
1378
+
1379
+ if (!appPath) {
1380
+ throw new Error('provision.electron requires appPath option');
1381
+ }
1382
+
1383
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1384
+ const argsString = args.join(' ');
1385
+
1386
+ if (this.os === 'windows') {
1387
+ await this.exec(
1388
+ shell,
1389
+ `Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
1390
+ 30000
1391
+ );
1392
+ } else {
1393
+ await this.exec(
1394
+ shell,
1395
+ `electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
1396
+ 30000
1397
+ );
1398
+ }
1399
+
1400
+ await this.focusApplication('Electron');
1401
+ console.log('[provision.electron] ✅ Electron app ready');
1402
+ },
1403
+ };
913
1404
  }
914
1405
 
915
1406
  /**
@@ -995,6 +1486,9 @@ class TestDriverSDK {
995
1486
  this.agent.sandboxOs = connectOptions.os;
996
1487
  } else if (this.sandboxOs) {
997
1488
  this.agent.sandboxOs = this.sandboxOs;
1489
+ } else {
1490
+ // Fall back to this.os (which defaults to "linux")
1491
+ this.agent.sandboxOs = this.os;
998
1492
  }
999
1493
 
1000
1494
  // Set redrawThreshold on agent's cliArgs.options
@@ -1009,6 +1503,22 @@ class TestDriverSDK {
1009
1503
  // Expose the agent's commands, parser, and commander
1010
1504
  this.commands = this.agent.commands;
1011
1505
 
1506
+ // Recreate commands with dashcam elapsed time support
1507
+ const { createCommands } = require("./agent/lib/commands.js");
1508
+ const commandsResult = createCommands(
1509
+ this.agent.emitter,
1510
+ this.agent.system,
1511
+ this.agent.sandbox,
1512
+ this.agent.config,
1513
+ this.agent.session,
1514
+ () => this.agent.sourceMapper?.currentFilePath || this.agent.thisFile,
1515
+ this.agent.cliArgs.options.redrawThreshold,
1516
+ () => this.getDashcamElapsedTime(), // Pass dashcam elapsed time function
1517
+ );
1518
+ this.commands = commandsResult.commands;
1519
+ this.agent.commands = commandsResult.commands;
1520
+ this.agent.redraw = commandsResult.redraw;
1521
+
1012
1522
  // Dynamically create command methods based on available commands
1013
1523
  this._setupCommandMethods();
1014
1524
 
@@ -1027,13 +1537,19 @@ class TestDriverSDK {
1027
1537
  * @returns {Promise<void>}
1028
1538
  */
1029
1539
  async disconnect() {
1540
+ // Track disconnect event if we were connected
1030
1541
  if (this.connected && this.instance) {
1031
- // Track disconnect event
1032
1542
  this.analytics.track("sdk.disconnect");
1543
+ }
1033
1544
 
1034
- this.connected = false;
1035
- this.instance = null;
1545
+ // Always close the sandbox WebSocket connection to clean up resources
1546
+ // This ensures we don't leave orphaned connections even if connect() failed
1547
+ if (this.sandbox && typeof this.sandbox.close === 'function') {
1548
+ this.sandbox.close();
1036
1549
  }
1550
+
1551
+ this.connected = false;
1552
+ this.instance = null;
1037
1553
  }
1038
1554
 
1039
1555
  /**
@@ -1054,16 +1570,24 @@ class TestDriverSDK {
1054
1570
  * Automatically locates the element and returns it
1055
1571
  *
1056
1572
  * @param {string} description - Description of the element to find
1057
- * @param {number} [cacheThreshold] - Cache threshold for this specific find (overrides global setting)
1058
- * @returns {Promise<Element>} Element instance that has been located
1573
+ * @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
1574
+ * @returns {Promise<Element> & ChainableElement} Element instance that has been located, with chainable methods
1059
1575
  *
1060
1576
  * @example
1061
- * // Find and click immediately
1577
+ * // Find and click immediately (chainable)
1578
+ * await client.find('the sign in button').click();
1579
+ *
1580
+ * @example
1581
+ * // Find and click (traditional)
1062
1582
  * const element = await client.find('the sign in button');
1063
1583
  * await element.click();
1064
1584
  *
1065
1585
  * @example
1066
- * // Find with custom cache threshold
1586
+ * // Find with cache key to enable caching
1587
+ * const element = await client.find('login button', { cacheKey: 'my-test-run' });
1588
+ *
1589
+ * @example
1590
+ * // Find with custom cache threshold (legacy)
1067
1591
  * const element = await client.find('login button', 0.01);
1068
1592
  *
1069
1593
  * @example
@@ -1077,10 +1601,14 @@ class TestDriverSDK {
1077
1601
  * }
1078
1602
  * await element.click();
1079
1603
  */
1080
- async find(description, cacheThreshold) {
1604
+ find(description, options) {
1081
1605
  this._ensureConnected();
1082
1606
  const element = new Element(description, this, this.system, this.commands);
1083
- return await element.find(null, cacheThreshold);
1607
+ const findPromise = element.find(null, options);
1608
+
1609
+ // Create a chainable promise that allows direct method chaining
1610
+ // e.g., await testdriver.find("button").click()
1611
+ return createChainablePromise(findPromise);
1084
1612
  }
1085
1613
 
1086
1614
  /**
@@ -1088,7 +1616,7 @@ class TestDriverSDK {
1088
1616
  * Automatically locates all matching elements and returns them as an array
1089
1617
  *
1090
1618
  * @param {string} description - Description of the elements to find
1091
- * @param {number} [cacheThreshold] - Cache threshold for this specific findAll (overrides global setting)
1619
+ * @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
1092
1620
  * @returns {Promise<Element[]>} Array of Element instances that have been located
1093
1621
  *
1094
1622
  * @example
@@ -1099,13 +1627,13 @@ class TestDriverSDK {
1099
1627
  * }
1100
1628
  *
1101
1629
  * @example
1102
- * // Find all list items with custom cache threshold
1103
- * const items = await client.findAll('list item', 0.01);
1630
+ * // Find all list items with cache key to enable caching
1631
+ * const items = await client.findAll('list item', { cacheKey: 'my-test-run' });
1104
1632
  * for (const item of items) {
1105
1633
  * console.log(`Found item at (${item.x}, ${item.y})`);
1106
1634
  * }
1107
1635
  */
1108
- async findAll(description, cacheThreshold) {
1636
+ async findAll(description, options) {
1109
1637
  this._ensureConnected();
1110
1638
 
1111
1639
  const startTime = Date.now();
@@ -1118,15 +1646,59 @@ class TestDriverSDK {
1118
1646
  try {
1119
1647
  const screenshot = await this.system.captureScreenBase64();
1120
1648
 
1121
- // Use per-command threshold if provided, otherwise fall back to global threshold
1122
- const threshold = cacheThreshold ?? this.cacheThresholds?.findAll ?? 0.05;
1649
+ // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
1650
+ let cacheKey = null;
1651
+ let cacheThreshold = null;
1652
+
1653
+ if (typeof options === 'number') {
1654
+ // Legacy: options is just a number threshold
1655
+ cacheThreshold = options;
1656
+ } else if (typeof options === 'object' && options !== null) {
1657
+ // New: options is an object with cacheKey and/or cacheThreshold
1658
+ cacheKey = options.cacheKey || null;
1659
+ cacheThreshold = options.cacheThreshold ?? null;
1660
+ }
1661
+
1662
+ // Use default cacheKey from SDK constructor if not provided in findAll() options
1663
+ if (!cacheKey && this.options?.cacheKey) {
1664
+ cacheKey = this.options.cacheKey;
1665
+ }
1666
+
1667
+ // Determine threshold:
1668
+ // - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
1669
+ // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
1670
+ let threshold;
1671
+ if (cacheKey) {
1672
+ // cacheKey provided - enable cache with threshold
1673
+ threshold = cacheThreshold ?? 0.05;
1674
+ } else if (cacheThreshold !== null) {
1675
+ // Explicit threshold provided without cacheKey
1676
+ threshold = cacheThreshold;
1677
+ } else {
1678
+ // No cacheKey, no explicit threshold - use global default (which is -1 now)
1679
+ threshold = this.cacheThresholds?.findAll ?? -1;
1680
+ }
1681
+
1682
+ // Debug log threshold
1683
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
1684
+ if (debugMode) {
1685
+ const autoGenMsg = (this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey)
1686
+ ? ' (auto-generated from file hash)'
1687
+ : '';
1688
+ this.emitter.emit(
1689
+ events.log.debug,
1690
+ `🔍 findAll() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
1691
+ );
1692
+ }
1123
1693
 
1124
1694
  const response = await this.apiClient.req(
1125
1695
  "/api/v7.0.0/testdriver-agent/testdriver-find-all",
1126
1696
  {
1697
+ session: this.getSessionId(),
1127
1698
  element: description,
1128
1699
  image: screenshot,
1129
1700
  threshold: threshold,
1701
+ cacheKey: cacheKey,
1130
1702
  os: this.os,
1131
1703
  resolution: this.resolution,
1132
1704
  },
@@ -1173,6 +1745,27 @@ class TestDriverSDK {
1173
1745
  return element;
1174
1746
  });
1175
1747
 
1748
+ // Track successful findAll interaction
1749
+ const sessionId = this.getSessionId();
1750
+ if (sessionId && this.sandbox?.send) {
1751
+ try {
1752
+ await this.sandbox.send({
1753
+ type: "trackInteraction",
1754
+ interactionType: "findAll",
1755
+ session: sessionId,
1756
+ prompt: description,
1757
+ timestamp: startTime,
1758
+ success: true,
1759
+ input: { count: elements.length },
1760
+ cacheHit: response.cached || false,
1761
+ selector: response.selector,
1762
+ selectorUsed: !!response.selector,
1763
+ });
1764
+ } catch (err) {
1765
+ console.warn("Failed to track findAll interaction:", err.message);
1766
+ }
1767
+ }
1768
+
1176
1769
  // Log debug information when elements are found
1177
1770
  if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
1178
1771
  const { events } = require("./agent/events.js");
@@ -1189,10 +1782,51 @@ class TestDriverSDK {
1189
1782
 
1190
1783
  return elements;
1191
1784
  } else {
1785
+ // No elements found - track interaction
1786
+ const sessionId = this.getSessionId();
1787
+ if (sessionId && this.sandbox?.send) {
1788
+ try {
1789
+ await this.sandbox.send({
1790
+ type: "trackInteraction",
1791
+ interactionType: "findAll",
1792
+ session: sessionId,
1793
+ prompt: description,
1794
+ timestamp: startTime,
1795
+ success: false,
1796
+ error: "No elements found",
1797
+ input: { count: 0 },
1798
+ cacheHit: response?.cached || false,
1799
+ selector: response?.selector,
1800
+ selectorUsed: !!response?.selector,
1801
+ });
1802
+ } catch (err) {
1803
+ console.warn("Failed to track findAll interaction:", err.message);
1804
+ }
1805
+ }
1806
+
1192
1807
  // No elements found - return empty array
1193
1808
  return [];
1194
1809
  }
1195
1810
  } catch (error) {
1811
+ // Track findAll error interaction
1812
+ const sessionId = this.getSessionId();
1813
+ if (sessionId && this.sandbox?.send) {
1814
+ try {
1815
+ await this.sandbox.send({
1816
+ type: "trackInteraction",
1817
+ interactionType: "findAll",
1818
+ session: sessionId,
1819
+ prompt: description,
1820
+ timestamp: startTime,
1821
+ success: false,
1822
+ error: error.message,
1823
+ input: { count: 0 },
1824
+ });
1825
+ } catch (err) {
1826
+ console.warn("Failed to track findAll interaction:", err.message);
1827
+ }
1828
+ }
1829
+
1196
1830
  const { events } = require("./agent/events.js");
1197
1831
  this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
1198
1832
  return [];
@@ -1245,17 +1879,18 @@ class TestDriverSDK {
1245
1879
  */
1246
1880
  _setupCommandMethods() {
1247
1881
  // Mapping from command names to SDK method names with type definitions
1882
+ // Each command supports both positional args (legacy) and object args (new)
1248
1883
  const commandMapping = {
1249
1884
  "hover-text": {
1250
1885
  name: "hoverText",
1251
1886
  /**
1252
1887
  * Hover over text on screen
1253
1888
  * @deprecated Use find() and element.click() instead
1254
- * @param {string} text - Text to find and hover over
1255
- * @param {string | null} [description] - Optional description of the element
1256
- * @param {ClickAction} [action='click'] - Action to perform
1257
- * @param {TextMatchMethod} [method='turbo'] - Text matching method
1258
- * @param {number} [timeout=5000] - Timeout in milliseconds
1889
+ * @param {Object|string} options - Options object or text (legacy positional)
1890
+ * @param {string} options.text - Text to find and hover over
1891
+ * @param {string|null} [options.description] - Optional description of the element
1892
+ * @param {ClickAction} [options.action='click'] - Action to perform
1893
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
1259
1894
  * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1260
1895
  */
1261
1896
  doc: "Hover over text on screen (deprecated - use find() instead)",
@@ -1265,8 +1900,9 @@ class TestDriverSDK {
1265
1900
  /**
1266
1901
  * Hover over an image on screen
1267
1902
  * @deprecated Use find() and element.click() instead
1268
- * @param {string} description - Description of the image to find
1269
- * @param {ClickAction} [action='click'] - Action to perform
1903
+ * @param {Object|string} options - Options object or description (legacy positional)
1904
+ * @param {string} options.description - Description of the image to find
1905
+ * @param {ClickAction} [options.action='click'] - Action to perform
1270
1906
  * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1271
1907
  */
1272
1908
  doc: "Hover over an image on screen (deprecated - use find() instead)",
@@ -1275,9 +1911,10 @@ class TestDriverSDK {
1275
1911
  name: "matchImage",
1276
1912
  /**
1277
1913
  * Match and interact with an image template
1278
- * @param {string} imagePath - Path to the image template
1279
- * @param {ClickAction} [action='click'] - Action to perform
1280
- * @param {boolean} [invert=false] - Invert the match
1914
+ * @param {Object|string} options - Options object or path (legacy positional)
1915
+ * @param {string} options.path - Path to the image template
1916
+ * @param {ClickAction} [options.action='click'] - Action to perform
1917
+ * @param {boolean} [options.invert=false] - Invert the match
1281
1918
  * @returns {Promise<boolean>}
1282
1919
  */
1283
1920
  doc: "Match and interact with an image template",
@@ -1286,17 +1923,20 @@ class TestDriverSDK {
1286
1923
  name: "type",
1287
1924
  /**
1288
1925
  * Type text
1289
- * @param {string | number} text - Text to type
1290
- * @param {number} [delay=250] - Delay between keystrokes in milliseconds
1926
+ * @param {string|number} text - Text to type
1927
+ * @param {Object} [options] - Additional options
1928
+ * @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
1929
+ * @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
1291
1930
  * @returns {Promise<void>}
1292
1931
  */
1293
- doc: "Type text",
1932
+ doc: "Type text (use { secret: true } for passwords)",
1294
1933
  },
1295
1934
  "press-keys": {
1296
1935
  name: "pressKeys",
1297
1936
  /**
1298
1937
  * Press keyboard keys
1299
1938
  * @param {KeyboardKey[]} keys - Array of keys to press
1939
+ * @param {Object} [options] - Additional options (reserved for future use)
1300
1940
  * @returns {Promise<void>}
1301
1941
  */
1302
1942
  doc: "Press keyboard keys",
@@ -1305,9 +1945,10 @@ class TestDriverSDK {
1305
1945
  name: "click",
1306
1946
  /**
1307
1947
  * Click at coordinates
1308
- * @param {number} x - X coordinate
1309
- * @param {number} y - Y coordinate
1310
- * @param {ClickAction} [action='click'] - Type of click action
1948
+ * @param {Object|number} options - Options object or x coordinate (legacy positional)
1949
+ * @param {number} options.x - X coordinate
1950
+ * @param {number} options.y - Y coordinate
1951
+ * @param {ClickAction} [options.action='click'] - Type of click action
1311
1952
  * @returns {Promise<void>}
1312
1953
  */
1313
1954
  doc: "Click at coordinates",
@@ -1316,8 +1957,9 @@ class TestDriverSDK {
1316
1957
  name: "hover",
1317
1958
  /**
1318
1959
  * Hover at coordinates
1319
- * @param {number} x - X coordinate
1320
- * @param {number} y - Y coordinate
1960
+ * @param {Object|number} options - Options object or x coordinate (legacy positional)
1961
+ * @param {number} options.x - X coordinate
1962
+ * @param {number} options.y - Y coordinate
1321
1963
  * @returns {Promise<void>}
1322
1964
  */
1323
1965
  doc: "Hover at coordinates",
@@ -1327,7 +1969,8 @@ class TestDriverSDK {
1327
1969
  /**
1328
1970
  * Scroll the page
1329
1971
  * @param {ScrollDirection} [direction='down'] - Direction to scroll
1330
- * @param {number} [amount=300] - Amount to scroll in pixels
1972
+ * @param {Object} [options] - Additional options
1973
+ * @param {number} [options.amount=300] - Amount to scroll in pixels
1331
1974
  * @returns {Promise<void>}
1332
1975
  */
1333
1976
  doc: "Scroll the page",
@@ -1338,6 +1981,7 @@ class TestDriverSDK {
1338
1981
  * Wait for specified time
1339
1982
  * @deprecated Consider using element polling with find() instead of arbitrary waits
1340
1983
  * @param {number} [timeout=3000] - Time to wait in milliseconds
1984
+ * @param {Object} [options] - Additional options (reserved for future use)
1341
1985
  * @returns {Promise<void>}
1342
1986
  */
1343
1987
  doc: "Wait for specified time (deprecated - consider element polling instead)",
@@ -1347,10 +1991,9 @@ class TestDriverSDK {
1347
1991
  /**
1348
1992
  * Wait for text to appear on screen
1349
1993
  * @deprecated Use find() in a polling loop instead
1350
- * @param {string} text - Text to wait for
1351
- * @param {number} [timeout=5000] - Timeout in milliseconds
1352
- * @param {TextMatchMethod} [method='turbo'] - Text matching method
1353
- * @param {boolean} [invert=false] - Invert the match (wait for text to disappear)
1994
+ * @param {Object|string} options - Options object or text (legacy positional)
1995
+ * @param {string} options.text - Text to wait for
1996
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
1354
1997
  * @returns {Promise<void>}
1355
1998
  */
1356
1999
  doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
@@ -1360,9 +2003,9 @@ class TestDriverSDK {
1360
2003
  /**
1361
2004
  * Wait for image to appear on screen
1362
2005
  * @deprecated Use find() in a polling loop instead
1363
- * @param {string} description - Description of the image
1364
- * @param {number} [timeout=10000] - Timeout in milliseconds
1365
- * @param {boolean} [invert=false] - Invert the match (wait for image to disappear)
2006
+ * @param {Object|string} options - Options object or description (legacy positional)
2007
+ * @param {string} options.description - Description of the image
2008
+ * @param {number} [options.timeout=10000] - Timeout in milliseconds
1366
2009
  * @returns {Promise<void>}
1367
2010
  */
1368
2011
  doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
@@ -1371,12 +2014,11 @@ class TestDriverSDK {
1371
2014
  name: "scrollUntilText",
1372
2015
  /**
1373
2016
  * Scroll until text is found
1374
- * @param {string} text - Text to find
1375
- * @param {ScrollDirection} [direction='down'] - Scroll direction
1376
- * @param {number} [maxDistance=10000] - Maximum distance to scroll in pixels
1377
- * @param {TextMatchMethod} [textMatchMethod='turbo'] - Text matching method
1378
- * @param {ScrollMethod} [method='keyboard'] - Scroll method
1379
- * @param {boolean} [invert=false] - Invert the match
2017
+ * @param {Object|string} options - Options object or text (legacy positional)
2018
+ * @param {string} options.text - Text to find
2019
+ * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2020
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2021
+ * @param {boolean} [options.invert=false] - Invert the match
1380
2022
  * @returns {Promise<void>}
1381
2023
  */
1382
2024
  doc: "Scroll until text is found",
@@ -1385,12 +2027,13 @@ class TestDriverSDK {
1385
2027
  name: "scrollUntilImage",
1386
2028
  /**
1387
2029
  * Scroll until image is found
1388
- * @param {string} description - Description of the image (or use path parameter)
1389
- * @param {ScrollDirection} [direction='down'] - Scroll direction
1390
- * @param {number} [maxDistance=10000] - Maximum distance to scroll in pixels
1391
- * @param {ScrollMethod} [method='keyboard'] - Scroll method
1392
- * @param {string | null} [path=null] - Path to image template
1393
- * @param {boolean} [invert=false] - Invert the match
2030
+ * @param {Object|string} [options] - Options object or description (legacy positional)
2031
+ * @param {string} [options.description] - Description of the image
2032
+ * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2033
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2034
+ * @param {string} [options.method='mouse'] - Scroll method
2035
+ * @param {string} [options.path] - Path to image template
2036
+ * @param {boolean} [options.invert=false] - Invert the match
1394
2037
  * @returns {Promise<void>}
1395
2038
  */
1396
2039
  doc: "Scroll until image is found",
@@ -1400,6 +2043,7 @@ class TestDriverSDK {
1400
2043
  /**
1401
2044
  * Focus an application by name
1402
2045
  * @param {string} name - Application name
2046
+ * @param {Object} [options] - Additional options (reserved for future use)
1403
2047
  * @returns {Promise<string>}
1404
2048
  */
1405
2049
  doc: "Focus an application by name",
@@ -1408,7 +2052,8 @@ class TestDriverSDK {
1408
2052
  name: "remember",
1409
2053
  /**
1410
2054
  * Extract and remember information from the screen using AI
1411
- * @param {string} description - What to remember
2055
+ * @param {Object|string} options - Options object or description (legacy positional)
2056
+ * @param {string} options.description - What to remember
1412
2057
  * @returns {Promise<string>}
1413
2058
  */
1414
2059
  doc: "Extract and remember information from the screen",
@@ -1418,6 +2063,7 @@ class TestDriverSDK {
1418
2063
  /**
1419
2064
  * Make an AI-powered assertion
1420
2065
  * @param {string} assertion - Assertion to check
2066
+ * @param {Object} [options] - Additional options (reserved for future use)
1421
2067
  * @returns {Promise<boolean>}
1422
2068
  */
1423
2069
  doc: "Make an AI-powered assertion",
@@ -1426,10 +2072,11 @@ class TestDriverSDK {
1426
2072
  name: "exec",
1427
2073
  /**
1428
2074
  * Execute code in the sandbox
1429
- * @param {ExecLanguage} language - Language ('js' or 'pwsh')
1430
- * @param {string} code - Code to execute
1431
- * @param {number} timeout - Timeout in milliseconds
1432
- * @param {boolean} [silent=false] - Suppress output
2075
+ * @param {Object|ExecLanguage} options - Options object or language (legacy positional)
2076
+ * @param {ExecLanguage} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
2077
+ * @param {string} options.code - Code to execute
2078
+ * @param {number} [options.timeout] - Timeout in milliseconds
2079
+ * @param {boolean} [options.silent=false] - Suppress output
1433
2080
  * @returns {Promise<string>}
1434
2081
  */
1435
2082
  doc: "Execute code in the sandbox",
@@ -1568,6 +2215,9 @@ class TestDriverSDK {
1568
2215
  * @private
1569
2216
  */
1570
2217
  _setupLogging() {
2218
+ // Track the last fatal error message to throw on exit
2219
+ let lastFatalError = null;
2220
+
1571
2221
  // Set up markdown logger
1572
2222
  createMarkdownLogger(this.emitter);
1573
2223
 
@@ -1580,6 +2230,9 @@ class TestDriverSDK {
1580
2230
  ? `[${this.testContext}] ${message}`
1581
2231
  : message;
1582
2232
  console.log(prefixedMessage);
2233
+
2234
+ // Also forward to sandbox for dashcam
2235
+ this._forwardLogToSandbox(prefixedMessage);
1583
2236
  }
1584
2237
  });
1585
2238
 
@@ -1587,6 +2240,11 @@ class TestDriverSDK {
1587
2240
  if (this.loggingEnabled) {
1588
2241
  const event = this.emitter.event;
1589
2242
  console.error(event, ":", data);
2243
+
2244
+ // Capture fatal errors
2245
+ if (event === events.error.fatal) {
2246
+ lastFatalError = data;
2247
+ }
1590
2248
  }
1591
2249
  });
1592
2250
 
@@ -1613,6 +2271,19 @@ class TestDriverSDK {
1613
2271
  }
1614
2272
  });
1615
2273
 
2274
+ // Handle exit events - throw error with meaningful message instead of calling process.exit
2275
+ // This allows test frameworks like Vitest to properly catch and display the error
2276
+ this.emitter.on(events.exit, (exitCode) => {
2277
+ if (exitCode !== 0) {
2278
+ // Create an error with the fatal error message if available
2279
+ const errorMessage = lastFatalError || 'TestDriver fatal error';
2280
+ const error = new Error(errorMessage);
2281
+ error.name = 'TestDriverFatalError';
2282
+ error.exitCode = exitCode;
2283
+ throw error;
2284
+ }
2285
+ });
2286
+
1616
2287
  // Handle show window events for sandbox visualization
1617
2288
  this.emitter.on("show-window", async (url) => {
1618
2289
  if (this.loggingEnabled) {
@@ -1748,13 +2419,42 @@ class TestDriverSDK {
1748
2419
  );
1749
2420
  await sdk.auth();
1750
2421
 
1751
- const platform = options.platform || this.config.TD_PLATFORM || "windows";
2422
+ const platform = options.platform || this.config.TD_PLATFORM || "linux";
1752
2423
 
1753
2424
  // Auto-detect sandbox ID from the active sandbox if not provided
1754
2425
  const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
1755
2426
 
1756
- // Get session ID from the agent's session instance
1757
- const sessionId = this.agent?.sessionInstance?.get() || null;
2427
+ // Get or create session ID using the agent's newSession method
2428
+ let sessionId = this.agent?.sessionInstance?.get() || null;
2429
+
2430
+ // If no session exists, create one using the agent's method
2431
+ if (!sessionId && this.agent?.newSession) {
2432
+ try {
2433
+ await this.agent.newSession();
2434
+ sessionId = this.agent.sessionInstance.get();
2435
+
2436
+ // Save session ID to file for reuse across test runs
2437
+ if (sessionId) {
2438
+ const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2439
+ fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
2440
+ }
2441
+ } catch (error) {
2442
+ // Log but don't fail - tests can run without a session
2443
+ console.warn('Failed to create session:', error.message);
2444
+ }
2445
+ }
2446
+
2447
+ // If still no session, try reading from file (for reporter/separate processes)
2448
+ if (!sessionId) {
2449
+ try {
2450
+ const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2451
+ if (fs.existsSync(sessionFile)) {
2452
+ sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
2453
+ }
2454
+ } catch (error) {
2455
+ // Ignore file read errors
2456
+ }
2457
+ }
1758
2458
 
1759
2459
  const data = {
1760
2460
  runId: options.runId,
@@ -1884,21 +2584,34 @@ class TestDriverSDK {
1884
2584
  *
1885
2585
  * @example
1886
2586
  * // Simple execution
1887
- * await client.ai('Click the submit button');
2587
+ * await client.act('Click the submit button');
1888
2588
  *
1889
2589
  * @example
1890
2590
  * // With validation loop
1891
- * const result = await client.ai('Fill out the contact form', { validateAndLoop: true });
2591
+ * const result = await client.act('Fill out the contact form', { validateAndLoop: true });
1892
2592
  * console.log(result); // AI's final assessment
1893
2593
  */
1894
- async ai(task) {
2594
+ async act(task) {
1895
2595
  this._ensureConnected();
1896
2596
 
1897
- this.analytics.track("sdk.ai", { task });
2597
+ this.analytics.track("sdk.act", { task });
1898
2598
 
1899
2599
  // Use the agent's exploratoryLoop method directly
1900
2600
  return await this.agent.exploratoryLoop(task, false, true, false);
1901
2601
  }
2602
+
2603
+ /**
2604
+ * @deprecated Use act() instead
2605
+ * Execute a natural language task using AI
2606
+ *
2607
+ * @param {string} task - Natural language description of what to do
2608
+ * @param {Object} options - Execution options
2609
+ * @param {boolean} [options.validateAndLoop=false] - Whether to validate completion and retry if incomplete
2610
+ * @returns {Promise<string|void>} Final AI response if validateAndLoop is true
2611
+ */
2612
+ async ai(task) {
2613
+ return await this.act(task);
2614
+ }
1902
2615
  }
1903
2616
 
1904
2617
  module.exports = TestDriverSDK;