@testdriverai/agent 7.8.0-test.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +1 -0
  5. package/CHANGELOG.md +953 -0
  6. package/README.md +81 -0
  7. package/agent/events.js +135 -0
  8. package/agent/index.js +2450 -0
  9. package/agent/interface.js +35 -0
  10. package/agent/lib/analytics.js +22 -0
  11. package/agent/lib/censorship.js +75 -0
  12. package/agent/lib/commander.js +246 -0
  13. package/agent/lib/commands.js +1684 -0
  14. package/agent/lib/config.js +60 -0
  15. package/agent/lib/generator.js +91 -0
  16. package/agent/lib/http.js +144 -0
  17. package/agent/lib/logger.js +56 -0
  18. package/agent/lib/outputs.js +29 -0
  19. package/agent/lib/parser.js +209 -0
  20. package/agent/lib/redraw.js +386 -0
  21. package/agent/lib/resources/cursor-2.png +0 -0
  22. package/agent/lib/sandbox.js +1104 -0
  23. package/agent/lib/sdk.js +633 -0
  24. package/agent/lib/session.js +25 -0
  25. package/agent/lib/source-mapper.js +342 -0
  26. package/agent/lib/subimage/index.js +77 -0
  27. package/agent/lib/subimage/opencv.js +69 -0
  28. package/agent/lib/system.js +204 -0
  29. package/agent/lib/theme.js +14 -0
  30. package/agent/lib/valid-version.js +21 -0
  31. package/agent/lib/validation.js +169 -0
  32. package/ai/.claude-plugin/plugin.json +9 -0
  33. package/ai/agents/testdriver.md +638 -0
  34. package/ai/skills/testdriver-ai/SKILL.md +204 -0
  35. package/ai/skills/testdriver-assert/SKILL.md +315 -0
  36. package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
  37. package/ai/skills/testdriver-cache/SKILL.md +221 -0
  38. package/ai/skills/testdriver-caching/SKILL.md +124 -0
  39. package/ai/skills/testdriver-captcha/SKILL.md +158 -0
  40. package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
  41. package/ai/skills/testdriver-click/SKILL.md +286 -0
  42. package/ai/skills/testdriver-client/SKILL.md +477 -0
  43. package/ai/skills/testdriver-cloud/SKILL.md +119 -0
  44. package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
  45. package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
  46. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
  47. package/ai/skills/testdriver-device-config/SKILL.md +317 -0
  48. package/ai/skills/testdriver-double-click/SKILL.md +102 -0
  49. package/ai/skills/testdriver-elements/SKILL.md +605 -0
  50. package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
  51. package/ai/skills/testdriver-errors/SKILL.md +246 -0
  52. package/ai/skills/testdriver-events/SKILL.md +356 -0
  53. package/ai/skills/testdriver-examples/SKILL.md +7 -0
  54. package/ai/skills/testdriver-exec/SKILL.md +317 -0
  55. package/ai/skills/testdriver-find/SKILL.md +829 -0
  56. package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
  57. package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
  58. package/ai/skills/testdriver-hover/SKILL.md +278 -0
  59. package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
  60. package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
  61. package/ai/skills/testdriver-mcp/SKILL.md +7 -0
  62. package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
  63. package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
  64. package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
  65. package/ai/skills/testdriver-parse/SKILL.md +236 -0
  66. package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
  67. package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
  68. package/ai/skills/testdriver-provision/SKILL.md +331 -0
  69. package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
  70. package/ai/skills/testdriver-redraw/SKILL.md +214 -0
  71. package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
  72. package/ai/skills/testdriver-right-click/SKILL.md +123 -0
  73. package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
  74. package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
  75. package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
  76. package/ai/skills/testdriver-scroll/SKILL.md +335 -0
  77. package/ai/skills/testdriver-secrets/SKILL.md +115 -0
  78. package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
  79. package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
  80. package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
  81. package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -0
  82. package/ai/skills/testdriver-type/SKILL.md +357 -0
  83. package/ai/skills/testdriver-variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver-wait/SKILL.md +50 -0
  85. package/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
  86. package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
  87. package/bin/testdriverai.js +22 -0
  88. package/debugger/bg.png +0 -0
  89. package/debugger/icon.png +0 -0
  90. package/debugger/index.html +469 -0
  91. package/debugger/td.png +0 -0
  92. package/debugger/tray-buffered.png +0 -0
  93. package/debugger/tray.png +0 -0
  94. package/docs/GITHUB_COMMENTS.md +330 -0
  95. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  96. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  97. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  98. package/docs/_data/examples-manifest.json +177 -0
  99. package/docs/_data/examples-manifest.schema.json +41 -0
  100. package/docs/_scripts/extract-example-urls.js +165 -0
  101. package/docs/_scripts/generate-examples.js +560 -0
  102. package/docs/_scripts/generate-skills.js +154 -0
  103. package/docs/_scripts/link-replacer.js +164 -0
  104. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  105. package/docs/changelog.mdx +161 -0
  106. package/docs/claude-mcp-plugin.mdx +160 -0
  107. package/docs/docs.json +442 -0
  108. package/docs/github-integration-setup.md +266 -0
  109. package/docs/guide/best-practices-polling.mdx +174 -0
  110. package/docs/images/content/account/newprojectsettings.png +0 -0
  111. package/docs/images/content/account/projectpage.png +0 -0
  112. package/docs/images/content/account/projectreplays.png +0 -0
  113. package/docs/images/content/account/team-manage.png +0 -0
  114. package/docs/images/content/account/teampage.png +0 -0
  115. package/docs/images/content/extension/cursor.svg +1 -0
  116. package/docs/images/content/extension/vscode.svg +57 -0
  117. package/docs/images/content/extension/windsurf.svg +3 -0
  118. package/docs/images/content/parse/output.png +0 -0
  119. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  120. package/docs/images/content/side-by-side.png +0 -0
  121. package/docs/images/content/vscode/ide-full.png +0 -0
  122. package/docs/images/content/vscode/running.png +0 -0
  123. package/docs/images/content/vscode/v7-chat.png +0 -0
  124. package/docs/images/content/vscode/v7-choose-agent.png +0 -0
  125. package/docs/images/content/vscode/v7-full.png +0 -0
  126. package/docs/images/content/vscode/v7-onboarding.png +0 -0
  127. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  128. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  129. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  130. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  131. package/docs/images/content/vscode/vscode-install.png +0 -0
  132. package/docs/images/content/vscode/vscode-overview.png +0 -0
  133. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  134. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  135. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  136. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  137. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  138. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  139. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  140. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  141. package/docs/images/template/dark.png +0 -0
  142. package/docs/images/template/icon.png +0 -0
  143. package/docs/images/template/light.png +0 -0
  144. package/docs/snippets/calendar-link.mdx +4 -0
  145. package/docs/snippets/gitignore-warning.mdx +7 -0
  146. package/docs/snippets/lifecycle-warning.mdx +6 -0
  147. package/docs/snippets/test-prereqs.mdx +12 -0
  148. package/docs/snippets/tests/assert-replay.mdx +7 -0
  149. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  150. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  151. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  152. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  153. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  154. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  155. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  156. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  157. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  158. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  159. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  160. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  161. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  162. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  163. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  164. package/docs/snippets/tests/remember-replay.mdx +7 -0
  165. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  166. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  167. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  168. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  169. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  170. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  171. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  172. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  173. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  174. package/docs/snippets/tests/type-replay.mdx +7 -0
  175. package/docs/snippets/tests/type-yaml.mdx +28 -0
  176. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  177. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  178. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  179. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  180. package/docs/snippets/tests/wait-replay.mdx +7 -0
  181. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  182. package/docs/styles.css +65 -0
  183. package/docs/v6/account/dashboard.mdx +16 -0
  184. package/docs/v6/account/enterprise.mdx +110 -0
  185. package/docs/v6/account/pricing.mdx +33 -0
  186. package/docs/v6/account/projects.mdx +33 -0
  187. package/docs/v6/account/team.mdx +35 -0
  188. package/docs/v6/action/ami.mdx +109 -0
  189. package/docs/v6/action/performance.mdx +105 -0
  190. package/docs/v6/action/secrets.mdx +93 -0
  191. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  192. package/docs/v6/apps/desktop-apps.mdx +93 -0
  193. package/docs/v6/apps/mobile-apps.mdx +26 -0
  194. package/docs/v6/apps/static-websites.mdx +54 -0
  195. package/docs/v6/apps/tauri-apps.mdx +361 -0
  196. package/docs/v6/bugs/jira.mdx +232 -0
  197. package/docs/v6/cli/overview.mdx +66 -0
  198. package/docs/v6/commands/assert.mdx +45 -0
  199. package/docs/v6/commands/exec.mdx +276 -0
  200. package/docs/v6/commands/focus-application.mdx +44 -0
  201. package/docs/v6/commands/hover-image.mdx +69 -0
  202. package/docs/v6/commands/hover-text.mdx +47 -0
  203. package/docs/v6/commands/if.mdx +53 -0
  204. package/docs/v6/commands/match-image.mdx +67 -0
  205. package/docs/v6/commands/press-keys.mdx +87 -0
  206. package/docs/v6/commands/remember.mdx +49 -0
  207. package/docs/v6/commands/run.mdx +44 -0
  208. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  209. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  210. package/docs/v6/commands/scroll.mdx +69 -0
  211. package/docs/v6/commands/type.mdx +45 -0
  212. package/docs/v6/commands/wait-for-image.mdx +54 -0
  213. package/docs/v6/commands/wait-for-text.mdx +48 -0
  214. package/docs/v6/commands/wait.mdx +45 -0
  215. package/docs/v6/exporting/junit.mdx +218 -0
  216. package/docs/v6/exporting/playwright.mdx +197 -0
  217. package/docs/v6/features/auto-healing.mdx +144 -0
  218. package/docs/v6/features/generation.mdx +116 -0
  219. package/docs/v6/features/parallel-testing.mdx +151 -0
  220. package/docs/v6/features/reusable-snippets.mdx +131 -0
  221. package/docs/v6/features/selectorless.mdx +80 -0
  222. package/docs/v6/features/visual-assertions.mdx +139 -0
  223. package/docs/v6/getting-started/ci.mdx +146 -0
  224. package/docs/v6/getting-started/cli.mdx +91 -0
  225. package/docs/v6/getting-started/editing.mdx +100 -0
  226. package/docs/v6/getting-started/playwright.mdx +342 -0
  227. package/docs/v6/getting-started/running.mdx +48 -0
  228. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  229. package/docs/v6/getting-started/vscode.mdx +88 -0
  230. package/docs/v6/guide/assertions.mdx +189 -0
  231. package/docs/v6/guide/authentication.mdx +136 -0
  232. package/docs/v6/guide/code.mdx +65 -0
  233. package/docs/v6/guide/dashcam.mdx +118 -0
  234. package/docs/v6/guide/environment-variables.mdx +26 -0
  235. package/docs/v6/guide/lifecycle.mdx +242 -0
  236. package/docs/v6/guide/locating.mdx +141 -0
  237. package/docs/v6/guide/protips.mdx +43 -0
  238. package/docs/v6/guide/variables.mdx +143 -0
  239. package/docs/v6/guide/waiting.mdx +130 -0
  240. package/docs/v6/importing/csv.mdx +196 -0
  241. package/docs/v6/importing/gherkin.mdx +143 -0
  242. package/docs/v6/importing/jira.mdx +164 -0
  243. package/docs/v6/importing/testrail.mdx +162 -0
  244. package/docs/v6/integrations/electron.mdx +146 -0
  245. package/docs/v6/integrations/netlify.mdx +100 -0
  246. package/docs/v6/integrations/vercel.mdx +125 -0
  247. package/docs/v6/interactive/explore.mdx +99 -0
  248. package/docs/v6/interactive/run.mdx +52 -0
  249. package/docs/v6/interactive/save.mdx +63 -0
  250. package/docs/v6/overview/comparison.mdx +101 -0
  251. package/docs/v6/overview/faq.mdx +162 -0
  252. package/docs/v6/overview/performance.mdx +52 -0
  253. package/docs/v6/overview/quickstart.mdx +137 -0
  254. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  255. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  256. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  257. package/docs/v6/scenarios/file-upload.mdx +33 -0
  258. package/docs/v6/scenarios/form-filling.mdx +32 -0
  259. package/docs/v6/scenarios/log-in.mdx +75 -0
  260. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  261. package/docs/v6/scenarios/spell-check.mdx +22 -0
  262. package/docs/v6/security/action.mdx +84 -0
  263. package/docs/v6/security/agent.mdx +73 -0
  264. package/docs/v6/security/platform.mdx +77 -0
  265. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  266. package/docs/v6/tutorials/basic-test.mdx +45 -0
  267. package/docs/v7/_drafts/agents.mdx +843 -0
  268. package/docs/v7/_drafts/architecture.mdx +399 -0
  269. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  270. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  271. package/docs/v7/_drafts/best-practices.mdx +486 -0
  272. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  273. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  274. package/docs/v7/_drafts/caching.mdx +366 -0
  275. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  276. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  277. package/docs/v7/_drafts/commands/exec.mdx +276 -0
  278. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  279. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  280. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  281. package/docs/v7/_drafts/commands/if.mdx +53 -0
  282. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  283. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  284. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  285. package/docs/v7/_drafts/commands/run.mdx +44 -0
  286. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  287. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  288. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  289. package/docs/v7/_drafts/commands/type.mdx +45 -0
  290. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  291. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  292. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  293. package/docs/v7/_drafts/configuration.mdx +378 -0
  294. package/docs/v7/_drafts/contributing.mdx +174 -0
  295. package/docs/v7/_drafts/core.mdx +458 -0
  296. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  297. package/docs/v7/_drafts/debugging.mdx +349 -0
  298. package/docs/v7/_drafts/error-handling.mdx +501 -0
  299. package/docs/v7/_drafts/faq.mdx +393 -0
  300. package/docs/v7/_drafts/hooks.mdx +360 -0
  301. package/docs/v7/_drafts/init-command.mdx +95 -0
  302. package/docs/v7/_drafts/installation.mdx +420 -0
  303. package/docs/v7/_drafts/migration.mdx +562 -0
  304. package/docs/v7/_drafts/observable.mdx +604 -0
  305. package/docs/v7/_drafts/playwright.mdx +342 -0
  306. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  307. package/docs/v7/_drafts/powerful.mdx +419 -0
  308. package/docs/v7/_drafts/presets.mdx +210 -0
  309. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  310. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  311. package/docs/v7/_drafts/provision.mdx +390 -0
  312. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  313. package/docs/v7/_drafts/readme.mdx +135 -0
  314. package/docs/v7/_drafts/reports.mdx +414 -0
  315. package/docs/v7/_drafts/scalable.mdx +763 -0
  316. package/docs/v7/_drafts/screenshot.mdx +155 -0
  317. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  318. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  319. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  320. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  321. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  322. package/docs/v7/_drafts/test-recording.mdx +382 -0
  323. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  324. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  325. package/docs/v7/_drafts/vitest.mdx +535 -0
  326. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  327. package/docs/v7/ai.mdx +205 -0
  328. package/docs/v7/assert.mdx +316 -0
  329. package/docs/v7/aws-setup.mdx +449 -0
  330. package/docs/v7/cache.mdx +223 -0
  331. package/docs/v7/caching.mdx +128 -0
  332. package/docs/v7/captcha.mdx +159 -0
  333. package/docs/v7/ci-cd.mdx +603 -0
  334. package/docs/v7/click.mdx +287 -0
  335. package/docs/v7/client.mdx +478 -0
  336. package/docs/v7/copilot/auto-healing.mdx +265 -0
  337. package/docs/v7/copilot/creating-tests.mdx +156 -0
  338. package/docs/v7/copilot/github.mdx +143 -0
  339. package/docs/v7/copilot/running-tests.mdx +149 -0
  340. package/docs/v7/copilot/setup.mdx +143 -0
  341. package/docs/v7/customizing-devices.mdx +319 -0
  342. package/docs/v7/dashcam.mdx +419 -0
  343. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  344. package/docs/v7/device-config.mdx +317 -0
  345. package/docs/v7/double-click.mdx +102 -0
  346. package/docs/v7/elements.mdx +606 -0
  347. package/docs/v7/enterprise.mdx +9 -0
  348. package/docs/v7/errors.mdx +248 -0
  349. package/docs/v7/events.mdx +358 -0
  350. package/docs/v7/examples/ai.mdx +72 -0
  351. package/docs/v7/examples/assert.mdx +72 -0
  352. package/docs/v7/examples/captcha-api.mdx +92 -0
  353. package/docs/v7/examples/chrome-extension.mdx +132 -0
  354. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  355. package/docs/v7/examples/element-not-found.mdx +67 -0
  356. package/docs/v7/examples/exec-output.mdx +85 -0
  357. package/docs/v7/examples/exec-pwsh.mdx +83 -0
  358. package/docs/v7/examples/focus-window.mdx +62 -0
  359. package/docs/v7/examples/hover-image.mdx +94 -0
  360. package/docs/v7/examples/hover-text.mdx +69 -0
  361. package/docs/v7/examples/installer.mdx +91 -0
  362. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  363. package/docs/v7/examples/match-image.mdx +96 -0
  364. package/docs/v7/examples/press-keys.mdx +92 -0
  365. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  366. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  367. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  368. package/docs/v7/examples/scroll.mdx +81 -0
  369. package/docs/v7/examples/type.mdx +92 -0
  370. package/docs/v7/examples/windows-installer.mdx +89 -0
  371. package/docs/v7/exec.mdx +318 -0
  372. package/docs/v7/find.mdx +830 -0
  373. package/docs/v7/focus-application.mdx +294 -0
  374. package/docs/v7/generating-tests.mdx +36 -0
  375. package/docs/v7/hosted.mdx +158 -0
  376. package/docs/v7/hover.mdx +279 -0
  377. package/docs/v7/locating-elements.mdx +71 -0
  378. package/docs/v7/making-assertions.mdx +32 -0
  379. package/docs/v7/mcp.mdx +9 -0
  380. package/docs/v7/mouse-down.mdx +161 -0
  381. package/docs/v7/mouse-up.mdx +164 -0
  382. package/docs/v7/parse.mdx +237 -0
  383. package/docs/v7/performing-actions.mdx +54 -0
  384. package/docs/v7/press-keys.mdx +349 -0
  385. package/docs/v7/provision.mdx +333 -0
  386. package/docs/v7/quickstart.mdx +173 -0
  387. package/docs/v7/redraw.mdx +216 -0
  388. package/docs/v7/reusable-code.mdx +249 -0
  389. package/docs/v7/right-click.mdx +123 -0
  390. package/docs/v7/running-tests.mdx +185 -0
  391. package/docs/v7/screenshot.mdx +249 -0
  392. package/docs/v7/screenshots.mdx +186 -0
  393. package/docs/v7/scroll.mdx +336 -0
  394. package/docs/v7/secrets.mdx +115 -0
  395. package/docs/v7/self-hosted.mdx +149 -0
  396. package/docs/v7/type.mdx +358 -0
  397. package/docs/v7/variables.mdx +111 -0
  398. package/docs/v7/wait.mdx +52 -0
  399. package/docs/v7/waiting-for-elements.mdx +90 -0
  400. package/docs/v7/what-is-testdriver.mdx +54 -0
  401. package/eslint.config.js +67 -0
  402. package/examples/ai.test.mjs +31 -0
  403. package/examples/assert.test.mjs +47 -0
  404. package/examples/chrome-extension.test.mjs +97 -0
  405. package/examples/config.mjs +5 -0
  406. package/examples/element-not-found.test.mjs +27 -0
  407. package/examples/exec-output.test.mjs +60 -0
  408. package/examples/exec-pwsh.test.mjs +58 -0
  409. package/examples/findall-coffee-icons.test.mjs +42 -0
  410. package/examples/focus-window.test.mjs +37 -0
  411. package/examples/formatted-logging.test.mjs +27 -0
  412. package/examples/hover-image.test.mjs +53 -0
  413. package/examples/hover-text-with-description.test.mjs +57 -0
  414. package/examples/hover-text.test.mjs +28 -0
  415. package/examples/installer.test.mjs +50 -0
  416. package/examples/launch-vscode-linux.test.mjs +55 -0
  417. package/examples/match-image.test.mjs +55 -0
  418. package/examples/parse.test.mjs +19 -0
  419. package/examples/press-keys.test.mjs +44 -0
  420. package/examples/prompt.test.mjs +34 -0
  421. package/examples/scroll-keyboard.test.mjs +38 -0
  422. package/examples/scroll-until-image.test.mjs +40 -0
  423. package/examples/scroll.test.mjs +42 -0
  424. package/examples/type.test.mjs +46 -0
  425. package/examples/windows-installer.test.mjs +54 -0
  426. package/index.js +2 -0
  427. package/interfaces/cli/commands/init.js +438 -0
  428. package/interfaces/cli/commands/setup.js +382 -0
  429. package/interfaces/cli/lib/base.js +285 -0
  430. package/interfaces/cli.js +20 -0
  431. package/interfaces/junit-reporter.js +290 -0
  432. package/interfaces/logger.js +388 -0
  433. package/interfaces/readline.js +234 -0
  434. package/interfaces/shared-test-state.mjs +64 -0
  435. package/interfaces/vitest-plugin.d.ts +115 -0
  436. package/interfaces/vitest-plugin.mjs +1698 -0
  437. package/lib/captcha/solver.js +358 -0
  438. package/lib/core/Dashcam.js +533 -0
  439. package/lib/core/index.d.ts +172 -0
  440. package/lib/core/index.js +12 -0
  441. package/lib/environments.json +18 -0
  442. package/lib/github-comment-formatter.js +263 -0
  443. package/lib/github-comment.mjs +452 -0
  444. package/lib/init-project.js +575 -0
  445. package/lib/presets/index.mjs +331 -0
  446. package/lib/resolve-channel.js +46 -0
  447. package/lib/sentry.js +417 -0
  448. package/lib/vitest/hooks.d.ts +57 -0
  449. package/lib/vitest/hooks.mjs +674 -0
  450. package/lib/vitest/setup-aws.mjs +247 -0
  451. package/lib/vitest/setup-self-hosted.mjs +151 -0
  452. package/lib/vitest/setup.mjs +46 -0
  453. package/manual/captcha-api.test.mjs +51 -0
  454. package/manual/drag-and-drop.test.mjs +59 -0
  455. package/manual/flake-diffthreshold-001.test.mjs +9 -0
  456. package/manual/flake-diffthreshold-01.test.mjs +9 -0
  457. package/manual/flake-diffthreshold-05.test.mjs +9 -0
  458. package/manual/flake-noredraw-cache.test.mjs +9 -0
  459. package/manual/flake-noredraw-nocache.test.mjs +9 -0
  460. package/manual/flake-redraw-cache.test.mjs +9 -0
  461. package/manual/flake-redraw-nocache.test.mjs +9 -0
  462. package/manual/flake-rocket-match.test.mjs +30 -0
  463. package/manual/flake-shared.mjs +51 -0
  464. package/manual/no-provision.test.mjs +31 -0
  465. package/manual/packer-hover-image.test.mjs +176 -0
  466. package/manual/scroll-until-text.test.mjs +68 -0
  467. package/manual/test-init-command.js +223 -0
  468. package/mcp-server/README.md +322 -0
  469. package/mcp-server/dist/codegen.d.ts +9 -0
  470. package/mcp-server/dist/codegen.js +165 -0
  471. package/mcp-server/dist/mcp-app.html +114 -0
  472. package/mcp-server/dist/package.json +1 -0
  473. package/mcp-server/dist/provision-types.d.ts +290 -0
  474. package/mcp-server/dist/provision-types.js +174 -0
  475. package/mcp-server/dist/server.d.ts +6 -0
  476. package/mcp-server/dist/server.mjs +1925 -0
  477. package/mcp-server/dist/session.d.ts +85 -0
  478. package/mcp-server/dist/session.js +152 -0
  479. package/mcp-server/mcp-app.html +28 -0
  480. package/mcp-server/mcp-config.example.json +19 -0
  481. package/mcp-server/package-lock.json +4027 -0
  482. package/mcp-server/package.json +31 -0
  483. package/mcp-server/src/codegen.ts +189 -0
  484. package/mcp-server/src/mcp-app.css +360 -0
  485. package/mcp-server/src/mcp-app.ts +547 -0
  486. package/mcp-server/src/provision-types.ts +209 -0
  487. package/mcp-server/src/server.ts +2391 -0
  488. package/mcp-server/src/session.ts +194 -0
  489. package/mcp-server/tsconfig.json +16 -0
  490. package/mcp-server/vite.config.ts +23 -0
  491. package/package.json +158 -0
  492. package/schema.json +1046 -0
  493. package/scripts/generate-skills.js +94 -0
  494. package/sdk-log-formatter.js +1157 -0
  495. package/sdk.d.ts +1486 -0
  496. package/sdk.js +4336 -0
  497. package/setup/aws/cloudformation.yaml +463 -0
  498. package/setup/aws/disable-defender.sh +42 -0
  499. package/setup/aws/install-dev-runner.sh +79 -0
  500. package/setup/aws/spawn-runner.sh +289 -0
  501. package/test/captcha-solver.test.mjs +152 -0
  502. package/test/chrome-remote-debugging.test.mjs +66 -0
  503. package/test/duckduckgo/experiment.test.mjs +28 -0
  504. package/test/duckduckgo/setup.test.mjs +29 -0
  505. package/test/manual/debug-locate-response.js +82 -0
  506. package/test/manual/reconnect-provision.test.mjs +49 -0
  507. package/test/manual/test-console-logs.test.mjs +42 -0
  508. package/test/manual/test-find-api.js +73 -0
  509. package/test/manual/test-init.sh +54 -0
  510. package/test/manual/test-prompt-cache.js +97 -0
  511. package/test/manual/test-provision-auth.mjs +22 -0
  512. package/test/manual/test-sandbox-render.js +29 -0
  513. package/test/manual/test-sdk-methods.js +15 -0
  514. package/test/manual/test-sdk-refactor.js +53 -0
  515. package/test/manual/test-stack-trace.mjs +57 -0
  516. package/test/manual/verify-element-api.js +89 -0
  517. package/test/manual/verify-types.js +0 -0
  518. package/test/manual-unawaited-promise.test.mjs +31 -0
  519. package/vitest.config.mjs +58 -0
  520. package/vitest.runner.config.mjs +33 -0
  521. package/vscode-extension/.vscodeignore +12 -0
  522. package/vscode-extension/README.md +94 -0
  523. package/vscode-extension/media/icon.png +0 -0
  524. package/vscode-extension/package-lock.json +4126 -0
  525. package/vscode-extension/package.json +86 -0
  526. package/vscode-extension/src/extension.ts +829 -0
  527. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  528. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,1698 @@
