testdriverai 6.2.2 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/.github/workflows/acceptance-linux.yml +75 -0
  2. package/.github/workflows/acceptance-sdk-tests.yml +133 -0
  3. package/.vscode/settings.json +5 -1
  4. package/AGENTS.md +550 -0
  5. package/CODEOWNERS +0 -1
  6. package/README.md +126 -0
  7. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  8. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  9. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  10. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  11. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  12. package/_testdriver/lifecycle/prerun.yaml +15 -0
  13. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  14. package/agent/index.js +300 -85
  15. package/agent/interface.js +15 -0
  16. package/agent/lib/cache.js +142 -0
  17. package/agent/lib/commander.js +1 -39
  18. package/agent/lib/commands.js +910 -296
  19. package/agent/lib/redraw.js +129 -41
  20. package/agent/lib/sandbox.js +29 -6
  21. package/agent/lib/sdk.js +22 -0
  22. package/agent/lib/system.js +0 -3
  23. package/agent/lib/validation.js +1 -7
  24. package/debug-locate-response.js +82 -0
  25. package/debugger/index.html +15 -4
  26. package/docs/ARCHITECTURE.md +424 -0
  27. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  28. package/docs/MIGRATION.md +425 -0
  29. package/docs/PRESETS.md +210 -0
  30. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  31. package/docs/SDK_AWESOME_LOGS.md +468 -0
  32. package/docs/TEST_RECORDING.md +388 -0
  33. package/docs/docs.json +286 -152
  34. package/docs/guide/best-practices-polling.mdx +154 -0
  35. package/docs/sdk-browser-rendering.md +167 -0
  36. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  37. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  38. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  39. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  40. package/docs/v6/overview/comparison.mdx +101 -0
  41. package/docs/v7/README.md +135 -0
  42. package/docs/v7/api/ai.mdx +205 -0
  43. package/docs/v7/api/assert.mdx +285 -0
  44. package/docs/v7/api/assertions.mdx +403 -0
  45. package/docs/v7/api/click.mdx +287 -0
  46. package/docs/v7/api/client.mdx +322 -0
  47. package/docs/v7/api/dashcam.mdx +497 -0
  48. package/docs/v7/api/doubleClick.mdx +102 -0
  49. package/docs/v7/api/elements.mdx +479 -0
  50. package/docs/v7/api/exec.mdx +346 -0
  51. package/docs/v7/api/find.mdx +316 -0
  52. package/docs/v7/api/focusApplication.mdx +294 -0
  53. package/docs/v7/api/hover.mdx +279 -0
  54. package/docs/v7/api/mouseDown.mdx +161 -0
  55. package/docs/v7/api/mouseUp.mdx +164 -0
  56. package/docs/v7/api/pressKeys.mdx +349 -0
  57. package/docs/v7/api/rightClick.mdx +123 -0
  58. package/docs/v7/api/sandbox.mdx +404 -0
  59. package/docs/v7/api/scroll.mdx +300 -0
  60. package/docs/v7/api/type.mdx +314 -0
  61. package/docs/v7/commands/assert.mdx +45 -0
  62. package/docs/v7/commands/exec.mdx +282 -0
  63. package/docs/v7/commands/focus-application.mdx +44 -0
  64. package/docs/v7/commands/hover-image.mdx +69 -0
  65. package/docs/v7/commands/hover-text.mdx +47 -0
  66. package/docs/v7/commands/if.mdx +53 -0
  67. package/docs/v7/commands/match-image.mdx +67 -0
  68. package/docs/v7/commands/press-keys.mdx +87 -0
  69. package/docs/v7/commands/remember.mdx +49 -0
  70. package/docs/v7/commands/run.mdx +44 -0
  71. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  72. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  73. package/docs/v7/commands/scroll.mdx +69 -0
  74. package/docs/v7/commands/type.mdx +45 -0
  75. package/docs/v7/commands/wait-for-image.mdx +54 -0
  76. package/docs/v7/commands/wait-for-text.mdx +48 -0
  77. package/docs/v7/commands/wait.mdx +45 -0
  78. package/docs/v7/getting-started/configuration.mdx +380 -0
  79. package/docs/v7/getting-started/quickstart.mdx +332 -0
  80. package/docs/v7/guides/best-practices.mdx +486 -0
  81. package/docs/v7/guides/caching-ai.mdx +215 -0
  82. package/docs/v7/guides/caching-selectors.mdx +292 -0
  83. package/docs/v7/guides/caching.mdx +366 -0
  84. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  85. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  86. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  87. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  88. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  89. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  90. package/docs/v7/guides/debugging.mdx +349 -0
  91. package/docs/v7/guides/faq.mdx +393 -0
  92. package/docs/v7/guides/migration.mdx +562 -0
  93. package/docs/v7/guides/performance.mdx +517 -0
  94. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  95. package/docs/v7/guides/troubleshooting.mdx +526 -0
  96. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  97. package/docs/v7/guides/vitest.mdx +535 -0
  98. package/docs/v7/platforms/linux.mdx +308 -0
  99. package/docs/v7/platforms/macos.mdx +433 -0
  100. package/docs/v7/platforms/windows.mdx +430 -0
  101. package/docs/v7/playwright.mdx +342 -0
  102. package/docs/v7/presets/chrome-extension.mdx +223 -0
  103. package/docs/v7/presets/chrome.mdx +287 -0
  104. package/docs/v7/presets/electron.mdx +435 -0
  105. package/docs/v7/presets/vscode.mdx +398 -0
  106. package/docs/v7/presets/webapp.mdx +396 -0
  107. package/docs/v7/progressive-apis/CORE.md +459 -0
  108. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  109. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  110. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  111. package/eslint.config.js +19 -1
  112. package/interfaces/cli/lib/base.js +10 -4
  113. package/interfaces/logger.js +2 -1
  114. package/interfaces/shared-test-state.mjs +69 -0
  115. package/interfaces/vitest-plugin.mjs +830 -0
  116. package/package.json +29 -5
  117. package/schema.json +8 -29
  118. package/scripts/view-test-results.mjs +96 -0
  119. package/sdk-log-formatter.js +714 -0
  120. package/sdk.d.ts +1028 -0
  121. package/sdk.js +2567 -0
  122. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  123. package/setup/aws/cloudformation.yaml +9 -2
  124. package/src/core/Dashcam.js +469 -0
  125. package/src/core/index.d.ts +150 -0
  126. package/src/core/index.js +12 -0
  127. package/src/presets/index.mjs +331 -0
  128. package/src/vitest/extended.mjs +108 -0
  129. package/src/vitest/hooks.d.ts +119 -0
  130. package/src/vitest/hooks.mjs +298 -0
  131. package/src/vitest/index.mjs +64 -0
  132. package/src/vitest/lifecycle.mjs +277 -0
  133. package/src/vitest/utils.mjs +150 -0
  134. package/test/dashcam.test.js +137 -0
  135. package/test/mcp-example-test.yaml +27 -0
  136. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  137. package/testdriver/acceptance-sdk/README.md +128 -0
  138. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  139. package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
  140. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  141. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  142. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
  143. package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
  144. package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
  145. package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
  146. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
  147. package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
  148. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
  149. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  150. package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
  151. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
  152. package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
  153. package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
  154. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  155. package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
  156. package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
  157. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
  158. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
  159. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
  160. package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
  161. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  162. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
  163. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  164. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  165. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  166. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  167. package/testdriver/acceptance-sdk/type.test.mjs +45 -0
  168. package/verify-element-api.js +89 -0
  169. package/verify-types.js +0 -0
  170. package/vitest.config.example.js +19 -0
  171. package/vitest.config.mjs +66 -0
  172. package/vitest.config.mjs.bak +44 -0
  173. package/.github/workflows/acceptance-v6.yml +0 -169
  174. package/.vscode/mcp.json +0 -9
  175. package/docs/overview/comparison.mdx +0 -82
  176. package/testdriver/lifecycle/prerun.yaml +0 -17
  177. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  178. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  179. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  180. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  181. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  182. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  183. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  184. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  185. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  186. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  187. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  188. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  189. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  190. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  191. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  192. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  193. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  194. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  195. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  196. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  197. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  198. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  199. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  200. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  201. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  202. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  203. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  204. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  205. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  206. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  207. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  208. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  209. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  210. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  211. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  212. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  213. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  214. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  215. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  216. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  217. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  218. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  219. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  220. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  221. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  222. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  223. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  224. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  225. /package/docs/{account → v6/account}/projects.mdx +0 -0
  226. /package/docs/{account → v6/account}/team.mdx +0 -0
  227. /package/docs/{action → v6/action}/ami.mdx +0 -0
  228. /package/docs/{action → v6/action}/performance.mdx +0 -0
  229. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  230. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  231. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  232. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  233. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  234. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  235. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  236. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  237. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  238. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  239. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  240. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  241. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  242. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  243. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  244. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  245. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  246. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  247. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  248. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  249. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  250. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  251. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  252. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  253. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  254. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  255. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  256. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  257. /package/docs/{features → v6/features}/generation.mdx +0 -0
  258. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  259. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  260. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  261. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  262. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  263. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  264. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  265. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  266. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  267. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  268. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  269. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  270. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  271. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  272. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  273. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  274. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  275. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  276. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  277. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  278. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  279. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  280. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  281. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  282. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  283. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  284. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  285. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  286. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  287. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  288. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  289. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  290. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  291. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  292. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  293. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  294. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  295. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  296. /package/docs/{security → v6/security}/action.mdx +0 -0
  297. /package/docs/{security → v6/security}/agent.mdx +0 -0
  298. /package/docs/{security → v6/security}/platform.mdx +0 -0
  299. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  300. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -0,0 +1,830 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { setTestRunInfo } from "./shared-test-state.mjs";
