elasticdash-sdk 0.2.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 (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. package/src/workflow-runner.ts +58 -0
@@ -0,0 +1,127 @@
1
+ import { execSync } from 'node:child_process'
2
+
3
+ // ─── CI Environment Auto-Detection ──────────────────────────
4
+
5
+ export interface GitInfo {
6
+ branch?: string
7
+ commit?: string
8
+ commitMessage?: string
9
+ baseBranch?: string
10
+ prNumber?: number
11
+ prUrl?: string
12
+ repo?: string
13
+ ciProvider?: string
14
+ ciRunUrl?: string
15
+ }
16
+
17
+ /**
18
+ * Auto-detect git info from CI environment variables.
19
+ * Supports GitHub Actions, GitLab CI, CircleCI, Buildkite, and local git.
20
+ */
21
+ export function detectGitInfo(): GitInfo {
22
+ const env = process.env
23
+
24
+ // GitHub Actions
25
+ if (env.GITHUB_ACTIONS === 'true') {
26
+ const prNumber = env.GITHUB_EVENT_NAME === 'pull_request'
27
+ ? parseInt(env.GITHUB_REF?.match(/refs\/pull\/(\d+)/)?.[1] ?? '', 10) || undefined
28
+ : undefined
29
+
30
+ const repo = env.GITHUB_REPOSITORY
31
+ const prUrl = prNumber && repo
32
+ ? `https://github.com/${repo}/pull/${prNumber}`
33
+ : undefined
34
+
35
+ const serverUrl = env.GITHUB_SERVER_URL || 'https://github.com'
36
+ const ciRunUrl = repo && env.GITHUB_RUN_ID
37
+ ? `${serverUrl}/${repo}/actions/runs/${env.GITHUB_RUN_ID}`
38
+ : undefined
39
+
40
+ return {
41
+ branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME,
42
+ commit: env.GITHUB_SHA,
43
+ commitMessage: env.GITHUB_COMMIT_MESSAGE,
44
+ baseBranch: env.GITHUB_BASE_REF || undefined,
45
+ prNumber,
46
+ prUrl,
47
+ repo,
48
+ ciProvider: 'github-actions',
49
+ ciRunUrl,
50
+ }
51
+ }
52
+
53
+ // GitLab CI
54
+ if (env.GITLAB_CI === 'true') {
55
+ const prNumber = env.CI_MERGE_REQUEST_IID
56
+ ? parseInt(env.CI_MERGE_REQUEST_IID, 10) || undefined
57
+ : undefined
58
+
59
+ return {
60
+ branch: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME || env.CI_COMMIT_BRANCH,
61
+ commit: env.CI_COMMIT_SHA,
62
+ commitMessage: env.CI_COMMIT_MESSAGE,
63
+ baseBranch: env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || undefined,
64
+ prNumber,
65
+ prUrl: env.CI_MERGE_REQUEST_PROJECT_URL && prNumber
66
+ ? `${env.CI_MERGE_REQUEST_PROJECT_URL}/-/merge_requests/${prNumber}`
67
+ : undefined,
68
+ repo: env.CI_PROJECT_PATH,
69
+ ciProvider: 'gitlab-ci',
70
+ ciRunUrl: env.CI_JOB_URL,
71
+ }
72
+ }
73
+
74
+ // CircleCI
75
+ if (env.CIRCLECI === 'true') {
76
+ const prNumber = env.CIRCLE_PULL_REQUEST
77
+ ? parseInt(env.CIRCLE_PULL_REQUEST.split('/').pop() ?? '', 10) || undefined
78
+ : undefined
79
+
80
+ return {
81
+ branch: env.CIRCLE_BRANCH,
82
+ commit: env.CIRCLE_SHA1,
83
+ prNumber,
84
+ repo: env.CIRCLE_PROJECT_REPONAME,
85
+ ciProvider: 'circleci',
86
+ ciRunUrl: env.CIRCLE_BUILD_URL,
87
+ }
88
+ }
89
+
90
+ // Buildkite
91
+ if (env.BUILDKITE === 'true') {
92
+ const prNumber = env.BUILDKITE_PULL_REQUEST && env.BUILDKITE_PULL_REQUEST !== 'false'
93
+ ? parseInt(env.BUILDKITE_PULL_REQUEST, 10) || undefined
94
+ : undefined
95
+
96
+ return {
97
+ branch: env.BUILDKITE_BRANCH,
98
+ commit: env.BUILDKITE_COMMIT,
99
+ baseBranch: env.BUILDKITE_PULL_REQUEST_BASE_BRANCH || undefined,
100
+ prNumber,
101
+ repo: env.BUILDKITE_REPO,
102
+ ciProvider: 'buildkite',
103
+ ciRunUrl: env.BUILDKITE_BUILD_URL,
104
+ }
105
+ }
106
+
107
+ // Local fallback: shell out to git
108
+ return detectLocalGitInfo()
109
+ }
110
+
111
+ function detectLocalGitInfo(): GitInfo {
112
+ try {
113
+ const commit = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
114
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
115
+ return {
116
+ branch: branch || undefined,
117
+ commit: commit || undefined,
118
+ ciProvider: 'local',
119
+ }
120
+ } catch {
121
+ return {
122
+ branch: 'unknown',
123
+ commit: 'unknown',
124
+ ciProvider: 'local',
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,5 @@
1
+ export { runCI } from './runner.js'
2
+ export { fetchTestGroups, submitTestRun, createBatch } from './api-client.js'
3
+ export { detectGitInfo } from './git-info.js'
4
+ export type { CIRunConfig, CIRunSummary, CITestResult, CISingleRunResult, CIExpectationResult } from './types.js'
5
+ export type { GitInfo } from './git-info.js'
@@ -0,0 +1,25 @@
1
+ import type { EdReplayContext } from './replay.js'
2
+
3
+ export interface TestMeasurement {
4
+ duration_ms: number
5
+ tokens_input?: number
6
+ tokens_output?: number
7
+ tokens_total?: number
8
+ }
9
+
10
+ export function collectMeasurement(ctx: EdReplayContext): TestMeasurement | null {
11
+ const m = ctx.targetMeasurement
12
+ if (!m) return null
13
+
14
+ const result: TestMeasurement = {
15
+ duration_ms: m.duration_ms,
16
+ }
17
+
18
+ if (m.tokens) {
19
+ result.tokens_input = m.tokens.input
20
+ result.tokens_output = m.tokens.output
21
+ result.tokens_total = m.tokens.total
22
+ }
23
+
24
+ return result
25
+ }
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import type { DiskTrace, DiskTraceStep } from './trace-schema.js'
4
+
5
+ // ─── Types ──────────────────────────────────────────────────
6
+
7
+ export interface ReplayMeasurement {
8
+ duration_ms: number
9
+ tokens?: { input: number; output: number; total: number } | null
10
+ }
11
+
12
+ export interface EdReplayContext {
13
+ steps: DiskTraceStep[]
14
+ consumed: boolean[]
15
+ targetStepId: string
16
+ targetMeasurement: ReplayMeasurement | null
17
+ }
18
+
19
+ export class ReplayMissError extends Error {
20
+ constructor(public callType: string, public callName: string) {
21
+ super(`replay miss: ${callType}::${callName}`)
22
+ this.name = 'ReplayMissError'
23
+ }
24
+ }
25
+
26
+ // ─── ALS-backed context ─────────────────────────────────────
27
+
28
+ const g = globalThis as Record<string, unknown>
29
+ const ED_REPLAY_ALS_KEY = '__elasticdash_ed_replay_als__'
30
+ const edReplayAls: AsyncLocalStorage<EdReplayContext | undefined> =
31
+ (g[ED_REPLAY_ALS_KEY] as AsyncLocalStorage<EdReplayContext | undefined>) ??
32
+ new AsyncLocalStorage<EdReplayContext | undefined>()
33
+ if (!g[ED_REPLAY_ALS_KEY]) g[ED_REPLAY_ALS_KEY] = edReplayAls
34
+
35
+ export function getEdReplayContext(): EdReplayContext | undefined {
36
+ return edReplayAls.getStore()
37
+ }
38
+
39
+ export function installReplay(ctx: EdReplayContext): void {
40
+ edReplayAls.enterWith(ctx)
41
+ }
42
+
43
+ export function uninstallReplay(): void {
44
+ edReplayAls.enterWith(undefined)
45
+ }
46
+
47
+ // ─── Context creation ───────────────────────────────────────
48
+
49
+ export function createReplayContext(trace: DiskTrace, targetStepId: string): EdReplayContext {
50
+ return {
51
+ steps: trace.steps,
52
+ consumed: new Array(trace.steps.length).fill(false),
53
+ targetStepId,
54
+ targetMeasurement: null,
55
+ }
56
+ }
57
+
58
+ // ─── Input canonicalization & match key ─────────────────────
59
+
60
+ export function canonicalizeInput(input: unknown): string {
61
+ return JSON.stringify(sortKeys(input))
62
+ }
63
+
64
+ function sortKeys(value: unknown): unknown {
65
+ if (value === null || value === undefined) return value
66
+ if (Array.isArray(value)) return value.map(sortKeys)
67
+ if (typeof value === 'number') {
68
+ // Round floats to 6 decimal places
69
+ if (!Number.isInteger(value)) {
70
+ return Math.round(value * 1e6) / 1e6
71
+ }
72
+ return value
73
+ }
74
+ if (typeof value === 'string') {
75
+ // Normalize whitespace
76
+ return value.replace(/\s+/g, ' ').trim()
77
+ }
78
+ if (typeof value === 'object') {
79
+ const sorted: Record<string, unknown> = {}
80
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
81
+ sorted[key] = sortKeys((value as Record<string, unknown>)[key])
82
+ }
83
+ return sorted
84
+ }
85
+ return value
86
+ }
87
+
88
+ export function computeMatchKey(type: 'tool_call' | 'ai_call', name: string, input: unknown): string {
89
+ const canonical = canonicalizeInput(input)
90
+ const hash = createHash('sha256').update(canonical).digest('hex').slice(0, 16)
91
+ return `${type}::${name}::${hash}`
92
+ }
93
+
94
+ // ─── Replay call ────────────────────────────────────────────
95
+
96
+ export function replayCall(
97
+ ctx: EdReplayContext,
98
+ type: 'tool_call' | 'ai_call',
99
+ name: string,
100
+ input: unknown,
101
+ ): { output: unknown; measurement: ReplayMeasurement | null } {
102
+ const runtimeKey = computeMatchKey(type, name, input)
103
+
104
+ // Find next unconsumed step with matching key
105
+ for (let i = 0; i < ctx.steps.length; i++) {
106
+ if (ctx.consumed[i]) continue
107
+ const step = ctx.steps[i]
108
+ const stepKey = computeMatchKey(step.type, step.name, step.input)
109
+ if (stepKey === runtimeKey) {
110
+ ctx.consumed[i] = true
111
+
112
+ const measurement: ReplayMeasurement = {
113
+ duration_ms: step.duration_ms,
114
+ tokens: step.tokens ?? null,
115
+ }
116
+
117
+ // Capture measurement if this is the target step
118
+ if (step.step_id === ctx.targetStepId) {
119
+ ctx.targetMeasurement = measurement
120
+ }
121
+
122
+ return { output: step.output, measurement }
123
+ }
124
+ }
125
+
126
+ throw new ReplayMissError(type, name)
127
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk'
2
+ import type { Reporter } from './index.js'
3
+ import type { EdTestResult, EdTestRunResult } from '../ed-runner.js'
4
+
5
+ export class DefaultReporter implements Reporter {
6
+ onTestStart(name: string): void {
7
+ process.stdout.write(chalk.gray(` ${name} ... `))
8
+ }
9
+
10
+ onTestResult(result: EdTestResult): void {
11
+ if (result.status === 'pass') {
12
+ console.log(chalk.green('PASS'))
13
+ if (result.benchmarkResult) {
14
+ for (const m of result.benchmarkResult.metrics) {
15
+ console.log(chalk.gray(` ${m.name}: ${m.value} (threshold: ${m.threshold}) `) + chalk.green('✓'))
16
+ }
17
+ }
18
+ } else {
19
+ console.log(chalk.red('FAIL'))
20
+ if (result.benchmarkResult) {
21
+ for (const m of result.benchmarkResult.metrics) {
22
+ const icon = m.passed ? chalk.green('✓') : chalk.red('✗')
23
+ console.log(chalk.gray(` ${m.name}: ${m.value} (threshold: ${m.threshold}) `) + icon)
24
+ }
25
+ }
26
+ if (result.failureReason) {
27
+ console.log(chalk.red(` → ${result.failureReason}`))
28
+ }
29
+ }
30
+ console.log()
31
+ }
32
+
33
+ onRunComplete(runResult: EdTestRunResult, uploadUrl?: string): void {
34
+ const passed = runResult.results.filter(r => r.status === 'pass').length
35
+ const failed = runResult.results.filter(r => r.status === 'fail').length
36
+ const total = runResult.results.length
37
+
38
+ console.log(chalk.white.bold('─'.repeat(45)))
39
+
40
+ const parts: string[] = []
41
+ if (passed > 0) parts.push(chalk.green(`${passed} passed`))
42
+ if (failed > 0) parts.push(chalk.red(`${failed} failed`))
43
+ console.log(`${parts.join(', ')}, ${total} total`)
44
+
45
+ if (uploadUrl) {
46
+ console.log(chalk.gray(`Uploaded to ${uploadUrl}`))
47
+ }
48
+ console.log()
49
+ }
50
+ }
@@ -0,0 +1,21 @@
1
+ import type { EdTestResult, EdTestRunResult } from '../ed-runner.js'
2
+ import { DefaultReporter } from './default.js'
3
+ import { JsonReporter } from './json.js'
4
+ import { JunitReporter } from './junit.js'
5
+
6
+ export interface Reporter {
7
+ onTestStart(name: string): void
8
+ onTestResult(result: EdTestResult): void
9
+ onRunComplete(runResult: EdTestRunResult, uploadUrl?: string): void
10
+ }
11
+
12
+ export function createReporter(name: 'default' | 'json' | 'junit'): Reporter {
13
+ switch (name) {
14
+ case 'json':
15
+ return new JsonReporter()
16
+ case 'junit':
17
+ return new JunitReporter()
18
+ default:
19
+ return new DefaultReporter()
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import type { Reporter } from './index.js'
2
+ import type { EdTestResult, EdTestRunResult } from '../ed-runner.js'
3
+ import { buildUploadPayload } from '../upload-client.js'
4
+
5
+ export class JsonReporter implements Reporter {
6
+ onTestStart(_name: string): void {
7
+ // silent
8
+ }
9
+
10
+ onTestResult(_result: EdTestResult): void {
11
+ // silent — output everything at the end
12
+ }
13
+
14
+ onRunComplete(runResult: EdTestRunResult, _uploadUrl?: string): void {
15
+ const payload = buildUploadPayload(runResult)
16
+ console.log(JSON.stringify(payload, null, 2))
17
+ }
18
+ }
@@ -0,0 +1,61 @@
1
+ import type { Reporter } from './index.js'
2
+ import type { EdTestResult, EdTestRunResult } from '../ed-runner.js'
3
+
4
+ function escapeXml(str: string): string {
5
+ return str
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
+ .replace(/'/g, '&apos;')
11
+ }
12
+
13
+ export class JunitReporter implements Reporter {
14
+ onTestStart(_name: string): void {
15
+ // silent
16
+ }
17
+
18
+ onTestResult(_result: EdTestResult): void {
19
+ // silent — output everything at the end
20
+ }
21
+
22
+ onRunComplete(runResult: EdTestRunResult, _uploadUrl?: string): void {
23
+ const results = runResult.results
24
+ const passed = results.filter(r => r.status === 'pass').length
25
+ const failed = results.filter(r => r.status === 'fail').length
26
+ const totalTime = results.reduce((sum, r) => sum + r.durationMs, 0) / 1000
27
+
28
+ const lines: string[] = [
29
+ '<?xml version="1.0" encoding="UTF-8"?>',
30
+ `<testsuites tests="${results.length}" failures="${failed}" errors="0" time="${totalTime.toFixed(3)}">`,
31
+ ` <testsuite name="ed_tests" tests="${results.length}" failures="${failed}" errors="0" time="${totalTime.toFixed(3)}">`,
32
+ ]
33
+
34
+ for (const r of results) {
35
+ const time = (r.durationMs / 1000).toFixed(3)
36
+ // Use duration_ms measurement as the duration attribute if available
37
+ const metricTime = r.measurement ? (r.measurement.duration_ms / 1000).toFixed(3) : time
38
+
39
+ lines.push(` <testcase name="${escapeXml(r.testName)}" time="${metricTime}">`)
40
+
41
+ if (r.status === 'fail') {
42
+ const isReplayMiss = r.failureReason?.startsWith('replay miss:')
43
+ const isTimeout = r.failureReason?.startsWith('test timed out')
44
+ const isExecError = r.failureReason?.startsWith('execution error:')
45
+ const type = isReplayMiss ? 'ReplayMissError'
46
+ : isTimeout ? 'TimeoutError'
47
+ : isExecError ? 'ExecutionError'
48
+ : 'BenchmarkFailure'
49
+
50
+ lines.push(` <failure type="${type}" message="${escapeXml(r.failureReason ?? 'test failed')}">${escapeXml(r.failureReason ?? '')}</failure>`)
51
+ }
52
+
53
+ lines.push(' </testcase>')
54
+ }
55
+
56
+ lines.push(' </testsuite>')
57
+ lines.push('</testsuites>')
58
+
59
+ console.log(lines.join('\n'))
60
+ }
61
+ }
@@ -0,0 +1,208 @@
1
+ import chalk from 'chalk'
2
+ import { fetchTestGroups, submitTestRun, createBatch } from './api-client.js'
3
+ import { executeTest } from './executor.js'
4
+ import { detectGitInfo } from './git-info.js'
5
+ import type { CIRunConfig, CIRunSummary, CITestResult } from './types.js'
6
+
7
+ // ─── CI Runner ───────────────────────────────────────────────
8
+ // Orchestrates the full CI/CD test flow:
9
+ // 1. Fetch test groups from backend
10
+ // 2. Execute each test
11
+ // 3. Submit results
12
+ // 4. Create batch
13
+ // 5. Print summary
14
+
15
+ export async function runCI(config: CIRunConfig): Promise<CIRunSummary> {
16
+ const { serverUrl, apiKey } = config
17
+ const cwd = process.cwd()
18
+
19
+ // Debug: log config so CI logs show what was received
20
+ console.log(`[elasticdash ci] serverUrl: ${serverUrl || '(not set)'}`)
21
+ console.log(`[elasticdash ci] apiKey: ${apiKey ? apiKey.substring(0, 7) + '...' + apiKey.substring(apiKey.length - 4) : '(not set)'}`)
22
+
23
+ // Merge explicit git info with auto-detected CI env
24
+ const detected = detectGitInfo()
25
+ const gitBranch = config.gitBranch || detected.branch
26
+ const gitCommit = config.gitCommit || detected.commit
27
+ const gitCommitMessage = config.gitCommitMessage || detected.commitMessage
28
+ const gitPrNumber = config.gitPrNumber || detected.prNumber
29
+ const gitPrUrl = config.gitPrUrl || detected.prUrl
30
+ const triggeredBy = config.triggeredBy || 'ci'
31
+
32
+ const overallStart = Date.now()
33
+
34
+ // Step 1: Fetch test groups
35
+ console.log(chalk.cyan('\n[elasticdash ci] Fetching test groups...'))
36
+
37
+ const testGroups = await fetchTestGroups(serverUrl, apiKey, {
38
+ workflowName: config.workflowName,
39
+ tags: config.tags,
40
+ })
41
+
42
+ if (testGroups.length === 0) {
43
+ console.log(chalk.yellow('[elasticdash ci] No active test groups found.'))
44
+ return {
45
+ total: 0, passed: 0, failed: 0, skipped: 0,
46
+ durationMs: Date.now() - overallStart,
47
+ batchId: null,
48
+ results: [],
49
+ }
50
+ }
51
+
52
+ const totalTests = testGroups.reduce((sum, g) => sum + g.tests.length, 0)
53
+ console.log(chalk.cyan(`[elasticdash ci] Found ${testGroups.length} test group(s), ${totalTests} test(s) total.\n`))
54
+
55
+ // Step 2 & 3: Execute and submit
56
+ const allResults: CITestResult[] = []
57
+ const runIds: number[] = []
58
+
59
+ for (const group of testGroups) {
60
+ console.log(chalk.white.bold(` ${group.name}`) + chalk.gray(` (${group.tests.length} tests)`))
61
+
62
+ for (const test of group.tests) {
63
+ const testLabel = test.name || `${test.test_type}:${test.target_step_name || 'unnamed'}`
64
+ process.stdout.write(chalk.gray(` ${testLabel} ... `))
65
+
66
+ const testStart = Date.now()
67
+ let testResult: CITestResult
68
+
69
+ try {
70
+ const execution = await executeTest(test, cwd)
71
+
72
+ // Submit result to backend
73
+ const payload = {
74
+ testGroupTestId: test.id,
75
+ triggeredBy,
76
+ passed: execution.passed,
77
+ summary: execution.passed ? 'Passed' : 'Failed',
78
+ gitBranch, gitCommit, gitCommitMessage, gitPrNumber, gitPrUrl,
79
+ singleRuns: execution.singleRuns,
80
+ expectationResults: execution.expectationResults,
81
+ startedAt: new Date(testStart).toISOString(),
82
+ completedAt: new Date().toISOString(),
83
+ }
84
+
85
+ let runId: number | null = null
86
+ try {
87
+ const submitted = await submitTestRun(serverUrl, apiKey, group.id, payload)
88
+ runId = submitted.id
89
+ runIds.push(runId)
90
+ } catch (submitErr) {
91
+ // Non-fatal: log but don't fail the test
92
+ console.error(chalk.yellow(`\n [warn] Failed to submit result: ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`))
93
+ }
94
+
95
+ testResult = {
96
+ testGroupId: group.id,
97
+ testGroupName: group.name,
98
+ testId: test.id,
99
+ testName: test.name,
100
+ testType: test.test_type,
101
+ passed: execution.passed,
102
+ runId,
103
+ singleRuns: execution.singleRuns,
104
+ expectationResults: execution.expectationResults,
105
+ durationMs: execution.durationMs,
106
+ }
107
+ } catch (err) {
108
+ testResult = {
109
+ testGroupId: group.id,
110
+ testGroupName: group.name,
111
+ testId: test.id,
112
+ testName: test.name,
113
+ testType: test.test_type,
114
+ passed: false,
115
+ runId: null,
116
+ singleRuns: [],
117
+ expectationResults: [],
118
+ error: err instanceof Error ? err.message : String(err),
119
+ durationMs: Date.now() - testStart,
120
+ }
121
+ }
122
+
123
+ allResults.push(testResult)
124
+
125
+ // Print result
126
+ const durationStr = chalk.gray(`(${testResult.durationMs}ms)`)
127
+ if (testResult.passed) {
128
+ console.log(chalk.green('PASS') + ` ${durationStr}`)
129
+ } else {
130
+ console.log(chalk.red('FAIL') + ` ${durationStr}`)
131
+ if (testResult.error) {
132
+ console.log(chalk.red(` ${testResult.error}`))
133
+ }
134
+ // Show errors from individual single runs
135
+ for (const run of testResult.singleRuns) {
136
+ if (!run.passed && run.error) {
137
+ console.log(chalk.red(` [run ${run.runIndex}] ${run.error}`))
138
+ }
139
+ }
140
+ // Show failed expectations
141
+ for (const exp of testResult.expectationResults) {
142
+ if (!exp.passed) {
143
+ console.log(chalk.red(` [${exp.type}] ${exp.detail || 'failed'}`))
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ console.log() // blank line between groups
150
+ }
151
+
152
+ // Step 4: Create batch
153
+ let batchId: number | null = null
154
+ if (runIds.length > 0) {
155
+ try {
156
+ const passedCount = allResults.filter((r) => r.passed).length
157
+ const allPassed = passedCount === allResults.length
158
+
159
+ const batch = await createBatch(serverUrl, apiKey, {
160
+ testGroupRunIds: runIds,
161
+ status: allPassed ? 'success' : 'failed',
162
+ passed: allPassed,
163
+ summary: `CI: ${passedCount}/${allResults.length} passed`,
164
+ gitBranch, gitCommit,
165
+ startedAt: new Date(overallStart).toISOString(),
166
+ completedAt: new Date().toISOString(),
167
+ })
168
+ batchId = batch.id
169
+ } catch {
170
+ // Non-fatal
171
+ }
172
+ }
173
+
174
+ // Step 5: Print summary
175
+ const passed = allResults.filter((r) => r.passed).length
176
+ const failed = allResults.filter((r) => !r.passed).length
177
+ const durationMs = Date.now() - overallStart
178
+
179
+ console.log(chalk.white.bold('─'.repeat(50)))
180
+ console.log(chalk.white.bold('Summary'))
181
+ console.log(chalk.white.bold('─'.repeat(50)))
182
+ console.log(` Total: ${allResults.length}`)
183
+ console.log(` ${chalk.green(`Passed: ${passed}`)}`)
184
+ if (failed > 0) {
185
+ console.log(` ${chalk.red(`Failed: ${failed}`)}`)
186
+ }
187
+ console.log(` Duration: ${(durationMs / 1000).toFixed(1)}s`)
188
+ if (batchId) {
189
+ console.log(` Batch ID: ${batchId}`)
190
+ }
191
+ console.log(chalk.white.bold('─'.repeat(50)))
192
+
193
+ if (failed > 0) {
194
+ console.log(chalk.red(`\n[elasticdash ci] ${failed} test(s) failed.\n`))
195
+ } else {
196
+ console.log(chalk.green(`\n[elasticdash ci] All tests passed.\n`))
197
+ }
198
+
199
+ return {
200
+ total: allResults.length,
201
+ passed,
202
+ failed,
203
+ skipped: 0,
204
+ durationMs,
205
+ batchId,
206
+ results: allResults,
207
+ }
208
+ }
@@ -0,0 +1,16 @@
1
+ import fg from 'fast-glob'
2
+ import { resolve } from 'node:path'
3
+
4
+ export interface DiscoverOptions {
5
+ cwd?: string
6
+ }
7
+
8
+ export async function discoverTestFiles(options?: DiscoverOptions): Promise<string[]> {
9
+ const cwd = options?.cwd ?? process.cwd()
10
+ const files = await fg('**/ed_tests.{ts,js}', {
11
+ cwd,
12
+ absolute: true,
13
+ ignore: ['**/node_modules/**', '**/.git/**', '**/.ed_traces/**'],
14
+ })
15
+ return files.sort()
16
+ }