1
+ import { execSync } from "child_process";
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import { createRequire } from "module";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { postOrUpdateTestResults } from "../lib/github-comment.mjs";
8
+ import { setTestRunInfo } from "./shared-test-state.mjs";
9
+
10
+ // Use createRequire to import CommonJS modules without esbuild processing
11
+ const require = createRequire(import.meta.url);
12
+ const channelConfig = require("../lib/resolve-channel.js");
13
+
14
+ // Import Sentry for error reporting
15
+ const Sentry = require("@sentry/node");
16
+ const chalk = require("chalk");
17
+
18
+ // Track if Sentry has been initialized
19
+ let sentryInitialized = false;
20
+
21
+ /**
22
+ * Initialize Sentry for test failure reporting
23
+ * Uses same configuration as lib/sentry.js for consistency
24
+ */
25
+ function initializeSentry() {
26
+ if (sentryInitialized) return;
27
+
28
+ // Respect telemetry opt-out
29
+ if (process.env.TD_TELEMETRY === "false") {
30
+ return;
31
+ }
32
+
33
+ try {
34
+ const version = resolveTestDriverVersion() || "unknown";
35
+
36
+ Sentry.init({
37
+ dsn:
38
+ process.env.SENTRY_DSN ||
39
+ "https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
40
+ environment: "vitest",
41
+ release: version,
42
+ sampleRate: 1.0,
43
+ tracesSampleRate: 1.0,
44
+ enableLogs: true,
45
+ integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
46
+ initialScope: {
47
+ tags: {
48
+ platform: os.platform(),
49
+ arch: os.arch(),
50
+ nodeVersion: process.version,
51
+ runner: "vitest",
52
+ },
53
+ },
54
+ // Filter out events that should not be reported to Sentry
55
+ beforeSend(event, hint) {
56
+ const error = hint.originalException;
57
+
58
+ // Don't send user-cancelled errors
59
+ if (error && error.message && error.message.includes("User cancelled")) {
60
+ return null;
61
+ }
62
+
63
+ // Don't send test failures - these are expected behavior, not bugs in the SDK
64
+ // Test failures indicate the test found a problem, which is the intended use case
65
+ if (event.exception?.values) {
66
+ for (const exception of event.exception.values) {
67
+ // Filter out TestFailure type (from Vitest test failures)
68
+ if (exception.type === "TestFailure") {
69
+ return null;
70
+ }
71
+
72
+ // Filter out common user code errors (ReferenceError, TypeError from user tests)
73
+ // Only report if the error originates from TestDriver SDK code, not user test code
74
+ const isUserCodeError = exception.stacktrace?.frames?.some(frame => {
75
+ const filename = frame.filename || frame.abs_path || "";
76
+ // Check if error is from user test files (not from SDK internals)
77
+ return filename.includes("/tests/") ||
78
+ filename.includes("/test/") ||
79
+ filename.includes(".test.") ||
80
+ filename.includes(".spec.");
81
+ });
82
+
83
+ if (isUserCodeError && (exception.type === "ReferenceError" || exception.type === "TypeError")) {
84
+ return null;
85
+ }
86
+
87
+ // Filter out ElementNotFoundError - expected test outcome, not a crash
88
+ if (exception.type === "ElementNotFoundError") {
89
+ return null;
90
+ }
91
+ }
92
+ }
93
+
94
+ return event;
95
+ },
96
+ });
97
+
98
+ sentryInitialized = true;
99
+ logger.debug("Sentry initialized for vitest");
100
+ } catch (err) {
101
+ // Sentry init failed - continue without it
102
+ logger.debug("Failed to initialize Sentry:", err.message);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Flush Sentry events before process exit
108
+ * @param {number} [timeout=2000] - Timeout in ms
109
+ */
110
+ async function flushSentry(timeout = 2000) {
111
+ if (!sentryInitialized) return;
112
+ try {
113
+ await Sentry.flush(timeout);
114
+ } catch (err) {
115
+ // Ignore flush errors
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Resolve the TestDriver SDK version using multiple strategies.
121
+ * Similar to resolveVitestVersion(), guards against import.meta.url rewriting.
122
+ * @returns {string|null}
123
+ */
124
+ function resolveTestDriverVersion() {
125
+ try {
126
+ return require("../package.json").version;
127
+ } catch { }
128
+
129
+ try {
130
+ const cwdRequire = createRequire(path.join(process.cwd(), "package.json"));
131
+ return cwdRequire("testdriverai/package.json").version;
132
+ } catch { }
133
+
134
+ try {
135
+ const pkgPath = path.join(process.cwd(), "node_modules", "testdriverai", "package.json");
136
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
137
+ } catch { }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Minimum required Vitest major version
144
+ */
145
+ const MINIMUM_VITEST_VERSION = 4;
146
+
147
+ /**
148
+ * Try to read vitest's package.json version using multiple resolution strategies.
149
+ * Vitest's Vite-based transform pipeline can rewrite import.meta.url, causing
150
+ * createRequire to resolve from the wrong location. We fall back to resolving
151
+ * from process.cwd() and then to reading directly from node_modules.
152
+ * @returns {string|null} The vitest version string, or null if not found
153
+ */
154
+ function resolveVitestVersion() {
155
+ // Strategy 1: createRequire from import.meta.url (standard CJS interop)
156
+ try {
157
+ return require("vitest/package.json").version;
158
+ } catch { }
159
+
160
+ // Strategy 2: createRequire from process.cwd() (works when import.meta.url is rewritten)
161
+ try {
162
+ const cwdRequire = createRequire(path.join(process.cwd(), "package.json"));
163
+ return cwdRequire("vitest/package.json").version;
164
+ } catch { }
165
+
166
+ // Strategy 3: read directly from node_modules on disk
167
+ try {
168
+ const vitestPkgPath = path.join(process.cwd(), "node_modules", "vitest", "package.json");
169
+ return JSON.parse(fs.readFileSync(vitestPkgPath, "utf8")).version;
170
+ } catch { }
171
+
172
+ return null;
173
+ }
174
+
175
+ /**
176
+ * Check that Vitest version meets minimum requirements
177
+ * @throws {Error} if Vitest version is below minimum or not installed
178
+ */
179
+ function checkVitestVersion() {
180
+ const version = resolveVitestVersion();
181
+
182
+ if (!version) {
183
+ throw new Error(
184
+ "TestDriver requires Vitest to be installed. " +
185
+ "Please install it: npm install vitest@latest",
186
+ );
187
+ }
188
+
189
+ const major = parseInt(version.split(".")[0], 10);
190
+ if (major < MINIMUM_VITEST_VERSION) {
191
+ throw new Error(
192
+ `TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
193
+ `Please upgrade Vitest: npm install vitest@latest`,
194
+ );
195
+ }
196
+ }
197
+
198
+ // Check Vitest version at plugin load time
199
+ checkVitestVersion();
200
+
201
+ /**
202
+ * Simple logger for the vitest plugin
203
+ * Supports log levels: debug, info, warn, error
204
+ * Control via TD_LOG_LEVEL environment variable (default: "info")
205
+ * Set TD_LOG_LEVEL=debug for verbose output
206
+ */
207
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
208
+ const currentLogLevel =
209
+ LOG_LEVELS[process.env.TD_LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
210
+
211
+ const logger = {
212
+ debug: (...args) => {
213
+ if (currentLogLevel <= LOG_LEVELS.debug) {
214
+ console.log("[TestDriver]", ...args);
215
+ }
216
+ },
217
+ info: (...args) => {
218
+ if (currentLogLevel <= LOG_LEVELS.info) {
219
+ console.log("[TestDriver]", ...args);
220
+ }
221
+ },
222
+ warn: (...args) => {
223
+ if (currentLogLevel <= LOG_LEVELS.warn) {
224
+ console.warn("[TestDriver]", ...args);
225
+ }
226
+ },
227
+ error: (...args) => {
228
+ if (currentLogLevel <= LOG_LEVELS.error) {
229
+ console.error("[TestDriver]", ...args);
230
+ }
231
+ },
232
+ };
233
+
234
+ /**
235
+ * Timeout wrapper for promises
236
+ * @param {Promise} promise - Promise to wrap
237
+ * @param {number} timeoutMs - Timeout in milliseconds
238
+ * @param {string} operationName - Name of operation for error message
239
+ * @returns {Promise} Promise that rejects if timeout is reached
240
+ */
241
+ function withTimeout(promise, timeoutMs, operationName) {
242
+ return Promise.race([
243
+ promise,
244
+ new Promise((_, reject) =>
245
+ setTimeout(
246
+ () =>
247
+ reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)),
248
+ timeoutMs,
249
+ ),
250
+ ),
251
+ ]);
252
+ }
253
+
254
+ /**
255
+ * Vitest Plugin for TestDriver
256
+ *
257
+ * Records test runs, test cases, and associates them with dashcam recordings.
258
+ * Uses plugin architecture for better global state management.
259
+ *
260
+ * ## How it works:
261
+ *
262
+ * 1. **Plugin State**: All state is managed in a single `pluginState` object
263
+ * - No class instances or complex scoping
264
+ * - Easy to access from anywhere in the plugin
265
+ * - Dashcam URLs tracked in memory (no temp files!)
266
+ *
267
+ * 2. **Dashcam URL Registration**: Tests register dashcam URLs via simple API
268
+ * - `globalThis.__testdriverPlugin.registerDashcamUrl(testId, url, platform)`
269
+ * - No file system operations
270
+ * - No complex matching logic
271
+ * - Direct association via test ID
272
+ *
273
+ * 3. **Test Recording Flow**:
274
+ * - `onTestRunStart`: Create test run record
275
+ * - `onTestCaseReady`: Track test start time
276
+ * - `onTestCaseResult`: Record individual test result (immediate)
277
+ * - `onTestRunEnd`: Complete test run with final stats
278
+ *
279
+ * 4. **Platform Detection**: Automatically detects platform from SDK client
280
+ * - No manual configuration needed
281
+ * - Stored when dashcam URL is registered
282
+ */
283
+
284
+ // Shared state that can be imported by both the reporter and setup files
285
+ export const pluginState = {
286
+ testRun: null,
287
+ testRunId: null,
288
+ testRunCompleted: false,
289
+ client: null,
290
+ startTime: null,
291
+ testCases: new Map(),
292
+ recordedTestCases: [], // Store recorded test case data for GitHub comment
293
+ token: null,
294
+ detectedPlatform: null,
295
+ pendingTestCaseRecords: new Set(),
296
+ ciProvider: null,
297
+ gitInfo: {},
298
+ apiKey: null,
299
+ apiRoot: null,
300
+ // TestDriver options to pass to all instances
301
+ testDriverOptions: {},
302
+ // Dashcam URL tracking (in-memory, no files needed!)
303
+ dashcamUrls: new Map(), // testId -> [{url, platform, attempt}]
304
+ lastDashcamUrl: null, // Fallback for when test ID isn't available
305
+ // Suite-level test run tracking
306
+ suiteTestRuns: new Map(), // suiteId -> { runId, testRunDbId, token }
307
+ };
308
+
309
+ // Export functions that can be used by the reporter or tests
310
+ export function registerDashcamUrl(testId, url, platform, attempt) {
311
+ logger.debug(`Registering dashcam URL for test ${testId} (attempt ${attempt || 1}):`, url);
312
+ // Support multiple attempts per test - store as array
313
+ if (!pluginState.dashcamUrls.has(testId)) {
314
+ pluginState.dashcamUrls.set(testId, []);
315
+ }
316
+ pluginState.dashcamUrls.get(testId).push({ url, platform, attempt: attempt || 1 });
317
+ pluginState.lastDashcamUrl = url;
318
+ }
319
+
320
+ export function getDashcamUrl(testId) {
321
+ const entries = pluginState.dashcamUrls.get(testId);
322
+ if (!entries) return undefined;
323
+ // Return the last entry for backward compatibility (single URL callers)
324
+ return entries[entries.length - 1];
325
+ }
326
+
327
+ export function getAllDashcamUrls(testId) {
328
+ return pluginState.dashcamUrls.get(testId) || [];
329
+ }
330
+
331
+ export function clearDashcamUrls() {
332
+ pluginState.dashcamUrls.clear();
333
+ pluginState.lastDashcamUrl = null;
334
+ }
335
+
336
+ export function getSuiteTestRun(suiteId) {
337
+ return pluginState.suiteTestRuns.get(suiteId);
338
+ }
339
+
340
+ export function setSuiteTestRun(suiteId, runData) {
341
+ logger.debug(`Setting test run for suite ${suiteId}:`, runData);
342
+ pluginState.suiteTestRuns.set(suiteId, runData);
343
+ }
344
+
345
+ export function clearSuiteTestRun(suiteId) {
346
+ pluginState.suiteTestRuns.delete(suiteId);
347
+ }
348
+
349
+ export function getPluginState() {
350
+ return pluginState;
351
+ }
352
+
353
+ // Export API helper functions for direct use from tests
354
+ export async function authenticateWithApiKey(apiKey, apiRoot) {
355
+ if (!apiKey) {
356
+ const error = new Error(
357
+ "TD_API_KEY is not configured. Get your API key at https://console.testdriver.ai/team",
358
+ );
359
+ error.code = "MISSING_API_KEY";
360
+ error.isAuthError = true;
361
+ throw error;
362
+ }
363
+
364
+ const url = `${apiRoot}/auth/exchange-api-key`;
365
+ let response;
366
+
367
+ try {
368
+ response = await withTimeout(
369
+ fetch(url, {
370
+ method: "POST",
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ },
374
+ body: JSON.stringify({ apiKey }),
375
+ }),
376
+ 15000,
377
+ "Authentication",
378
+ );
379
+ } catch (fetchError) {
380
+ // Network-level error (fetch failed entirely)
381
+ const networkError = new Error(
382
+ `Unable to reach TestDriver API at ${apiRoot}. ` +
383
+ "Check your internet connection and try again.",
384
+ );
385
+ networkError.code = "NETWORK_ERROR";
386
+ networkError.isNetworkError = true;
387
+ networkError.originalError = fetchError;
388
+ throw networkError;
389
+ }
390
+
391
+ if (!response.ok) {
392
+ let data = {};
393
+ try {
394
+ data = await response.json();
395
+ } catch {
396
+ // Response wasn't JSON, use empty object
397
+ }
398
+
399
+ // Invalid API key (401)
400
+ if (response.status === 401) {
401
+ const authError = new Error(
402
+ data.message ||
403
+ "Invalid API key. Please check your TD_API_KEY and try again. " +
404
+ "Get your API key at https://console.testdriver.ai/team",
405
+ );
406
+ authError.code = data.error || "INVALID_API_KEY";
407
+ authError.isAuthError = true;
408
+ throw authError;
409
+ }
410
+
411
+ // Server errors (5xx) - API is down or having issues
412
+ if (response.status >= 500) {
413
+ const serverError = new Error(
414
+ data.message ||
415
+ `TestDriver API is currently unavailable (HTTP ${response.status}). Please try again later.`,
416
+ );
417
+ serverError.code = data.error || "API_UNAVAILABLE";
418
+ serverError.isServerError = true;
419
+ throw serverError;
420
+ }
421
+
422
+ // Rate limiting (429)
423
+ if (response.status === 429) {
424
+ const rateLimitError = new Error(
425
+ "Too many requests to TestDriver API. Please wait a moment and try again.",
426
+ );
427
+ rateLimitError.code = "RATE_LIMITED";
428
+ rateLimitError.isRateLimitError = true;
429
+ throw rateLimitError;
430
+ }
431
+
432
+ // Other HTTP errors
433
+ throw new Error(
434
+ `Authentication failed: ${response.status} ${response.statusText}` +
435
+ (data.message ? ` - ${data.message}` : ""),
436
+ );
437
+ }
438
+
439
+ const data = await response.json();
440
+ return data.token;
441
+ }
442
+
443
+ export async function createTestRunDirect(token, apiRoot, testRunData) {
444
+ const url = `${apiRoot}/api/v1/testdriver/test-run-create`;
445
+ const response = await withTimeout(
446
+ fetch(url, {
447
+ method: "POST",
448
+ headers: {
449
+ "Content-Type": "application/json",
450
+ Authorization: `Bearer ${token}`,
451
+ },
452
+ body: JSON.stringify(testRunData),
453
+ }),
454
+ 10000,
455
+ "Create Test Run",
456
+ );
457
+
458
+ if (!response.ok) {
459
+ const errorText = await response.text();
460
+ throw new Error(
461
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
462
+ );
463
+ }
464
+
465
+ return await response.json();
466
+ }
467
+
468
+ export async function recordTestCaseDirect(token, apiRoot, testCaseData) {
469
+ const url = `${apiRoot}/api/v1/testdriver/test-case-create`;
470
+ const response = await withTimeout(
471
+ fetch(url, {
472
+ method: "POST",
473
+ headers: {
474
+ "Content-Type": "application/json",
475
+ Authorization: `Bearer ${token}`,
476
+ },
477
+ body: JSON.stringify(testCaseData),
478
+ }),
479
+ 10000,
480
+ "Record Test Case",
481
+ );
482
+
483
+ if (!response.ok) {
484
+ const errorText = await response.text();
485
+ throw new Error(
486
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
487
+ );
488
+ }
489
+
490
+ return await response.json();
491
+ }
492
+
493
+ // Import TestDriverSDK using require to avoid esbuild transformation issues
494
+ const TestDriverSDK = require("../sdk.js");
495
+
496
+ /**
497
+ * Create a TestDriver client for use in beforeAll/beforeEach hooks
498
+ * This is for the shared instance pattern where one driver is used across multiple tests
499
+ *
500
+ * @param {object} options - TestDriver options
501
+ * @param {string} [options.apiKey] - TestDriver API key (defaults to process.env.TD_API_KEY)
502
+ * @param {boolean} [options.headless] - Run sandbox in headless mode
503
+ * @returns {Promise<TestDriver>} Connected TestDriver client instance
504
+ *
505
+ * @example
506
+ * let testdriver;
507
+ * beforeAll(async () => {
508
+ * testdriver = await createTestDriver({ headless: true });
509
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
510
+ * });
511
+ */
512
+ export async function createTestDriver(options = {}) {
513
+ // Get global plugin options if available
514
+ const pluginOptions =
515
+ globalThis.__testdriverPlugin?.state?.testDriverOptions || {};
516
+
517
+ // Merge options: plugin global options < test-specific options
518
+ const mergedOptions = { ...pluginOptions, ...options };
519
+
520
+ // Support TD_OS environment variable for specifying target OS (linux, mac, windows)
521
+ // Priority: test options > plugin options > environment variable > default (linux)
522
+ if (!mergedOptions.os && process.env.TD_OS) {
523
+ mergedOptions.os = process.env.TD_OS;
524
+ }
525
+
526
+ // Extract TestDriver-specific options
527
+ const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
528
+
529
+ // Build config for TestDriverSDK constructor
530
+ const config = { ...mergedOptions };
531
+ delete config.apiKey;
532
+
533
+ // Use TD_API_ROOT from environment if not provided in config
534
+ if (!config.apiRoot && process.env.TD_API_ROOT) {
535
+ config.apiRoot = process.env.TD_API_ROOT;
536
+ }
537
+
538
+ const testdriver = new TestDriverSDK(apiKey, config);
539
+
540
+ // Connect to sandbox
541
+ await testdriver.auth();
542
+ await testdriver.connect();
543
+
544
+ return testdriver;
545
+ }
546
+
547
+ /**
548
+ * Register a test with a shared TestDriver instance
549
+ * Call this at the start of each test to associate the test context with the driver
550
+ *
551
+ * @param {TestDriver} testdriver - TestDriver client instance from createTestDriver
552
+ * @param {object} context - Vitest test context (from async (context) => {})
553
+ *
554
+ * @example
555
+ * it("step01: verify login", async (context) => {
556
+ * registerTest(testdriver, context);
557
+ * const result = await testdriver.assert("login form visible");
558
+ * });
559
+ */
560
+ export function registerTest(testdriver, context) {
561
+ if (!testdriver) {
562
+ throw new Error("registerTest() requires a TestDriver instance");
563
+ }
564
+ if (!context || !context.task) {
565
+ throw new Error(
566
+ "registerTest() requires Vitest context. Pass the context parameter from your test function.",
567
+ );
568
+ }
569
+
570
+ testdriver.__vitestContext = context.task;
571
+ logger.debug(`Registered test: ${context.task.name}`);
572
+ }
573
+
574
+ /**
575
+ * Clean up a TestDriver client created with createTestDriver
576
+ * Call this in afterAll to properly disconnect and stop recordings
577
+ *
578
+ * @param {TestDriver} testdriver - TestDriver client instance
579
+ *
580
+ * @example
581
+ * afterAll(async () => {
582
+ * await cleanupTestDriver(testdriver);
583
+ * });
584
+ */
585
+ export async function cleanupTestDriver(testdriver) {
586
+ if (!testdriver) {
587
+ return;
588
+ }
589
+
590
+ try {
591
+ // Stop dashcam if it was started
592
+ if (testdriver._dashcam && testdriver._dashcam.recording) {
593
+ try {
594
+ const dashcamUrl = await testdriver.dashcam.stop();
595
+ const debugMode =
596
+ process.env.VERBOSE || process.env.TD_DEBUG;
597
+ if (debugMode) {
598
+ console.log("šŸŽ„ Dashcam URL:", dashcamUrl);
599
+ }
600
+
601
+ // Register dashcam URL in memory for the reporter
602
+ if (dashcamUrl && globalThis.__testdriverPlugin?.registerDashcamUrl) {
603
+ const testId = testdriver.__vitestContext?.id || "unknown";
604
+ const platform = testdriver.os || "linux";
605
+ globalThis.__testdriverPlugin.registerDashcamUrl(
606
+ testId,
607
+ dashcamUrl,
608
+ platform,
609
+ );
610
+ }
611
+ } catch (error) {
612
+ console.error("āŒ Failed to stop dashcam:", error.message);
613
+ if (
614
+ error.name === "NotFoundError" ||
615
+ error.responseData?.error === "NotFoundError"
616
+ ) {
617
+ console.log(
618
+ " ā„¹ļø Sandbox session already terminated - dashcam stop skipped",
619
+ );
620
+ }
621
+ }
622
+ }
623
+
624
+ await testdriver.disconnect();
625
+ } catch (error) {
626
+ console.error("Error disconnecting client:", error);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Handle process termination and mark test run as cancelled
632
+ */
633
+ async function handleProcessExit() {
634
+ logger.debug("handleProcessExit called");
635
+ logger.debug("testRun:", !!pluginState.testRun);
636
+ logger.debug("testRunId:", pluginState.testRunId);
637
+ logger.debug("testRunCompleted:", pluginState.testRunCompleted);
638
+
639
+ if (!pluginState.testRun || !pluginState.testRunId) {
640
+ logger.debug("No test run to cancel - skipping cleanup");
641
+ return;
642
+ }
643
+
644
+ // Prevent duplicate completion
645
+ if (pluginState.testRunCompleted) {
646
+ logger.debug("Test run already completed - skipping cancellation");
647
+ return;
648
+ }
649
+
650
+ logger.debug("Marking test run as cancelled...");
651
+
652
+ try {
653
+ const stats = {
654
+ totalTests: pluginState.testCases.size,
655
+ passedTests: 0,
656
+ failedTests: 0,
657
+ skippedTests: 0,
658
+ };
659
+
660
+ const completeData = {
661
+ runId: pluginState.testRunId,
662
+ status: "cancelled",
663
+ totalTests: stats.totalTests,
664
+ passedTests: stats.passedTests,
665
+ failedTests: stats.failedTests,
666
+ skippedTests: stats.skippedTests,
667
+ duration: Date.now() - pluginState.startTime,
668
+ };
669
+
670
+ // Update platform if detected
671
+ const platform = getPlatform();
672
+ if (platform) {
673
+ completeData.platform = platform;
674
+ }
675
+
676
+ logger.debug("Calling completeTestRun with:", JSON.stringify(completeData));
677
+ await completeTestRun(completeData);
678
+ pluginState.testRunCompleted = true;
679
+ logger.info("Test run marked as cancelled");
680
+ } catch (error) {
681
+ logger.error("Failed to mark test run as cancelled:", error.message);
682
+ }
683
+ }
684
+
685
+ // Set up process exit handlers
686
+ let exitHandlersRegistered = false;
687
+ let isExiting = false;
688
+ let isCancelling = false; // Track if we're in the process of cancelling due to SIGINT/SIGTERM
689
+
690
+ function registerExitHandlers() {
691
+ if (exitHandlersRegistered) return;
692
+ exitHandlersRegistered = true;
693
+
694
+ // Handle Ctrl+C - use 'once' and prepend to run before Vitest's handler
695
+ process.prependOnceListener("SIGINT", () => {
696
+ logger.debug("SIGINT received, cleaning up...");
697
+ if (isExiting) {
698
+ logger.debug("Already exiting, skipping duplicate handler");
699
+ return;
700
+ }
701
+ isExiting = true;
702
+ isCancelling = true; // Mark that we're cancelling
703
+
704
+ // Temporarily override process.exit to prevent Vitest from exiting before we're done
705
+ const originalExit = process.exit;
706
+ let exitCalled = false;
707
+ let exitCode = 130;
708
+
709
+ process.exit = (code) => {
710
+ if (!exitCalled) {
711
+ exitCalled = true;
712
+ exitCode = code ?? 130;
713
+ logger.debug(
714
+ `process.exit(${exitCode}) called, waiting for cleanup...`,
715
+ );
716
+ }
717
+ };
718
+
719
+ handleProcessExit()
720
+ .then(() => {
721
+ logger.debug("Cleanup completed successfully");
722
+ })
723
+ .catch((err) => {
724
+ logger.error("Error during SIGINT cleanup:", err.message);
725
+ })
726
+ .finally(() => {
727
+ logger.debug(`Exiting with code ${exitCode}`);
728
+ // Restore and call original exit
729
+ process.exit = originalExit;
730
+ process.exit(exitCode);
731
+ });
732
+ });
733
+
734
+ // Handle kill command
735
+ process.prependOnceListener("SIGTERM", () => {
736
+ logger.debug("SIGTERM received, cleaning up...");
737
+ if (isExiting) return;
738
+ isExiting = true;
739
+ isCancelling = true;
740
+
741
+ const originalExit = process.exit;
742
+ let exitCode = 143;
743
+
744
+ process.exit = (code) => {
745
+ exitCode = code ?? 143;
746
+ };
747
+
748
+ handleProcessExit()
749
+ .then(() => {
750
+ logger.debug("Cleanup completed successfully");
751
+ })
752
+ .catch((err) => {
753
+ logger.error("Error during SIGTERM cleanup:", err.message);
754
+ })
755
+ .finally(() => {
756
+ logger.debug(`Exiting with code ${exitCode}`);
757
+ process.exit = originalExit;
758
+ process.exit(exitCode);
759
+ });
760
+ });
761
+ }
762
+
763
+ /**
764
+ * Create the TestDriver Vitest plugin
765
+ * This sets up global state and provides the registration API
766
+ */
767
+ export default function testDriverPlugin(options = {}) {
768
+ // Store options but don't read env vars yet - they may not be loaded
769
+ // Environment variables will be read in onInit after setupFiles run
770
+ pluginState.apiRoot =
771
+ options.apiRoot ||
772
+ process.env.TD_API_ROOT ||
773
+ channelConfig.channels[channelConfig.active];
774
+ pluginState.ciProvider = detectCI();
775
+ pluginState.gitInfo = getGitInfo();
776
+
777
+ // Store TestDriver-specific options (excluding plugin-specific ones)
778
+ const { apiKey, apiRoot, ...testDriverOptions } = options;
779
+ pluginState.testDriverOptions = testDriverOptions;
780
+
781
+ // Register process exit handlers to handle cancellation
782
+ registerExitHandlers();
783
+
784
+ // Note: globalThis setup happens in vitestSetup.mjs for worker processes
785
+ logger.debug("TestDriver plugin initializing...");
786
+ logger.debug("API root:", pluginState.apiRoot);
787
+ logger.debug("API key from options:", !!options.apiKey);
788
+ logger.debug("API key from env (at config time):", !!process.env.TD_API_KEY);
789
+ logger.debug("CI Provider:", pluginState.ciProvider || "none");
790
+ if (Object.keys(testDriverOptions).length > 0) {
791
+ logger.debug("Global TestDriver options:", testDriverOptions);
792
+ }
793
+
794
+ // Create reporter instance
795
+ const reporter = new TestDriverReporter(options);
796
+
797
+ // Add name property for Vitest
798
+ reporter.name = "testdriver";
799
+
800
+ return reporter;
801
+ }
802
+
803
+ /**
804
+ * TestDriver Reporter Class
805
+ * Handles Vitest test lifecycle events
806
+ */
807
+ class TestDriverReporter {
808
+ constructor(options = {}) {
809
+ this.options = options;
810
+ logger.debug("Reporter created with options:", {
811
+ hasApiKey: !!options.apiKey,
812
+ hasApiRoot: !!options.apiRoot,
813
+ });
814
+ }
815
+
816
+ async onInit(ctx) {
817
+ this.ctx = ctx;
818
+ logger.debug("onInit called - UPDATED VERSION");
819
+
820
+ // Initialize Sentry for error reporting
821
+ initializeSentry();
822
+
823
+ // Store project root for making file paths relative
824
+ pluginState.projectRoot = ctx.config.root || process.cwd();
825
+ logger.debug("Project root:", pluginState.projectRoot);
826
+
827
+ // NOW read the API key and API root (after setupFiles have run, including dotenv/config)
828
+ pluginState.apiKey = this.options.apiKey || process.env.TD_API_KEY;
829
+ pluginState.apiRoot =
830
+ this.options.apiRoot ||
831
+ process.env.TD_API_ROOT ||
832
+ channelConfig.channels[channelConfig.active];
833
+ logger.debug("API key from options:", !!this.options.apiKey);
834
+ logger.debug("API key from env (at onInit):", !!process.env.TD_API_KEY);
835
+ logger.debug("API root from options:", this.options.apiRoot);
836
+ logger.debug("API root from env (at onInit):", process.env.TD_API_ROOT);
837
+ logger.debug("Final API key set:", !!pluginState.apiKey);
838
+ logger.debug("Final API root set:", pluginState.apiRoot);
839
+
840
+ // Initialize test run
841
+ await this.initializeTestRun();
842
+ }
843
+
844
+ async initializeTestRun() {
845
+ logger.debug("initializeTestRun called");
846
+ logger.debug("API key present:", !!pluginState.apiKey);
847
+ logger.debug("API root:", pluginState.apiRoot);
848
+
849
+ // Environment info is printed by the SDK when each test initializes,
850
+ // so we skip the duplicate banner here in the reporter.
851
+
852
+ // Check if we should enable the reporter
853
+ if (!pluginState.apiKey) {
854
+ logger.warn("No API key provided, skipping test recording");
855
+ logger.debug(
856
+ "API key sources - options:",
857
+ !!this.options.apiKey,
858
+ "env:",
859
+ !!process.env.TD_API_KEY,
860
+ );
861
+ return;
862
+ }
863
+
864
+ try {
865
+ // Exchange API key for JWT token
866
+ logger.debug("Authenticating with API...");
867
+ await authenticate();
868
+ logger.debug("Authentication successful, token received");
869
+
870
+ // Generate unique run ID
871
+ pluginState.testRunId = generateRunId();
872
+ pluginState.startTime = Date.now();
873
+ pluginState.testRunCompleted = false; // Reset completion flag
874
+
875
+ // Create test run via direct API call
876
+ const testRunData = {
877
+ runId: pluginState.testRunId,
878
+ suiteName: getSuiteName(),
879
+ ...pluginState.gitInfo,
880
+ };
881
+
882
+ // Session ID will be added from the first test result file that includes it
883
+
884
+ // Only add ciProvider if it's not null
885
+ if (pluginState.ciProvider) {
886
+ testRunData.ciProvider = pluginState.ciProvider;
887
+ }
888
+
889
+ // Platform will be set from the first test result file
890
+ // Default to linux if no tests write platform info
891
+ testRunData.platform = "linux";
892
+
893
+ // Send version metadata
894
+ testRunData.nodeVersion = process.version;
895
+ const tdVer = resolveTestDriverVersion();
896
+ if (tdVer) {
897
+ testRunData.testDriverVersion = tdVer;
898
+ }
899
+ const vitestVer = resolveVitestVersion();
900
+ if (vitestVer) {
901
+ testRunData.vitestVersion = vitestVer;
902
+ }
903
+
904
+ logger.debug("Creating test run with data:", JSON.stringify(testRunData));
905
+ pluginState.testRun = await createTestRun(testRunData);
906
+ logger.debug("Test run created:", JSON.stringify(pluginState.testRun));
907
+
908
+ // Store in environment variables for worker processes to access
909
+ process.env.TD_TEST_RUN_ID = pluginState.testRunId;
910
+ process.env.TD_TEST_RUN_DB_ID = pluginState.testRun.data?.id || "";
911
+ process.env.TD_TEST_RUN_TOKEN = pluginState.token;
912
+
913
+ // Also store in shared state module (won't work across processes but good for main)
914
+ setTestRunInfo({
915
+ testRun: pluginState.testRun,
916
+ testRunId: pluginState.testRunId,
917
+ token: pluginState.token,
918
+ apiKey: pluginState.apiKey,
919
+ apiRoot: pluginState.apiRoot,
920
+ startTime: pluginState.startTime,
921
+ });
922
+ } catch (error) {
923
+ logger.error("Failed to initialize:", error.message);
924
+ pluginState.apiKey = null;
925
+ pluginState.token = null;
926
+ }
927
+ }
928
+
929
+ async onTestRunEnd(testModules, unhandledErrors, reason) {
930
+ logger.debug("onTestRunEnd called with reason:", reason);
931
+ logger.debug("API key present:", !!pluginState.apiKey);
932
+ logger.debug("Test run present:", !!pluginState.testRun);
933
+ logger.debug("Test run ID:", pluginState.testRunId);
934
+ logger.debug("isCancelling:", isCancelling);
935
+ logger.debug("testRunCompleted:", pluginState.testRunCompleted);
936
+
937
+ // If we're cancelling due to SIGINT/SIGTERM, skip - handleProcessExit will handle it
938
+ if (isCancelling) {
939
+ logger.debug(
940
+ "Cancellation in progress via signal handler, skipping onTestRunEnd",
941
+ );
942
+ return;
943
+ }
944
+
945
+ // If already completed (by handleProcessExit), skip
946
+ if (pluginState.testRunCompleted) {
947
+ logger.debug("Test run already completed, skipping");
948
+ return;
949
+ }
950
+
951
+ if (!pluginState.apiKey) {
952
+ logger.warn(
953
+ "Skipping completion - no API key (was it cleared after init failure?)",
954
+ );
955
+ return;
956
+ }
957
+
958
+ if (!pluginState.testRun) {
959
+ logger.warn(
960
+ "Skipping completion - no test run created (check initialization logs)",
961
+ );
962
+ return;
963
+ }
964
+
965
+ logger.debug("Completing test run...");
966
+
967
+ try {
968
+ // Calculate statistics from testModules
969
+ const stats = calculateStatsFromModules(testModules);
970
+
971
+ logger.debug("Stats:", stats);
972
+
973
+ // Determine overall status based on stats (not reason, which is unreliable in parallel runs)
974
+ let status = "passed";
975
+ if (stats.failedTests > 0) {
976
+ status = "failed";
977
+ } else if (reason === "interrupted") {
978
+ status = "cancelled";
979
+ } else if (stats.totalTests === 0) {
980
+ status = "cancelled";
981
+ } else if (stats.passedTests === 0 && stats.skippedTests === 0) {
982
+ // No tests actually ran (all were filtered/excluded)
983
+ status = "cancelled";
984
+ }
985
+
986
+ // Complete test run via API
987
+ logger.debug(
988
+ `Completing test run ${pluginState.testRunId} with status: ${status}`,
989
+ );
990
+
991
+ const completeData = {
992
+ runId: pluginState.testRunId,
993
+ status,
994
+ totalTests: stats.totalTests,
995
+ passedTests: stats.passedTests,
996
+ failedTests: stats.failedTests,
997
+ skippedTests: stats.skippedTests,
998
+ duration: Date.now() - pluginState.startTime,
999
+ };
1000
+
1001
+ // Update platform if detected from test results
1002
+ const platform = getPlatform();
1003
+ logger.debug(
1004
+ `Platform detection result: ${platform}, detectedPlatform in state: ${pluginState.detectedPlatform}`,
1005
+ );
1006
+ if (platform) {
1007
+ completeData.platform = platform;
1008
+ logger.debug(`Updating test run with platform: ${platform}`);
1009
+ } else {
1010
+ logger.warn(
1011
+ `No platform detected, test run will keep default platform`,
1012
+ );
1013
+ }
1014
+
1015
+ // Wait for any pending operations (shouldn't be any, but just in case)
1016
+ if (pluginState.pendingTestCaseRecords.size > 0) {
1017
+ logger.debug(
1018
+ `Waiting for ${pluginState.pendingTestCaseRecords.size} pending operations...`,
1019
+ );
1020
+ await Promise.all(Array.from(pluginState.pendingTestCaseRecords));
1021
+ }
1022
+
1023
+ // Test cases are reported directly from teardownTest
1024
+ logger.debug("Calling completeTestRun API...");
1025
+ logger.debug("Complete data:", JSON.stringify(completeData));
1026
+
1027
+ const completeResponse = await completeTestRun(completeData);
1028
+ logger.debug("API response:", JSON.stringify(completeResponse));
1029
+
1030
+ // Mark test run as completed to prevent duplicate completion
1031
+ pluginState.testRunCompleted = true;
1032
+
1033
+ // Output the test run URL for CI to capture
1034
+ const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
1035
+ const consoleUrl = getConsoleUrl(pluginState.apiRoot);
1036
+ if (testRunDbId) {
1037
+ const testRunUrl = `${consoleUrl}/runs/${testRunDbId}`;
1038
+ logger.info(`View test run: ${testRunUrl}`);
1039
+ // Output in a parseable format for CI
1040
+ console.log(`TESTDRIVER_RUN_URL=${testRunUrl}`);
1041
+
1042
+ // Post GitHub comment if in CI environment
1043
+ await postGitHubCommentIfEnabled(testRunUrl, stats, completeData);
1044
+ }
1045
+
1046
+ logger.info(
1047
+ `Test run completed: ${stats.passedTests}/${stats.totalTests} passed`,
1048
+ );
1049
+ } catch (error) {
1050
+ logger.error("Failed to complete test run:", error.message);
1051
+ logger.debug("Error stack:", error.stack);
1052
+ } finally {
1053
+ // Flush any pending Sentry events before process exits
1054
+ await flushSentry();
1055
+ }
1056
+ }
1057
+
1058
+ onTestCaseReady(test) {
1059
+ if (!pluginState.apiKey || !pluginState.testRun) return;
1060
+
1061
+ pluginState.testCases.set(test.id, {
1062
+ test,
1063
+ startTime: Date.now(),
1064
+ });
1065
+ }
1066
+
1067
+ async onTestCaseResult(test) {
1068
+ if (!pluginState.apiKey || !pluginState.testRun) return;
1069
+
1070
+ const result = test.result();
1071
+ const status =
1072
+ result.state === "passed"
1073
+ ? "passed"
1074
+ : result.state === "skipped"
1075
+ ? "skipped"
1076
+ : "failed";
1077
+
1078
+ logger.debug(`Test case completed: ${test.name} (${status})`);
1079
+
1080
+ // Calculate duration from tracked start time
1081
+ const testCase = pluginState.testCases.get(test.id);
1082
+ const duration = testCase ? Date.now() - testCase.startTime : 0;
1083
+
1084
+ logger.debug(
1085
+ `Calculated duration: ${duration}ms (startTime: ${testCase?.startTime}, now: ${Date.now()})`,
1086
+ );
1087
+
1088
+ // Read test metadata from Vitest's task.meta (set in test hooks)
1089
+ const meta = test.meta();
1090
+ logger.debug(`Test meta for ${test.id}:`, meta);
1091
+
1092
+ const dashcamUrl = meta.dashcamUrl || null;
1093
+ const dashcamUrls = meta.dashcamUrls || []; // Per-attempt URLs
1094
+ const sessionId = meta.sessionId || null;
1095
+ const platform = meta.platform || null;
1096
+ const sandboxId = meta.sandboxId || null;
1097
+ let testFile = meta.testFile || "unknown";
1098
+ const testOrder = meta.testOrder !== undefined ? meta.testOrder : 0;
1099
+
1100
+ // If testFile not in meta, fallback to test object properties
1101
+ if (testFile === "unknown") {
1102
+ const absolutePath =
1103
+ test.module?.task?.filepath ||
1104
+ test.module?.file?.filepath ||
1105
+ test.module?.file?.name ||
1106
+ test.file?.filepath ||
1107
+ test.file?.name ||
1108
+ test.suite?.file?.filepath ||
1109
+ test.suite?.file?.name ||
1110
+ test.location?.file ||
1111
+ "unknown";
1112
+ testFile =
1113
+ pluginState.projectRoot && absolutePath !== "unknown"
1114
+ ? path.relative(pluginState.projectRoot, absolutePath)
1115
+ : absolutePath;
1116
+ logger.debug(`Resolved testFile from fallback: ${testFile}`);
1117
+ }
1118
+
1119
+ // Update test run platform from first test that reports it
1120
+ if (platform && !pluginState.detectedPlatform) {
1121
+ pluginState.detectedPlatform = platform;
1122
+ }
1123
+
1124
+ // Get test run info from environment variables
1125
+ const testRunId = process.env.TD_TEST_RUN_ID;
1126
+ const token = process.env.TD_TEST_RUN_TOKEN;
1127
+
1128
+ if (!testRunId || !token) {
1129
+ logger.warn(
1130
+ `Test run not initialized, skipping test case recording for: ${test.name}`,
1131
+ );
1132
+ return;
1133
+ }
1134
+
1135
+ try {
1136
+ let errorMessage = null;
1137
+ let errorStack = null;
1138
+
1139
+ if (
1140
+ result.state === "failed" &&
1141
+ result.errors &&
1142
+ result.errors.length > 0
1143
+ ) {
1144
+ const error = result.errors[0];
1145
+ errorMessage = error.message;
1146
+ errorStack = error.stack;
1147
+
1148
+ // Note: We do NOT report test failures to Sentry.
1149
+ // Test failures are expected behavior (they indicate a test found a bug).
1150
+ // We only want actual SDK crashes and exceptions reported to Sentry.
1151
+ }
1152
+
1153
+ const suiteName = test.suite?.name;
1154
+ const startTime = Date.now() - duration; // Calculate start time from duration
1155
+ // In Vitest v4, retryCount is on diagnostic(), not result()
1156
+ // result() only returns { state, errors }, while diagnostic() has retryCount, duration, etc.
1157
+ const diagnostic = test.diagnostic?.();
1158
+ const retryCount = diagnostic?.retryCount || 0;
1159
+ const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
1160
+ const consoleUrl = getConsoleUrl(pluginState.apiRoot);
1161
+ const hasRetries = retryCount > 0 && dashcamUrls.length > 1;
1162
+
1163
+ // Record a single test case with all metadata
1164
+ const testCaseData = {
1165
+ runId: testRunId,
1166
+ testName: test.name,
1167
+ testFile: testFile,
1168
+ testOrder: testOrder,
1169
+ status,
1170
+ startTime: startTime,
1171
+ endTime: Date.now(),
1172
+ duration: duration,
1173
+ retries: retryCount,
1174
+ };
1175
+
1176
+ // Add sessionId if available
1177
+ if (sessionId) {
1178
+ testCaseData.sessionId = sessionId;
1179
+ }
1180
+
1181
+ // Only include replayUrl if we have a valid dashcam URL
1182
+ if (dashcamUrl) {
1183
+ testCaseData.replayUrl = dashcamUrl;
1184
+ }
1185
+
1186
+ // Include per-attempt replay URLs for retry visibility
1187
+ if (dashcamUrls.length > 0) {
1188
+ const attemptUrls = dashcamUrls
1189
+ .map(a => ({ attempt: a.attempt, url: a.url || null, sessionId: a.sessionId || null }));
1190
+ testCaseData.replayUrls = attemptUrls;
1191
+ }
1192
+
1193
+ if (suiteName) testCaseData.suiteName = suiteName;
1194
+ if (errorMessage) testCaseData.errorMessage = errorMessage;
1195
+ if (errorStack) testCaseData.errorStack = errorStack;
1196
+
1197
+ logger.debug(
1198
+ `Recording test case: ${test.name} (${status}) with testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms, replay: ${dashcamUrl ? "yes" : "no"}`,
1199
+ );
1200
+
1201
+ const testCaseResponse = await recordTestCaseDirect(
1202
+ token,
1203
+ pluginState.apiRoot,
1204
+ testCaseData,
1205
+ );
1206
+
1207
+ const testCaseDbId = testCaseResponse.data?.id;
1208
+
1209
+ // Store test case data for GitHub comment generation
1210
+ pluginState.recordedTestCases.push({
1211
+ ...testCaseData,
1212
+ id: testCaseDbId,
1213
+ });
1214
+
1215
+ console.log("");
1216
+ console.log(
1217
+ chalk.cyan(`šŸ”— Test Report: ${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}`),
1218
+ );
1219
+ console.log("");
1220
+
1221
+ // If there were retries, list all per-attempt dashcam URLs for debugging
1222
+ if (hasRetries) {
1223
+ const validAttempts = dashcamUrls.filter(a => a.url);
1224
+ if (validAttempts.length > 0) {
1225
+ console.log(`šŸ“‹ Retry attempts (${dashcamUrls.length} total):`);
1226
+ for (const attempt of validAttempts) {
1227
+ console.log(` Attempt ${attempt.attempt}: ${attempt.url}`);
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ // Output parseable format for docs generation (examples only)
1233
+ if (testFile.startsWith("examples/")) {
1234
+ const testFileName = path.basename(testFile);
1235
+ console.log(
1236
+ `TESTDRIVER_EXAMPLE_URL::${testFileName}::${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}`,
1237
+ );
1238
+ }
1239
+ } catch (error) {
1240
+ logger.error("Failed to report test case:", error.message);
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ // ============================================================================
1246
+ // Helper Functions
1247
+ // ============================================================================
1248
+
1249
+ /**
1250
+ * Maps an API root URL to its corresponding web console URL.
1251
+ * The API and web console are served from different domains/ports.
1252
+ *
1253
+ * @param {string} apiRoot - The API root URL (e.g., https://api.testdriver.ai)
1254
+ * @returns {string} The corresponding web console URL
1255
+ */
1256
+ function getConsoleUrl(apiRoot) {
1257
+ // Explicit override — use TD_CONSOLE_URL when deliberately set
1258
+ if (process.env.TD_CONSOLE_URL) return process.env.TD_CONSOLE_URL;
1259
+
1260
+ if (!apiRoot) return "https://console.testdriver.ai";
1261
+
1262
+ // Fly.io: swap "-api" for "-web" in the hostname
1263
+ // e.g. preview-138-api.fly.dev -> preview-138-web.fly.dev
1264
+ // td-test-api.fly.dev -> td-test-web.fly.dev
1265
+ const flyMatch = apiRoot.match(/https:\/\/([\w-]+)-api\.fly\.dev/);
1266
+ if (flyMatch) {
1267
+ return `https://${flyMatch[1]}-web.fly.dev`;
1268
+ }
1269
+
1270
+ // Known channel API URLs -> console equivalents
1271
+ // e.g. https://api-canary.testdriver.ai -> https://console-canary.testdriver.ai
1272
+ for (const url of Object.values(channelConfig.channels)) {
1273
+ if (url === apiRoot) {
1274
+ return url.replace("api", "console").replace("1337", "3001");
1275
+ }
1276
+ }
1277
+
1278
+ // Local development
1279
+ if (apiRoot.includes("ngrok.io") || apiRoot.includes("trycloudflare.com") || apiRoot.includes("localhost")) {
1280
+ return "http://localhost:3001";
1281
+ }
1282
+
1283
+ // Render PR previews (legacy)
1284
+ const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
1285
+ if (renderPrMatch) {
1286
+ const [, prefix, suffix, prSuffix] = renderPrMatch;
1287
+ const webPrefix = (prefix === 'testdriver' && suffix) ? 'web' + suffix : prefix + '-web';
1288
+ return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
1289
+ }
1290
+
1291
+ return apiRoot;
1292
+ }
1293
+
1294
+ function generateRunId() {
1295
+ return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
1296
+ }
1297
+
1298
+ function getSuiteName() {
1299
+ return process.env.npm_package_name || path.basename(process.cwd());
1300
+ }
1301
+
1302
+ function getPlatform() {
1303
+ // First try to get platform from SDK client detected during test execution
1304
+ if (pluginState.detectedPlatform) {
1305
+ logger.debug(
1306
+ `Using platform from SDK client: ${pluginState.detectedPlatform}`,
1307
+ );
1308
+ return pluginState.detectedPlatform;
1309
+ }
1310
+
1311
+ // Try to get platform from dashcam URLs (registered during test cleanup)
1312
+ for (const [, entries] of pluginState.dashcamUrls) {
1313
+ // entries is now an array of {url, platform, attempt}
1314
+ const arr = Array.isArray(entries) ? entries : [entries];
1315
+ for (const data of arr) {
1316
+ if (data.platform) {
1317
+ logger.debug(
1318
+ `Using platform from dashcam URL registration: ${data.platform}`,
1319
+ );
1320
+ return data.platform;
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ logger.debug("Platform not yet detected from client");
1326
+ return null;
1327
+ }
1328
+
1329
+ function calculateStatsFromModules(testModules) {
1330
+ let totalTests = 0;
1331
+ let passedTests = 0;
1332
+ let failedTests = 0;
1333
+ let skippedTests = 0;
1334
+
1335
+ // Guard against corrupt or circular test tree structures
1336
+ // (can happen with --sequence.concurrent in some Vitest versions)
1337
+ const seen = new Set();
1338
+
1339
+ for (const testModule of testModules) {
1340
+ try {
1341
+ for (const testCase of testModule.children.allTests()) {
1342
+ // Deduplicate - skip if we've already counted this test
1343
+ if (seen.has(testCase.id)) continue;
1344
+ seen.add(testCase.id);
1345
+
1346
+ const result = testCase.result();
1347
+ if (result.state === "passed") {
1348
+ passedTests++;
1349
+ totalTests++;
1350
+ } else if (result.state === "failed") {
1351
+ failedTests++;
1352
+ totalTests++;
1353
+ } else if (result.state === "skipped") {
1354
+ skippedTests++;
1355
+ }
1356
+ }
1357
+ } catch (err) {
1358
+ logger.warn(`Error calculating stats for module: ${err.message}`);
1359
+ }
1360
+ }
1361
+
1362
+ return { totalTests, passedTests, failedTests, skippedTests };
1363
+ }
1364
+
1365
+ function detectCI() {
1366
+ if (process.env.GITHUB_ACTIONS) return "github";
1367
+ if (process.env.GITLAB_CI) return "gitlab";
1368
+ if (process.env.CIRCLECI) return "circle";
1369
+ if (process.env.TRAVIS) return "travis";
1370
+ if (process.env.JENKINS_URL) return "jenkins";
1371
+ if (process.env.BUILDKITE) return "buildkite";
1372
+ return null;
1373
+ }
1374
+
1375
+ function getGitInfo() {
1376
+ const info = {};
1377
+
1378
+ if (process.env.GITHUB_ACTIONS) {
1379
+ if (process.env.GITHUB_REPOSITORY)
1380
+ info.repo = process.env.GITHUB_REPOSITORY;
1381
+ if (process.env.GITHUB_REF_NAME) info.branch = process.env.GITHUB_REF_NAME;
1382
+ if (process.env.GITHUB_SHA) info.commit = process.env.GITHUB_SHA;
1383
+ if (process.env.GITHUB_ACTOR) info.author = process.env.GITHUB_ACTOR;
1384
+ } else if (process.env.GITLAB_CI) {
1385
+ if (process.env.CI_PROJECT_PATH) info.repo = process.env.CI_PROJECT_PATH;
1386
+ if (process.env.CI_COMMIT_BRANCH)
1387
+ info.branch = process.env.CI_COMMIT_BRANCH;
1388
+ if (process.env.CI_COMMIT_SHA) info.commit = process.env.CI_COMMIT_SHA;
1389
+ if (process.env.GITLAB_USER_LOGIN)
1390
+ info.author = process.env.GITLAB_USER_LOGIN;
1391
+ } else if (process.env.CIRCLECI) {
1392
+ if (
1393
+ process.env.CIRCLE_PROJECT_USERNAME &&
1394
+ process.env.CIRCLE_PROJECT_REPONAME
1395
+ ) {
1396
+ info.repo = `${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`;
1397
+ }
1398
+ if (process.env.CIRCLE_BRANCH) info.branch = process.env.CIRCLE_BRANCH;
1399
+ if (process.env.CIRCLE_SHA1) info.commit = process.env.CIRCLE_SHA1;
1400
+ if (process.env.CIRCLE_USERNAME) info.author = process.env.CIRCLE_USERNAME;
1401
+ }
1402
+
1403
+ // If not in CI or if commit info is missing, try to get it from local git
1404
+ if (!info.commit) {
1405
+ try {
1406
+ info.commit = execSync("git rev-parse HEAD", {
1407
+ encoding: "utf8",
1408
+ stdio: ["pipe", "pipe", "ignore"],
1409
+ }).trim();
1410
+ logger.debug("Git commit from local:", info.commit);
1411
+ } catch (e) {
1412
+ logger.debug("Failed to get git commit:", e.message);
1413
+ }
1414
+ }
1415
+
1416
+ if (!info.branch) {
1417
+ try {
1418
+ info.branch = execSync("git rev-parse --abbrev-ref HEAD", {
1419
+ encoding: "utf8",
1420
+ stdio: ["pipe", "pipe", "ignore"],
1421
+ }).trim();
1422
+ logger.debug("Git branch from local:", info.branch);
1423
+ } catch (e) {
1424
+ logger.debug("Failed to get git branch:", e.message);
1425
+ }
1426
+ }
1427
+
1428
+ if (!info.author) {
1429
+ try {
1430
+ info.author = execSync("git config user.name", {
1431
+ encoding: "utf8",
1432
+ stdio: ["pipe", "pipe", "ignore"],
1433
+ }).trim();
1434
+ logger.debug("Git author from local:", info.author);
1435
+ } catch (e) {
1436
+ logger.debug("Failed to get git author:", e.message);
1437
+ }
1438
+ }
1439
+
1440
+ if (!info.repo) {
1441
+ try {
1442
+ const remoteUrl = execSync("git config --get remote.origin.url", {
1443
+ encoding: "utf8",
1444
+ stdio: ["pipe", "pipe", "ignore"],
1445
+ }).trim();
1446
+
1447
+ // Extract repo from git URL (supports both SSH and HTTPS)
1448
+ // SSH: git@github.com:user/repo.git
1449
+ // HTTPS: https://github.com/user/repo.git
1450
+ const match = remoteUrl.match(/[:/]([^/:]+\/[^/:]+?)(\.git)?$/);
1451
+ if (match) {
1452
+ info.repo = match[1];
1453
+ logger.debug("Git repo from local:", info.repo);
1454
+ }
1455
+ } catch (e) {
1456
+ logger.debug("Failed to get git repo:", e.message);
1457
+ }
1458
+ }
1459
+
1460
+ logger.debug("Collected git info:", info);
1461
+ return info;
1462
+ }
1463
+
1464
+ // ============================================================================
1465
+ // GitHub Comment Helper
1466
+ // ============================================================================
1467
+
1468
+ /**
1469
+ * Extract PR number from GitHub Actions environment
1470
+ * Checks multiple sources: env vars, event file, and GITHUB_REF
1471
+ * @returns {string|null} PR number or null if not found
1472
+ */
1473
+ function extractPRNumber() {
1474
+ // Try direct environment variables first
1475
+ let prNumber =
1476
+ process.env.GITHUB_PR_NUMBER ||
1477
+ process.env.TD_GITHUB_PR ||
1478
+ process.env.PR_NUMBER;
1479
+
1480
+ if (prNumber) {
1481
+ return prNumber;
1482
+ }
1483
+
1484
+ // Try to extract from GitHub Actions event path
1485
+ if (process.env.GITHUB_EVENT_PATH) {
1486
+ try {
1487
+ const eventData = JSON.parse(
1488
+ fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"),
1489
+ );
1490
+ if (eventData.pull_request?.number) {
1491
+ return String(eventData.pull_request.number);
1492
+ }
1493
+ } catch (err) {
1494
+ logger.debug("Could not read GitHub event file:", err.message);
1495
+ }
1496
+ }
1497
+
1498
+ // Try to extract from GITHUB_REF (refs/pull/123/merge or refs/pull/123/head)
1499
+ if (process.env.GITHUB_REF) {
1500
+ const match = process.env.GITHUB_REF.match(
1501
+ /refs\/pull\/(\d+)\/(merge|head)/,
1502
+ );
1503
+ if (match) {
1504
+ return match[1];
1505
+ }
1506
+ }
1507
+
1508
+ return null;
1509
+ }
1510
+
1511
+ /**
1512
+ * Post GitHub comment with test results if enabled
1513
+ * Checks for GitHub token and PR number in environment variables
1514
+ * @param {string} testRunUrl - URL to the test run
1515
+ * @param {Object} stats - Test statistics
1516
+ * @param {Object} completeData - Test run completion data
1517
+ */
1518
+ async function postGitHubCommentIfEnabled(testRunUrl, stats, completeData) {
1519
+ try {
1520
+ // Check if GitHub comments are explicitly disabled
1521
+ if (process.env.TESTDRIVER_SKIP_GITHUB_COMMENT === "true") {
1522
+ logger.debug(
1523
+ "GitHub comments disabled via TESTDRIVER_SKIP_GITHUB_COMMENT",
1524
+ );
1525
+ return;
1526
+ }
1527
+
1528
+ // Check if GitHub comment posting is enabled
1529
+ const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
1530
+ const prNumber = extractPRNumber();
1531
+ const commitSha = process.env.GITHUB_SHA || pluginState.gitInfo.commit;
1532
+
1533
+ // Only post if we have a token and either a PR number or commit SHA
1534
+ if (!githubToken) {
1535
+ logger.debug("GitHub token not found, skipping comment posting");
1536
+ return;
1537
+ }
1538
+
1539
+ if (!prNumber && !commitSha) {
1540
+ logger.debug(
1541
+ "Neither PR number nor commit SHA found, skipping comment posting",
1542
+ );
1543
+ return;
1544
+ }
1545
+
1546
+ // Extract owner/repo from git info
1547
+ const repo = pluginState.gitInfo.repo;
1548
+ if (!repo) {
1549
+ logger.warn("Repository info not available, skipping GitHub comment");
1550
+ return;
1551
+ }
1552
+
1553
+ const [owner, repoName] = repo.split("/");
1554
+ if (!owner || !repoName) {
1555
+ logger.warn("Invalid repository format, expected owner/repo");
1556
+ return;
1557
+ }
1558
+
1559
+ logger.debug("Preparing GitHub comment...");
1560
+
1561
+ // Prepare test run data for comment
1562
+ const testRunData = {
1563
+ runId: pluginState.testRunId,
1564
+ status: completeData.status,
1565
+ totalTests: stats.totalTests,
1566
+ passedTests: stats.passedTests,
1567
+ failedTests: stats.failedTests,
1568
+ skippedTests: stats.skippedTests,
1569
+ duration: completeData.duration,
1570
+ testRunUrl,
1571
+ platform:
1572
+ completeData.platform || pluginState.detectedPlatform || "unknown",
1573
+ branch: pluginState.gitInfo.branch || "unknown",
1574
+ commit: commitSha || "unknown",
1575
+ };
1576
+
1577
+ // Use recorded test cases from pluginState
1578
+ const testCases = pluginState.recordedTestCases || [];
1579
+
1580
+ logger.info(
1581
+ `Posting GitHub comment with ${testCases.length} test cases...`,
1582
+ );
1583
+
1584
+ // Post or update GitHub comment
1585
+ const githubOptions = {
1586
+ token: githubToken,
1587
+ owner,
1588
+ repo: repoName,
1589
+ prNumber: prNumber ? parseInt(prNumber, 10) : undefined,
1590
+ commitSha: commitSha,
1591
+ };
1592
+
1593
+ const comment = await postOrUpdateTestResults(
1594
+ testRunData,
1595
+ testCases,
1596
+ githubOptions,
1597
+ );
1598
+ logger.info(`āœ… GitHub comment posted: ${comment.html_url}`);
1599
+ console.log(`\nšŸ”— GitHub Comment: ${comment.html_url}\n`);
1600
+ } catch (error) {
1601
+ logger.warn("Failed to post GitHub comment:", error.message);
1602
+ logger.debug("GitHub comment error stack:", error.stack);
1603
+ }
1604
+ }
1605
+
1606
+ // ============================================================================
1607
+ // API Methods
1608
+ // ============================================================================
1609
+
1610
+ async function authenticate() {
1611
+ const url = `${pluginState.apiRoot}/auth/exchange-api-key`;
1612
+ const response = await withTimeout(
1613
+ fetch(url, {
1614
+ method: "POST",
1615
+ headers: {
1616
+ "Content-Type": "application/json",
1617
+ },
1618
+ body: JSON.stringify({
1619
+ apiKey: pluginState.apiKey,
1620
+ }),
1621
+ }),
1622
+ 10000,
1623
+ "Internal Authentication",
1624
+ );
1625
+
1626
+ if (!response.ok) {
1627
+ throw new Error(
1628
+ `Authentication failed: ${response.status} ${response.statusText}`,
1629
+ );
1630
+ }
1631
+
1632
+ const data = await response.json();
1633
+ pluginState.token = data.token;
1634
+ }
1635
+
1636
+ async function createTestRun(data) {
1637
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-create`;
1638
+ const response = await withTimeout(
1639
+ fetch(url, {
1640
+ method: "POST",
1641
+ headers: {
1642
+ "Content-Type": "application/json",
1643
+ Authorization: `Bearer ${pluginState.token}`,
1644
+ },
1645
+ body: JSON.stringify(data),
1646
+ }),
1647
+ 10000,
1648
+ "Internal Create Test Run",
1649
+ );
1650
+
1651
+ if (!response.ok) {
1652
+ const errorText = await response.text();
1653
+ throw new Error(
1654
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
1655
+ );
1656
+ }
1657
+
1658
+ return await response.json();
1659
+ }
1660
+
1661
+ async function completeTestRun(data) {
1662
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-complete`;
1663
+ logger.debug(`completeTestRun: POSTing to ${url}`);
1664
+
1665
+ try {
1666
+ const response = await withTimeout(
1667
+ fetch(url, {
1668
+ method: "POST",
1669
+ headers: {
1670
+ "Content-Type": "application/json",
1671
+ Authorization: `Bearer ${pluginState.token}`,
1672
+ },
1673
+ body: JSON.stringify(data),
1674
+ }),
1675
+ 10000,
1676
+ "Internal Complete Test Run",
1677
+ );
1678
+
1679
+ logger.debug(`completeTestRun: Response status ${response.status}`);
1680
+
1681
+ if (!response.ok) {
1682
+ const errorText = await response.text();
1683
+ throw new Error(
1684
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
1685
+ );
1686
+ }
1687
+
1688
+ const result = await response.json();
1689
+ logger.debug(`completeTestRun: Success`);
1690
+ return result;
1691
+ } catch (error) {
1692
+ logger.error(`completeTestRun: Error - ${error.message}`);
1693
+ throw error;
1694
+ }
1695
+ }
1696
+
1697
+ // Global state setup moved to setup file (vitestSetup.mjs)
1698
+ // The setup file imports the exported functions and makes them available globally in worker processes