6
+
7
+ /**
8
+ * Simple logger for the vitest plugin
9
+ * Supports log levels: debug, info, warn, error
10
+ * Control via TD_LOG_LEVEL environment variable (default: "info")
11
+ * Set TD_LOG_LEVEL=debug for verbose output
12
+ */
13
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
14
+ const currentLogLevel = LOG_LEVELS[process.env.TD_LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
15
+
16
+ const logger = {
17
+ debug: (...args) => {
18
+ if (currentLogLevel <= LOG_LEVELS.debug) {
19
+ console.log("[TestDriver]", ...args);
20
+ }
21
+ },
22
+ info: (...args) => {
23
+ if (currentLogLevel <= LOG_LEVELS.info) {
24
+ console.log("[TestDriver]", ...args);
25
+ }
26
+ },
27
+ warn: (...args) => {
28
+ if (currentLogLevel <= LOG_LEVELS.warn) {
29
+ console.warn("[TestDriver]", ...args);
30
+ }
31
+ },
32
+ error: (...args) => {
33
+ if (currentLogLevel <= LOG_LEVELS.error) {
34
+ console.error("[TestDriver]", ...args);
35
+ }
36
+ },
37
+ };
38
+
39
+ /**
40
+ * Timeout wrapper for promises
41
+ * @param {Promise} promise - Promise to wrap
42
+ * @param {number} timeoutMs - Timeout in milliseconds
43
+ * @param {string} operationName - Name of operation for error message
44
+ * @returns {Promise} Promise that rejects if timeout is reached
45
+ */
46
+ function withTimeout(promise, timeoutMs, operationName) {
47
+ return Promise.race([
48
+ promise,
49
+ new Promise((_, reject) =>
50
+ setTimeout(
51
+ () =>
52
+ reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)),
53
+ timeoutMs,
54
+ ),
55
+ ),
56
+ ]);
57
+ }
58
+
59
+ /**
60
+ * Vitest Plugin for TestDriver
61
+ *
62
+ * Records test runs, test cases, and associates them with dashcam recordings.
63
+ * Uses plugin architecture for better global state management.
64
+ *
65
+ * ## How it works:
66
+ *
67
+ * 1. **Plugin State**: All state is managed in a single `pluginState` object
68
+ * - No class instances or complex scoping
69
+ * - Easy to access from anywhere in the plugin
70
+ * - Dashcam URLs tracked in memory (no temp files!)
71
+ *
72
+ * 2. **Dashcam URL Registration**: Tests register dashcam URLs via simple API
73
+ * - `globalThis.__testdriverPlugin.registerDashcamUrl(testId, url, platform)`
74
+ * - No file system operations
75
+ * - No complex matching logic
76
+ * - Direct association via test ID
77
+ *
78
+ * 3. **Test Recording Flow**:
79
+ * - `onTestRunStart`: Create test run record
80
+ * - `onTestCaseReady`: Track test start time
81
+ * - `onTestCaseResult`: Record individual test result (immediate)
82
+ * - `onTestRunEnd`: Complete test run with final stats
83
+ *
84
+ * 4. **Platform Detection**: Automatically detects platform from SDK client
85
+ * - No manual configuration needed
86
+ * - Stored when dashcam URL is registered
87
+ */
88
+
89
+ // Shared state that can be imported by both the reporter and setup files
90
+ export const pluginState = {
91
+ testRun: null,
92
+ testRunId: null,
93
+ testRunCompleted: false,
94
+ client: null,
95
+ startTime: null,
96
+ testCases: new Map(),
97
+ token: null,
98
+ detectedPlatform: null,
99
+ pendingTestCaseRecords: new Set(),
100
+ ciProvider: null,
101
+ gitInfo: {},
102
+ apiKey: null,
103
+ apiRoot: null,
104
+ // TestDriver options to pass to all instances
105
+ testDriverOptions: {},
106
+ // Dashcam URL tracking (in-memory, no files needed!)
107
+ dashcamUrls: new Map(), // testId -> dashcamUrl
108
+ lastDashcamUrl: null, // Fallback for when test ID isn't available
109
+ // Suite-level test run tracking
110
+ suiteTestRuns: new Map(), // suiteId -> { runId, testRunDbId, token }
111
+ };
112
+
113
+ // Export functions that can be used by the reporter or tests
114
+ export function registerDashcamUrl(testId, url, platform) {
115
+ logger.debug(`Registering dashcam URL for test ${testId}:`, url);
116
+ pluginState.dashcamUrls.set(testId, { url, platform });
117
+ pluginState.lastDashcamUrl = url;
118
+ }
119
+
120
+ export function getDashcamUrl(testId) {
121
+ return pluginState.dashcamUrls.get(testId);
122
+ }
123
+
124
+ export function clearDashcamUrls() {
125
+ pluginState.dashcamUrls.clear();
126
+ pluginState.lastDashcamUrl = null;
127
+ }
128
+
129
+ export function getSuiteTestRun(suiteId) {
130
+ return pluginState.suiteTestRuns.get(suiteId);
131
+ }
132
+
133
+ export function setSuiteTestRun(suiteId, runData) {
134
+ logger.debug(`Setting test run for suite ${suiteId}:`, runData);
135
+ pluginState.suiteTestRuns.set(suiteId, runData);
136
+ }
137
+
138
+ export function clearSuiteTestRun(suiteId) {
139
+ pluginState.suiteTestRuns.delete(suiteId);
140
+ }
141
+
142
+ export function getPluginState() {
143
+ return pluginState;
144
+ }
145
+
146
+ // Export API helper functions for direct use from tests
147
+ export async function authenticateWithApiKey(apiKey, apiRoot) {
148
+ const url = `${apiRoot}/auth/exchange-api-key`;
149
+ const response = await withTimeout(
150
+ fetch(url, {
151
+ method: "POST",
152
+ headers: {
153
+ "Content-Type": "application/json",
154
+ },
155
+ body: JSON.stringify({ apiKey }),
156
+ }),
157
+ 10000,
158
+ "Authentication",
159
+ );
160
+
161
+ if (!response.ok) {
162
+ throw new Error(
163
+ `Authentication failed: ${response.status} ${response.statusText}`,
164
+ );
165
+ }
166
+
167
+ const data = await response.json();
168
+ return data.token;
169
+ }
170
+
171
+ export async function createTestRunDirect(token, apiRoot, testRunData) {
172
+ const url = `${apiRoot}/api/v1/testdriver/test-run-create`;
173
+ const response = await withTimeout(
174
+ fetch(url, {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ Authorization: `Bearer ${token}`,
179
+ },
180
+ body: JSON.stringify(testRunData),
181
+ }),
182
+ 10000,
183
+ "Create Test Run",
184
+ );
185
+
186
+ if (!response.ok) {
187
+ const errorText = await response.text();
188
+ throw new Error(
189
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
190
+ );
191
+ }
192
+
193
+ return await response.json();
194
+ }
195
+
196
+ export async function recordTestCaseDirect(token, apiRoot, testCaseData) {
197
+ const url = `${apiRoot}/api/v1/testdriver/test-case-create`;
198
+ const response = await withTimeout(
199
+ fetch(url, {
200
+ method: "POST",
201
+ headers: {
202
+ "Content-Type": "application/json",
203
+ Authorization: `Bearer ${token}`,
204
+ },
205
+ body: JSON.stringify(testCaseData),
206
+ }),
207
+ 10000,
208
+ "Record Test Case",
209
+ );
210
+
211
+ if (!response.ok) {
212
+ const errorText = await response.text();
213
+ throw new Error(
214
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
215
+ );
216
+ }
217
+
218
+ return await response.json();
219
+ }
220
+
221
+ /**
222
+ * Handle process termination and mark test run as cancelled
223
+ */
224
+ async function handleProcessExit() {
225
+ if (!pluginState.testRun || !pluginState.testRunId) {
226
+ return;
227
+ }
228
+
229
+ logger.info("Process interrupted, marking test run as cancelled...");
230
+
231
+ try {
232
+ const stats = {
233
+ totalTests: pluginState.testCases.size,
234
+ passedTests: 0,
235
+ failedTests: 0,
236
+ skippedTests: 0,
237
+ };
238
+
239
+ const completeData = {
240
+ runId: pluginState.testRunId,
241
+ status: "cancelled",
242
+ totalTests: stats.totalTests,
243
+ passedTests: stats.passedTests,
244
+ failedTests: stats.failedTests,
245
+ skippedTests: stats.skippedTests,
246
+ duration: Date.now() - pluginState.startTime,
247
+ };
248
+
249
+ // Update platform if detected
250
+ const platform = getPlatform();
251
+ if (platform) {
252
+ completeData.platform = platform;
253
+ }
254
+
255
+ await completeTestRun(completeData);
256
+ logger.info("✅ Test run marked as cancelled");
257
+ } catch (error) {
258
+ logger.error("Failed to mark test run as cancelled:", error.message);
259
+ }
260
+ }
261
+
262
+ // Set up process exit handlers
263
+ let exitHandlersRegistered = false;
264
+
265
+ function registerExitHandlers() {
266
+ if (exitHandlersRegistered) return;
267
+ exitHandlersRegistered = true;
268
+
269
+ // Handle Ctrl+C
270
+ process.on("SIGINT", async () => {
271
+ await handleProcessExit();
272
+ process.exit(130); // Standard exit code for SIGINT
273
+ });
274
+
275
+ // Handle kill command
276
+ process.on("SIGTERM", async () => {
277
+ await handleProcessExit();
278
+ process.exit(143); // Standard exit code for SIGTERM
279
+ });
280
+
281
+ // Handle unexpected exits
282
+ process.on("beforeExit", async () => {
283
+ // Only handle if test run is still running (hasn't been completed normally)
284
+ if (pluginState.testRun && !pluginState.testRunCompleted) {
285
+ await handleProcessExit();
286
+ }
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Create the TestDriver Vitest plugin
292
+ * This sets up global state and provides the registration API
293
+ */
294
+ export default function testDriverPlugin(options = {}) {
295
+ // Initialize plugin state with options
296
+ pluginState.apiKey = options.apiKey;
297
+ pluginState.apiRoot =
298
+ options.apiRoot || process.env.TD_API_ROOT || "http://localhost:1337";
299
+ pluginState.ciProvider = detectCI();
300
+ pluginState.gitInfo = getGitInfo();
301
+
302
+ // Store TestDriver-specific options (excluding plugin-specific ones)
303
+ const { apiKey, apiRoot, ...testDriverOptions } = options;
304
+ pluginState.testDriverOptions = testDriverOptions;
305
+
306
+ // Register process exit handlers to handle cancellation
307
+ registerExitHandlers();
308
+
309
+ // Note: globalThis setup happens in vitestSetup.mjs for worker processes
310
+ logger.debug("Initialized with API root:", pluginState.apiRoot);
311
+ if (Object.keys(testDriverOptions).length > 0) {
312
+ logger.debug("Global TestDriver options:", testDriverOptions);
313
+ }
314
+
315
+ return new TestDriverReporter(options);
316
+ }
317
+
318
+ /**
319
+ * TestDriver Reporter Class
320
+ * Handles Vitest test lifecycle events
321
+ */
322
+ class TestDriverReporter {
323
+ constructor(options = {}) {
324
+ this.options = options;
325
+ logger.debug("Reporter created");
326
+ }
327
+
328
+ async onInit(ctx) {
329
+ this.ctx = ctx;
330
+ logger.debug("onInit called");
331
+
332
+ // Initialize test run
333
+ await this.initializeTestRun();
334
+ }
335
+
336
+ async initializeTestRun() {
337
+ logger.debug("Initializing test run...");
338
+
339
+ // Check if we should enable the reporter
340
+ if (!pluginState.apiKey) {
341
+ logger.debug("No API key provided, skipping test recording");
342
+ return;
343
+ }
344
+
345
+ try {
346
+ // Exchange API key for JWT token
347
+ await authenticate();
348
+
349
+ // Generate unique run ID
350
+ pluginState.testRunId = generateRunId();
351
+ pluginState.startTime = Date.now();
352
+ pluginState.testRunCompleted = false; // Reset completion flag
353
+
354
+ // Create test run via direct API call
355
+ const testRunData = {
356
+ runId: pluginState.testRunId,
357
+ suiteName: getSuiteName(),
358
+ ...pluginState.gitInfo,
359
+ };
360
+
361
+ // Session ID will be added from the first test result file that includes it
362
+
363
+ // Only add ciProvider if it's not null
364
+ if (pluginState.ciProvider) {
365
+ testRunData.ciProvider = pluginState.ciProvider;
366
+ }
367
+
368
+ // Platform will be set from the first test result file
369
+ // Default to linux if no tests write platform info
370
+ testRunData.platform = "linux";
371
+
372
+ pluginState.testRun = await createTestRun(testRunData);
373
+
374
+ // Store in environment variables for worker processes to access
375
+ process.env.TD_TEST_RUN_ID = pluginState.testRunId;
376
+ process.env.TD_TEST_RUN_DB_ID = pluginState.testRun.data?.id || "";
377
+ process.env.TD_TEST_RUN_TOKEN = pluginState.token;
378
+
379
+ // Also store in shared state module (won't work across processes but good for main)
380
+ setTestRunInfo({
381
+ testRun: pluginState.testRun,
382
+ testRunId: pluginState.testRunId,
383
+ token: pluginState.token,
384
+ apiKey: pluginState.apiKey,
385
+ apiRoot: pluginState.apiRoot,
386
+ startTime: pluginState.startTime,
387
+ });
388
+
389
+ logger.info(`Test run created: ${pluginState.testRunId}`);
390
+ } catch (error) {
391
+ logger.error("Failed to initialize:", error.message);
392
+ pluginState.apiKey = null;
393
+ pluginState.token = null;
394
+ }
395
+ }
396
+
397
+ async onTestRunEnd(testModules, unhandledErrors, reason) {
398
+ logger.debug("Test run ending with reason:", reason);
399
+
400
+ if (!pluginState.apiKey) {
401
+ logger.debug("Skipping completion - no API key");
402
+ return;
403
+ }
404
+
405
+ if (!pluginState.testRun) {
406
+ logger.debug("Skipping completion - no test run created");
407
+ return;
408
+ }
409
+
410
+ try {
411
+ // Calculate statistics from testModules
412
+ const stats = calculateStatsFromModules(testModules);
413
+
414
+ logger.debug("Stats:", stats);
415
+
416
+ // Determine overall status based on reason and stats
417
+ let status = "passed";
418
+ if (reason === "failed" || stats.failedTests > 0) {
419
+ status = "failed";
420
+ } else if (reason === "interrupted") {
421
+ status = "cancelled";
422
+ } else if (stats.totalTests === 0) {
423
+ status = "cancelled";
424
+ }
425
+
426
+ // Complete test run via API
427
+ logger.debug(`Completing test run ${pluginState.testRunId} with status: ${status}`);
428
+
429
+ const completeData = {
430
+ runId: pluginState.testRunId,
431
+ status,
432
+ totalTests: stats.totalTests,
433
+ passedTests: stats.passedTests,
434
+ failedTests: stats.failedTests,
435
+ skippedTests: stats.skippedTests,
436
+ duration: Date.now() - pluginState.startTime,
437
+ };
438
+
439
+ // Update platform if detected from test results
440
+ const platform = getPlatform();
441
+ if (platform) {
442
+ completeData.platform = platform;
443
+ logger.debug(`Updating test run with platform: ${platform}`);
444
+ }
445
+
446
+ // Wait for any pending operations (shouldn't be any, but just in case)
447
+ if (pluginState.pendingTestCaseRecords.size > 0) {
448
+ logger.debug(`Waiting for ${pluginState.pendingTestCaseRecords.size} pending operations...`);
449
+ await Promise.all(Array.from(pluginState.pendingTestCaseRecords));
450
+ }
451
+
452
+ // Test cases are reported directly from teardownTest
453
+ logger.debug("All test cases reported from teardown");
454
+
455
+ const completeResponse = await completeTestRun(completeData);
456
+ logger.debug("Test run completion API response:", completeResponse);
457
+
458
+ // Mark test run as completed to prevent duplicate completion
459
+ pluginState.testRunCompleted = true;
460
+
461
+ logger.info(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
462
+ } catch (error) {
463
+ logger.error("Failed to complete test run:", error.message);
464
+ logger.debug("Error stack:", error.stack);
465
+ }
466
+ }
467
+
468
+ onTestCaseReady(test) {
469
+ if (!pluginState.apiKey || !pluginState.testRun) return;
470
+
471
+ pluginState.testCases.set(test.id, {
472
+ test,
473
+ startTime: Date.now(),
474
+ });
475
+
476
+ // Try to detect platform from test context
477
+ detectPlatformFromTest(test);
478
+ }
479
+
480
+ async onTestCaseResult(test) {
481
+ if (!pluginState.apiKey || !pluginState.testRun) return;
482
+
483
+ const result = test.result();
484
+ const status =
485
+ result.state === "passed"
486
+ ? "passed"
487
+ : result.state === "skipped"
488
+ ? "skipped"
489
+ : "failed";
490
+
491
+ logger.info(`Test case completed: ${test.name} (${status})`);
492
+
493
+ // Calculate duration from tracked start time
494
+ const testCase = pluginState.testCases.get(test.id);
495
+ const duration = testCase ? Date.now() - testCase.startTime : 0;
496
+
497
+ logger.debug(`Calculated duration: ${duration}ms (startTime: ${testCase?.startTime}, now: ${Date.now()})`);
498
+
499
+ // Read test metadata from file (cross-process communication)
500
+ let dashcamUrl = null;
501
+ let sessionId = null;
502
+ let testFile = "unknown";
503
+ let testOrder = 0;
504
+
505
+ const testResultFile = path.join(
506
+ os.tmpdir(),
507
+ "testdriver-results",
508
+ `${test.id}.json`,
509
+ );
510
+
511
+ logger.debug(`Looking for test result file with test.id: ${test.id}`);
512
+ logger.debug(`Test result file path: ${testResultFile}`);
513
+
514
+ try {
515
+ if (fs.existsSync(testResultFile)) {
516
+ const testResult = JSON.parse(fs.readFileSync(testResultFile, "utf-8"));
517
+ dashcamUrl = testResult.dashcamUrl || null;
518
+ const platform = testResult.platform || null;
519
+ sessionId = testResult.sessionId || null;
520
+ testFile =
521
+ testResult.testFile ||
522
+ test.file?.filepath ||
523
+ test.file?.name ||
524
+ "unknown";
525
+ testOrder =
526
+ testResult.testOrder !== undefined ? testResult.testOrder : 0;
527
+ // Don't override duration from file - use Vitest's result.duration
528
+ // duration is already set above from result.duration
529
+
530
+ logger.debug(`Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, sessionId: ${sessionId}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`);
531
+
532
+ // Update test run platform from first test that reports it
533
+ if (platform && !pluginState.detectedPlatform) {
534
+ pluginState.detectedPlatform = platform;
535
+ logger.debug(`Detected platform from test: ${platform}`);
536
+ }
537
+
538
+ // Clean up the file after reading
539
+ try {
540
+ fs.unlinkSync(testResultFile);
541
+ } catch {
542
+ // Ignore cleanup errors
543
+ }
544
+ } else {
545
+ logger.debug(`No result file found for test: ${test.id}`);
546
+ // Fallback to test object properties - try multiple sources
547
+ // In Vitest, the file path is on test.module.task.filepath
548
+ testFile =
549
+ test.module?.task?.filepath ||
550
+ test.module?.file?.filepath ||
551
+ test.module?.file?.name ||
552
+ test.file?.filepath ||
553
+ test.file?.name ||
554
+ test.suite?.file?.filepath ||
555
+ test.suite?.file?.name ||
556
+ test.location?.file ||
557
+ "unknown";
558
+ logger.debug(`Resolved testFile: ${testFile}`);
559
+ }
560
+ } catch (error) {
561
+ logger.error("Failed to read test result file:", error.message);
562
+ // Fallback to test object properties - try multiple sources
563
+ // In Vitest, the file path is on test.module.task.filepath
564
+ testFile =
565
+ test.module?.task?.filepath ||
566
+ test.module?.file?.filepath ||
567
+ test.module?.file?.name ||
568
+ test.file?.filepath ||
569
+ test.file?.name ||
570
+ test.suite?.file?.filepath ||
571
+ test.suite?.file?.name ||
572
+ test.location?.file ||
573
+ "unknown";
574
+ logger.debug(`Resolved testFile from fallback: ${testFile}`);
575
+ }
576
+
577
+ // Get test run info from environment variables
578
+ const testRunId = process.env.TD_TEST_RUN_ID;
579
+ const token = process.env.TD_TEST_RUN_TOKEN;
580
+
581
+ if (!testRunId || !token) {
582
+ logger.warn(`Test run not initialized, skipping test case recording for: ${test.name}`);
583
+ return;
584
+ }
585
+
586
+ try {
587
+ let errorMessage = null;
588
+ let errorStack = null;
589
+
590
+ if (
591
+ result.state === "failed" &&
592
+ result.errors &&
593
+ result.errors.length > 0
594
+ ) {
595
+ const error = result.errors[0];
596
+ errorMessage = error.message;
597
+ errorStack = error.stack;
598
+ }
599
+
600
+ const suiteName = test.suite?.name;
601
+ const startTime = Date.now() - duration; // Calculate start time from duration
602
+
603
+ // Record test case with all metadata
604
+ const testCaseData = {
605
+ runId: testRunId,
606
+ testName: test.name,
607
+ testFile: testFile,
608
+ testOrder: testOrder,
609
+ status,
610
+ startTime: startTime,
611
+ endTime: Date.now(),
612
+ duration: duration,
613
+ retries: result.retryCount || 0,
614
+ };
615
+
616
+ // Add sessionId if available
617
+ if (sessionId) {
618
+ testCaseData.sessionId = sessionId;
619
+ }
620
+
621
+ // Only include replayUrl if we have a valid dashcam URL
622
+ if (dashcamUrl) {
623
+ testCaseData.replayUrl = dashcamUrl;
624
+ }
625
+
626
+ if (suiteName) testCaseData.suiteName = suiteName;
627
+ if (errorMessage) testCaseData.errorMessage = errorMessage;
628
+ if (errorStack) testCaseData.errorStack = errorStack;
629
+
630
+ logger.debug(`Recording test case: ${test.name} (${status}) with testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms, replay: ${dashcamUrl ? "yes" : "no"}`);
631
+
632
+ const testCaseResponse = await recordTestCaseDirect(
633
+ token,
634
+ pluginState.apiRoot,
635
+ testCaseData,
636
+ );
637
+
638
+ const testCaseDbId = testCaseResponse.data?.id;
639
+ const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
640
+
641
+ logger.debug(`Reported test case to API${dashcamUrl ? " with dashcam URL" : ""}`);
642
+ logger.info(`🔗 View test: ${pluginState.apiRoot.replace("testdriver-api.onrender.com", "app.testdriver.ai")}/runs/${testRunDbId}/${testCaseDbId}`);
643
+ } catch (error) {
644
+ logger.error("Failed to report test case:", error.message);
645
+ }
646
+ }
647
+ }
648
+
649
+ // ============================================================================
650
+ // Helper Functions
651
+ // ============================================================================
652
+
653
+ function generateRunId() {
654
+ return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
655
+ }
656
+
657
+ function getSuiteName() {
658
+ return process.env.npm_package_name || path.basename(process.cwd());
659
+ }
660
+
661
+ function getPlatform() {
662
+ // First try to get platform from SDK client detected during test execution
663
+ if (pluginState.detectedPlatform) {
664
+ logger.debug(`Using platform from SDK client: ${pluginState.detectedPlatform}`);
665
+ return pluginState.detectedPlatform;
666
+ }
667
+
668
+ logger.debug("Platform not yet detected from client");
669
+ return null;
670
+ }
671
+
672
+ function detectPlatformFromTest(test) {
673
+ // Check if testdriver client is accessible via test context
674
+ const client = test.context?.testdriver || test.meta?.testdriver;
675
+
676
+ if (client && client.os) {
677
+ // Normalize platform value
678
+ let platform = client.os.toLowerCase();
679
+ if (platform === "darwin" || platform === "mac") platform = "mac";
680
+ else if (platform === "win32" || platform === "windows")
681
+ platform = "windows";
682
+ else if (platform === "linux") platform = "linux";
683
+
684
+ pluginState.detectedPlatform = platform;
685
+ logger.debug(`Detected platform from test context: ${platform}`);
686
+ }
687
+ }
688
+
689
+ function calculateStatsFromModules(testModules) {
690
+ let totalTests = 0;
691
+ let passedTests = 0;
692
+ let failedTests = 0;
693
+ let skippedTests = 0;
694
+
695
+ for (const testModule of testModules) {
696
+ for (const testCase of testModule.children.allTests()) {
697
+ totalTests++;
698
+ const result = testCase.result();
699
+ if (result.state === "passed") passedTests++;
700
+ else if (result.state === "failed") failedTests++;
701
+ else if (result.state === "skipped") skippedTests++;
702
+ }
703
+ }
704
+
705
+ return { totalTests, passedTests, failedTests, skippedTests };
706
+ }
707
+
708
+ function detectCI() {
709
+ if (process.env.GITHUB_ACTIONS) return "github";
710
+ if (process.env.GITLAB_CI) return "gitlab";
711
+ if (process.env.CIRCLECI) return "circle";
712
+ if (process.env.TRAVIS) return "travis";
713
+ if (process.env.JENKINS_URL) return "jenkins";
714
+ if (process.env.BUILDKITE) return "buildkite";
715
+ return null;
716
+ }
717
+
718
+ function getGitInfo() {
719
+ const info = {};
720
+
721
+ if (process.env.GITHUB_ACTIONS) {
722
+ if (process.env.GITHUB_REPOSITORY)
723
+ info.repo = process.env.GITHUB_REPOSITORY;
724
+ if (process.env.GITHUB_REF_NAME) info.branch = process.env.GITHUB_REF_NAME;
725
+ if (process.env.GITHUB_SHA) info.commit = process.env.GITHUB_SHA;
726
+ if (process.env.GITHUB_ACTOR) info.author = process.env.GITHUB_ACTOR;
727
+ } else if (process.env.GITLAB_CI) {
728
+ if (process.env.CI_PROJECT_PATH) info.repo = process.env.CI_PROJECT_PATH;
729
+ if (process.env.CI_COMMIT_BRANCH)
730
+ info.branch = process.env.CI_COMMIT_BRANCH;
731
+ if (process.env.CI_COMMIT_SHA) info.commit = process.env.CI_COMMIT_SHA;
732
+ if (process.env.GITLAB_USER_LOGIN)
733
+ info.author = process.env.GITLAB_USER_LOGIN;
734
+ } else if (process.env.CIRCLECI) {
735
+ if (
736
+ process.env.CIRCLE_PROJECT_USERNAME &&
737
+ process.env.CIRCLE_PROJECT_REPONAME
738
+ ) {
739
+ info.repo = `${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`;
740
+ }
741
+ if (process.env.CIRCLE_BRANCH) info.branch = process.env.CIRCLE_BRANCH;
742
+ if (process.env.CIRCLE_SHA1) info.commit = process.env.CIRCLE_SHA1;
743
+ if (process.env.CIRCLE_USERNAME) info.author = process.env.CIRCLE_USERNAME;
744
+ }
745
+
746
+ return info;
747
+ }
748
+
749
+ // ============================================================================
750
+ // API Methods
751
+ // ============================================================================
752
+
753
+ async function authenticate() {
754
+ const url = `${pluginState.apiRoot}/auth/exchange-api-key`;
755
+ const response = await withTimeout(
756
+ fetch(url, {
757
+ method: "POST",
758
+ headers: {
759
+ "Content-Type": "application/json",
760
+ },
761
+ body: JSON.stringify({
762
+ apiKey: pluginState.apiKey,
763
+ }),
764
+ }),
765
+ 10000,
766
+ "Internal Authentication",
767
+ );
768
+
769
+ if (!response.ok) {
770
+ throw new Error(
771
+ `Authentication failed: ${response.status} ${response.statusText}`,
772
+ );
773
+ }
774
+
775
+ const data = await response.json();
776
+ pluginState.token = data.token;
777
+ }
778
+
779
+ async function createTestRun(data) {
780
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-create`;
781
+ const response = await withTimeout(
782
+ fetch(url, {
783
+ method: "POST",
784
+ headers: {
785
+ "Content-Type": "application/json",
786
+ Authorization: `Bearer ${pluginState.token}`,
787
+ },
788
+ body: JSON.stringify(data),
789
+ }),
790
+ 10000,
791
+ "Internal Create Test Run",
792
+ );
793
+
794
+ if (!response.ok) {
795
+ const errorText = await response.text();
796
+ throw new Error(
797
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
798
+ );
799
+ }
800
+
801
+ return await response.json();
802
+ }
803
+
804
+ async function completeTestRun(data) {
805
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-complete`;
806
+ const response = await withTimeout(
807
+ fetch(url, {
808
+ method: "POST",
809
+ headers: {
810
+ "Content-Type": "application/json",
811
+ Authorization: `Bearer ${pluginState.token}`,
812
+ },
813
+ body: JSON.stringify(data),
814
+ }),
815
+ 10000,
816
+ "Internal Complete Test Run",
817
+ );
818
+
819
+ if (!response.ok) {
820
+ const errorText = await response.text();
821
+ throw new Error(
822
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
823
+ );
824
+ }
825
+
826
+ return await response.json();
827
+ }
828
+
829
+ // Global state setup moved to setup file (vitestSetup.mjs)
830
+ // The setup file imports the exported functions and makes them available globally in worker processes