@miragon/pulse-analytics-connector 0.0.0-bootstrap.0 → 0.21.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 (357) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -3
  3. package/dist/bpmn-name-lookup.d.ts +72 -0
  4. package/dist/bpmn-name-lookup.d.ts.map +1 -0
  5. package/dist/bpmn-name-lookup.js +123 -0
  6. package/dist/bpmn-name-lookup.js.map +1 -0
  7. package/dist/bpmn-name-lookup.test.d.ts +2 -0
  8. package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
  9. package/dist/bpmn-name-lookup.test.js +287 -0
  10. package/dist/bpmn-name-lookup.test.js.map +1 -0
  11. package/dist/bpmn-names.d.ts +38 -0
  12. package/dist/bpmn-names.d.ts.map +1 -0
  13. package/dist/bpmn-names.js +22 -0
  14. package/dist/bpmn-names.js.map +1 -0
  15. package/dist/brand-gates.test.d.ts +2 -0
  16. package/dist/brand-gates.test.d.ts.map +1 -0
  17. package/dist/brand-gates.test.js +58 -0
  18. package/dist/brand-gates.test.js.map +1 -0
  19. package/dist/builder-texts.d.ts +4 -0
  20. package/dist/builder-texts.d.ts.map +1 -0
  21. package/dist/builder-texts.js +14 -0
  22. package/dist/builder-texts.js.map +1 -0
  23. package/dist/builder-texts.test.d.ts +2 -0
  24. package/dist/builder-texts.test.d.ts.map +1 -0
  25. package/dist/builder-texts.test.js +32 -0
  26. package/dist/builder-texts.test.js.map +1 -0
  27. package/dist/definition.d.ts +3 -0
  28. package/dist/definition.d.ts.map +1 -0
  29. package/dist/definition.js +151 -0
  30. package/dist/definition.js.map +1 -0
  31. package/dist/engine-ids.d.ts +42 -0
  32. package/dist/engine-ids.d.ts.map +1 -0
  33. package/dist/engine-ids.js +50 -0
  34. package/dist/engine-ids.js.map +1 -0
  35. package/dist/engine-scope.test.d.ts +2 -0
  36. package/dist/engine-scope.test.d.ts.map +1 -0
  37. package/dist/engine-scope.test.js +260 -0
  38. package/dist/engine-scope.test.js.map +1 -0
  39. package/dist/heatmap-data.d.ts +15 -0
  40. package/dist/heatmap-data.d.ts.map +1 -0
  41. package/dist/heatmap-data.js +2 -0
  42. package/dist/heatmap-data.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/instructions.d.ts +8 -0
  48. package/dist/instructions.d.ts.map +1 -0
  49. package/dist/instructions.js +33 -0
  50. package/dist/instructions.js.map +1 -0
  51. package/dist/messages/de.ask-ai.d.ts +9 -0
  52. package/dist/messages/de.ask-ai.d.ts.map +1 -0
  53. package/dist/messages/de.ask-ai.js +18 -0
  54. package/dist/messages/de.ask-ai.js.map +1 -0
  55. package/dist/messages/de.builder.d.ts +12 -0
  56. package/dist/messages/de.builder.d.ts.map +1 -0
  57. package/dist/messages/de.builder.js +53 -0
  58. package/dist/messages/de.builder.js.map +1 -0
  59. package/dist/messages/de.server.d.ts +8 -0
  60. package/dist/messages/de.server.d.ts.map +1 -0
  61. package/dist/messages/de.server.js +26 -0
  62. package/dist/messages/de.server.js.map +1 -0
  63. package/dist/messages/de.sweep.d.ts +10 -0
  64. package/dist/messages/de.sweep.d.ts.map +1 -0
  65. package/dist/messages/de.sweep.js +203 -0
  66. package/dist/messages/de.sweep.js.map +1 -0
  67. package/dist/messages/en.ask-ai.d.ts +25 -0
  68. package/dist/messages/en.ask-ai.d.ts.map +1 -0
  69. package/dist/messages/en.ask-ai.js +24 -0
  70. package/dist/messages/en.ask-ai.js.map +1 -0
  71. package/dist/messages/en.builder.d.ts +12 -0
  72. package/dist/messages/en.builder.d.ts.map +1 -0
  73. package/dist/messages/en.builder.js +53 -0
  74. package/dist/messages/en.builder.js.map +1 -0
  75. package/dist/messages/en.server.d.ts +8 -0
  76. package/dist/messages/en.server.d.ts.map +1 -0
  77. package/dist/messages/en.server.js +26 -0
  78. package/dist/messages/en.server.js.map +1 -0
  79. package/dist/messages/en.sweep.d.ts +11 -0
  80. package/dist/messages/en.sweep.d.ts.map +1 -0
  81. package/dist/messages/en.sweep.js +204 -0
  82. package/dist/messages/en.sweep.js.map +1 -0
  83. package/dist/messages/glossary.d.ts +26 -0
  84. package/dist/messages/glossary.d.ts.map +1 -0
  85. package/dist/messages/glossary.js +37 -0
  86. package/dist/messages/glossary.js.map +1 -0
  87. package/dist/messages/glossary.test.d.ts +2 -0
  88. package/dist/messages/glossary.test.d.ts.map +1 -0
  89. package/dist/messages/glossary.test.js +109 -0
  90. package/dist/messages/glossary.test.js.map +1 -0
  91. package/dist/messages/index.d.ts +12 -0
  92. package/dist/messages/index.d.ts.map +1 -0
  93. package/dist/messages/index.js +23 -0
  94. package/dist/messages/index.js.map +1 -0
  95. package/dist/messages/plural.d.ts +10 -0
  96. package/dist/messages/plural.d.ts.map +1 -0
  97. package/dist/messages/plural.js +17 -0
  98. package/dist/messages/plural.js.map +1 -0
  99. package/dist/messages/use-t.d.ts +10 -0
  100. package/dist/messages/use-t.d.ts.map +1 -0
  101. package/dist/messages/use-t.js +10 -0
  102. package/dist/messages/use-t.js.map +1 -0
  103. package/dist/module.d.ts +48 -0
  104. package/dist/module.d.ts.map +1 -0
  105. package/dist/module.js +174 -0
  106. package/dist/module.js.map +1 -0
  107. package/dist/module.test.d.ts +2 -0
  108. package/dist/module.test.d.ts.map +1 -0
  109. package/dist/module.test.js +230 -0
  110. package/dist/module.test.js.map +1 -0
  111. package/dist/named-views.d.ts +27 -0
  112. package/dist/named-views.d.ts.map +1 -0
  113. package/dist/named-views.js +73 -0
  114. package/dist/named-views.js.map +1 -0
  115. package/dist/named-views.test.d.ts +2 -0
  116. package/dist/named-views.test.d.ts.map +1 -0
  117. package/dist/named-views.test.js +110 -0
  118. package/dist/named-views.test.js.map +1 -0
  119. package/dist/plugin.d.ts +44 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +52 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/prometheus-wiring.test.d.ts +2 -0
  124. package/dist/prometheus-wiring.test.d.ts.map +1 -0
  125. package/dist/prometheus-wiring.test.js +142 -0
  126. package/dist/prometheus-wiring.test.js.map +1 -0
  127. package/dist/server-locale.d.ts +48 -0
  128. package/dist/server-locale.d.ts.map +1 -0
  129. package/dist/server-locale.js +33 -0
  130. package/dist/server-locale.js.map +1 -0
  131. package/dist/settings-tools.d.ts +23 -0
  132. package/dist/settings-tools.d.ts.map +1 -0
  133. package/dist/settings-tools.js +100 -0
  134. package/dist/settings-tools.js.map +1 -0
  135. package/dist/settings.d.ts +77 -0
  136. package/dist/settings.d.ts.map +1 -0
  137. package/dist/settings.js +99 -0
  138. package/dist/settings.js.map +1 -0
  139. package/dist/settings.test.d.ts +2 -0
  140. package/dist/settings.test.d.ts.map +1 -0
  141. package/dist/settings.test.js +299 -0
  142. package/dist/settings.test.js.map +1 -0
  143. package/dist/steps/app-config.d.ts +30 -0
  144. package/dist/steps/app-config.d.ts.map +1 -0
  145. package/dist/steps/app-config.js +46 -0
  146. package/dist/steps/app-config.js.map +1 -0
  147. package/dist/steps/dashboard.d.ts +12 -0
  148. package/dist/steps/dashboard.d.ts.map +1 -0
  149. package/dist/steps/dashboard.js +58 -0
  150. package/dist/steps/dashboard.js.map +1 -0
  151. package/dist/steps/failure-dashboard.d.ts +10 -0
  152. package/dist/steps/failure-dashboard.d.ts.map +1 -0
  153. package/dist/steps/failure-dashboard.js +32 -0
  154. package/dist/steps/failure-dashboard.js.map +1 -0
  155. package/dist/steps/index.d.ts +3 -0
  156. package/dist/steps/index.d.ts.map +1 -0
  157. package/dist/steps/index.js +3 -0
  158. package/dist/steps/index.js.map +1 -0
  159. package/dist/steps/steps.test.d.ts +2 -0
  160. package/dist/steps/steps.test.d.ts.map +1 -0
  161. package/dist/steps/steps.test.js +153 -0
  162. package/dist/steps/steps.test.js.map +1 -0
  163. package/dist/tool-names.d.ts +13 -0
  164. package/dist/tool-names.d.ts.map +1 -0
  165. package/dist/tool-names.js +27 -0
  166. package/dist/tool-names.js.map +1 -0
  167. package/dist/tools/cluster-compare.d.ts +8 -0
  168. package/dist/tools/cluster-compare.d.ts.map +1 -0
  169. package/dist/tools/cluster-compare.js +17 -0
  170. package/dist/tools/cluster-compare.js.map +1 -0
  171. package/dist/tools/element.d.ts +8 -0
  172. package/dist/tools/element.d.ts.map +1 -0
  173. package/dist/tools/element.js +25 -0
  174. package/dist/tools/element.js.map +1 -0
  175. package/dist/tools/engine-compare.d.ts +8 -0
  176. package/dist/tools/engine-compare.d.ts.map +1 -0
  177. package/dist/tools/engine-compare.js +18 -0
  178. package/dist/tools/engine-compare.js.map +1 -0
  179. package/dist/tools/engine-compare.test.d.ts +2 -0
  180. package/dist/tools/engine-compare.test.d.ts.map +1 -0
  181. package/dist/tools/engine-compare.test.js +166 -0
  182. package/dist/tools/engine-compare.test.js.map +1 -0
  183. package/dist/tools/engine-landscape.d.ts +7 -0
  184. package/dist/tools/engine-landscape.d.ts.map +1 -0
  185. package/dist/tools/engine-landscape.js +13 -0
  186. package/dist/tools/engine-landscape.js.map +1 -0
  187. package/dist/tools/engine-landscape.test.d.ts +2 -0
  188. package/dist/tools/engine-landscape.test.d.ts.map +1 -0
  189. package/dist/tools/engine-landscape.test.js +110 -0
  190. package/dist/tools/engine-landscape.test.js.map +1 -0
  191. package/dist/tools/failures.d.ts +7 -0
  192. package/dist/tools/failures.d.ts.map +1 -0
  193. package/dist/tools/failures.js +13 -0
  194. package/dist/tools/failures.js.map +1 -0
  195. package/dist/tools/health.d.ts +7 -0
  196. package/dist/tools/health.d.ts.map +1 -0
  197. package/dist/tools/health.js +23 -0
  198. package/dist/tools/health.js.map +1 -0
  199. package/dist/tools/health.test.d.ts +2 -0
  200. package/dist/tools/health.test.d.ts.map +1 -0
  201. package/dist/tools/health.test.js +119 -0
  202. package/dist/tools/health.test.js.map +1 -0
  203. package/dist/tools/index.d.ts +18 -0
  204. package/dist/tools/index.d.ts.map +1 -0
  205. package/dist/tools/index.js +35 -0
  206. package/dist/tools/index.js.map +1 -0
  207. package/dist/tools/performance.d.ts +8 -0
  208. package/dist/tools/performance.d.ts.map +1 -0
  209. package/dist/tools/performance.js +25 -0
  210. package/dist/tools/performance.js.map +1 -0
  211. package/dist/tools/performance.test.d.ts +2 -0
  212. package/dist/tools/performance.test.d.ts.map +1 -0
  213. package/dist/tools/performance.test.js +176 -0
  214. package/dist/tools/performance.test.js.map +1 -0
  215. package/dist/tools/version-compare.d.ts +8 -0
  216. package/dist/tools/version-compare.d.ts.map +1 -0
  217. package/dist/tools/version-compare.js +17 -0
  218. package/dist/tools/version-compare.js.map +1 -0
  219. package/dist/tools/version-compare.test.d.ts +2 -0
  220. package/dist/tools/version-compare.test.d.ts.map +1 -0
  221. package/dist/tools/version-compare.test.js +82 -0
  222. package/dist/tools/version-compare.test.js.map +1 -0
  223. package/dist/toolsets.d.ts +17 -0
  224. package/dist/toolsets.d.ts.map +1 -0
  225. package/dist/toolsets.js +47 -0
  226. package/dist/toolsets.js.map +1 -0
  227. package/dist/toolsets.test.d.ts +2 -0
  228. package/dist/toolsets.test.d.ts.map +1 -0
  229. package/dist/toolsets.test.js +91 -0
  230. package/dist/toolsets.test.js.map +1 -0
  231. package/dist/view-title.test.d.ts +2 -0
  232. package/dist/view-title.test.d.ts.map +1 -0
  233. package/dist/view-title.test.js +91 -0
  234. package/dist/view-title.test.js.map +1 -0
  235. package/dist/widget-tools/comparisons.d.ts +13 -0
  236. package/dist/widget-tools/comparisons.d.ts.map +1 -0
  237. package/dist/widget-tools/comparisons.js +177 -0
  238. package/dist/widget-tools/comparisons.js.map +1 -0
  239. package/dist/widget-tools/comparisons.test.d.ts +2 -0
  240. package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
  241. package/dist/widget-tools/comparisons.test.js +123 -0
  242. package/dist/widget-tools/comparisons.test.js.map +1 -0
  243. package/dist/widget-tools/shared.d.ts +36 -0
  244. package/dist/widget-tools/shared.d.ts.map +1 -0
  245. package/dist/widget-tools/shared.js +39 -0
  246. package/dist/widget-tools/shared.js.map +1 -0
  247. package/dist/widget-tools.d.ts +25 -0
  248. package/dist/widget-tools.d.ts.map +1 -0
  249. package/dist/widget-tools.js +269 -0
  250. package/dist/widget-tools.js.map +1 -0
  251. package/dist/widget-tools.test.d.ts +2 -0
  252. package/dist/widget-tools.test.d.ts.map +1 -0
  253. package/dist/widget-tools.test.js +272 -0
  254. package/dist/widget-tools.test.js.map +1 -0
  255. package/package.json +91 -5
  256. package/src/bpmn-name-lookup.test.ts +333 -0
  257. package/src/bpmn-name-lookup.ts +197 -0
  258. package/src/bpmn-names.ts +51 -0
  259. package/src/brand-gates.test.ts +69 -0
  260. package/src/builder-texts.test.ts +40 -0
  261. package/src/builder-texts.ts +15 -0
  262. package/src/definition.ts +181 -0
  263. package/src/engine-ids.ts +85 -0
  264. package/src/engine-scope.test.ts +324 -0
  265. package/src/heatmap-data.ts +14 -0
  266. package/src/index.ts +4 -0
  267. package/src/instructions.ts +33 -0
  268. package/src/messages/GLOSSARY.md +88 -0
  269. package/src/messages/de.ask-ai.ts +28 -0
  270. package/src/messages/de.builder.ts +77 -0
  271. package/src/messages/de.server.ts +39 -0
  272. package/src/messages/de.sweep.ts +244 -0
  273. package/src/messages/en.ask-ai.ts +34 -0
  274. package/src/messages/en.builder.ts +76 -0
  275. package/src/messages/en.server.ts +39 -0
  276. package/src/messages/en.sweep.ts +240 -0
  277. package/src/messages/glossary.test.ts +122 -0
  278. package/src/messages/glossary.ts +62 -0
  279. package/src/messages/index.ts +24 -0
  280. package/src/messages/plural.ts +18 -0
  281. package/src/messages/use-t.ts +12 -0
  282. package/src/module.test.ts +319 -0
  283. package/src/module.ts +211 -0
  284. package/src/named-views.test.ts +141 -0
  285. package/src/named-views.ts +97 -0
  286. package/src/plugin.ts +94 -0
  287. package/src/prometheus-wiring.test.ts +174 -0
  288. package/src/server-locale.ts +59 -0
  289. package/src/settings-tools.ts +170 -0
  290. package/src/settings.test.ts +402 -0
  291. package/src/settings.ts +114 -0
  292. package/src/steps/app-config.ts +75 -0
  293. package/src/steps/dashboard.ts +73 -0
  294. package/src/steps/failure-dashboard.ts +38 -0
  295. package/src/steps/index.ts +2 -0
  296. package/src/steps/steps.test.ts +216 -0
  297. package/src/tool-names.ts +26 -0
  298. package/src/tools/cluster-compare.ts +28 -0
  299. package/src/tools/element.ts +35 -0
  300. package/src/tools/engine-compare.test.ts +210 -0
  301. package/src/tools/engine-compare.ts +30 -0
  302. package/src/tools/engine-landscape.test.ts +138 -0
  303. package/src/tools/engine-landscape.ts +21 -0
  304. package/src/tools/failures.ts +19 -0
  305. package/src/tools/health.test.ts +156 -0
  306. package/src/tools/health.ts +29 -0
  307. package/src/tools/index.ts +44 -0
  308. package/src/tools/performance.test.ts +210 -0
  309. package/src/tools/performance.ts +38 -0
  310. package/src/tools/version-compare.test.ts +97 -0
  311. package/src/tools/version-compare.ts +28 -0
  312. package/src/toolsets.test.ts +107 -0
  313. package/src/toolsets.ts +56 -0
  314. package/src/view-title.test.ts +123 -0
  315. package/src/widget-tools/comparisons.test.ts +170 -0
  316. package/src/widget-tools/comparisons.ts +229 -0
  317. package/src/widget-tools/shared.ts +67 -0
  318. package/src/widget-tools.test.ts +306 -0
  319. package/src/widget-tools.ts +375 -0
  320. package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
  321. package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
  322. package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
  323. package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
  324. package/src/widgets/analytics-dashboard/lib.ts +43 -0
  325. package/src/widgets/bpmn-heatmap.test.tsx +75 -0
  326. package/src/widgets/bpmn-heatmap.tsx +127 -0
  327. package/src/widgets/catalogue-sync.test.ts +16 -0
  328. package/src/widgets/cluster-compare.tsx +148 -0
  329. package/src/widgets/comparison-shared.test.ts +237 -0
  330. package/src/widgets/comparison-shared.tsx +371 -0
  331. package/src/widgets/comparison-widgets.test.tsx +307 -0
  332. package/src/widgets/dashboard-widgets.test.tsx +397 -0
  333. package/src/widgets/engine-compare.tsx +87 -0
  334. package/src/widgets/engine-landscape.tsx +352 -0
  335. package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
  336. package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
  337. package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
  338. package/src/widgets/failure-dashboard/lib.ts +32 -0
  339. package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
  340. package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
  341. package/src/widgets/figure.tsx +79 -0
  342. package/src/widgets/hand-off.test.ts +126 -0
  343. package/src/widgets/hand-off.ts +102 -0
  344. package/src/widgets/index.ts +124 -0
  345. package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
  346. package/src/widgets/model-descriptions.test.ts +251 -0
  347. package/src/widgets/model-descriptions.ts +428 -0
  348. package/src/widgets/names.test.ts +32 -0
  349. package/src/widgets/names.ts +39 -0
  350. package/src/widgets/query-gate.test.tsx +57 -0
  351. package/src/widgets/query-gate.tsx +50 -0
  352. package/src/widgets/settings-section.test.tsx +174 -0
  353. package/src/widgets/settings-section.tsx +244 -0
  354. package/src/widgets/version-compare.test.ts +68 -0
  355. package/src/widgets/version-compare.tsx +96 -0
  356. package/src/widgets/view-meta.ts +18 -0
  357. package/src/widgets/widget-render.test-support.tsx +103 -0
@@ -0,0 +1,307 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
3
+ import type { ComponentType } from "react"
4
+ import { cleanup, render, screen, within } from "@testing-library/react"
5
+ import { LocaleProvider, queryClient } from "@miragon/mcp-toolkit-ui"
6
+ import { WidgetFixtureHost } from "@miragon/mcp-toolkit-ui/app"
7
+ import type {
8
+ ClusterCompareResult,
9
+ CompareKpiDelta,
10
+ EngineCompareResult,
11
+ VersionCompareKpi,
12
+ VersionCompareResult,
13
+ } from "@miragon/pulse-analytics-client"
14
+ import { setFormatLocale } from "@miragon/pulse-widget-shell/testing"
15
+ import { VersionCompareWidget } from "./version-compare.js"
16
+ import { EngineCompareWidget } from "./engine-compare.js"
17
+ import { ClusterCompareWidget } from "./cluster-compare.js"
18
+
19
+ /**
20
+ * The comparison widgets as a person reads them, in German and English (U6):
21
+ * a volume row (starts) carries no verdict, a quality row is judged only from
22
+ * its threshold on, a suppressed comparison says "nicht belastbar" instead of
23
+ * judging, and the reference frame stands under the title (U7).
24
+ */
25
+
26
+ const NOW = "2026-10-10T12:40:00.000Z"
27
+ const AS_OF = "2026-10-10T12:32:00.000Z"
28
+
29
+ beforeEach(() => {
30
+ vi.useFakeTimers({ toFake: ["Date"] })
31
+ vi.setSystemTime(new Date(NOW))
32
+ })
33
+ afterEach(() => {
34
+ cleanup()
35
+ queryClient.clear()
36
+ setFormatLocale(undefined)
37
+ vi.useRealTimers()
38
+ })
39
+
40
+ function renderIn(language: "de" | "en", Widget: ComponentType<{ data: never }>, data: unknown) {
41
+ setFormatLocale({
42
+ language,
43
+ locale: language === "de" ? "de-DE" : "en-GB",
44
+ timeZone: "Europe/Berlin",
45
+ })
46
+ return render(
47
+ <LocaleProvider locale={language}>
48
+ <WidgetFixtureHost
49
+ widget={Widget as unknown as ComponentType<Record<string, unknown>>}
50
+ data={data as Record<string, unknown>}
51
+ />
52
+ </LocaleProvider>,
53
+ )
54
+ }
55
+
56
+ /** The text of one metric row's change cell, spaces normalized. */
57
+ function deltaCell(label: string) {
58
+ const row = screen.getByText(label).closest("tr")!
59
+ const cell = row.querySelector("[data-verdict]")!
60
+ return {
61
+ verdict: cell.getAttribute("data-verdict"),
62
+ text: cell.textContent.replace(/\u00a0/g, " "),
63
+ icon: cell.querySelector("svg")?.getAttribute("class") ?? null,
64
+ }
65
+ }
66
+
67
+ const metaLine = () =>
68
+ document.querySelector("[data-view-meta]")?.textContent?.replace(/\u00a0/g, " ")
69
+
70
+ const versionKpi = (version: number, over: Partial<VersionCompareKpi> = {}): VersionCompareKpi => ({
71
+ version,
72
+ bucket: version === 1 ? "versionA" : "versionB",
73
+ instance_count: 576,
74
+ completed_count: 500,
75
+ incident_count: 12,
76
+ incident_rate_pct: 2.1,
77
+ element_incident_count: null,
78
+ element_incident_rate_pct: null,
79
+ avg_duration_sec: 8340,
80
+ p95_duration_sec: 20100,
81
+ ...over,
82
+ })
83
+
84
+ const DELTA: CompareKpiDelta = {
85
+ // Traffic moved to v2: a volume, never a verdict.
86
+ started_per_day_delta_pct: -84.7,
87
+ // Past the 1-per-100-starts threshold: worse.
88
+ incident_rate_delta_pp: 1.4,
89
+ element_incident_rate_delta_pp: null,
90
+ // Slower by 25 %: past the 5 % duration threshold.
91
+ avg_duration_delta_pct: 25,
92
+ // Rounding jitter: below the threshold, neutral.
93
+ p95_duration_delta_pct: 3,
94
+ }
95
+
96
+ const VERSION: VersionCompareResult = {
97
+ engines: ["prod-leasing"],
98
+ processDefinitionKey: "miraveloLeasing",
99
+ versionA: 1,
100
+ versionB: 2,
101
+ windowDays: 14,
102
+ minBucketSize: 10,
103
+ suppressed: false,
104
+ kpis: [versionKpi(1), versionKpi(2, { instance_count: 88 })],
105
+ delta: DELTA,
106
+ asOf: AS_OF,
107
+ }
108
+
109
+ describe("VersionCompareWidget in German", () => {
110
+ it("judges quality from its threshold on and never the volume", () => {
111
+ renderIn("de", VersionCompareWidget, VERSION)
112
+
113
+ expect(screen.getByText("Versionsvergleich")).toBeTruthy()
114
+ expect(screen.getByText("Änderung")).toBeTruthy()
115
+ expect(screen.getByText("v1 → v2")).toBeTruthy()
116
+
117
+ const starts = deltaCell("Starts (Änderung je Tag)")
118
+ expect(starts).toEqual({ verdict: "none", text: "-84,7 %", icon: null })
119
+
120
+ const avg = deltaCell("Durchschnittliche Dauer")
121
+ expect(avg.verdict).toBe("worse")
122
+ expect(avg.text).toBe("+25 %schlechter")
123
+ expect(avg.icon).toContain("lucide-trending-up")
124
+ expect(avg.icon).toContain("text-danger")
125
+
126
+ expect(deltaCell("P95-Dauer")).toEqual({ verdict: "none", text: "+3 %", icon: null })
127
+ // Incidents are measured per version; a process-wide comparison has no element row.
128
+ expect(deltaCell("Incidents je 100 Starts").verdict).toBe("worse")
129
+ expect(deltaCell("Incidents je 100 Starts").text).toBe("+1,4schlechter")
130
+ expect(screen.queryAllByText("nicht gemessen")).toHaveLength(0)
131
+ expect(screen.queryByText("Incidents je 100 Starts am Element")).toBeNull()
132
+ })
133
+
134
+ it("states the reference frame and hands off with a verb that names the chat", () => {
135
+ renderIn("de", VersionCompareWidget, VERSION)
136
+ expect(metaLine()).toBe("Letzte 14 Tage · 1 Engine · Stand 14:32")
137
+ const ask = screen.getByRole("button", { name: /Im Chat bewerten/ })
138
+ expect(ask.querySelector("svg")?.getAttribute("class")).toContain("lucide-list-checks")
139
+ })
140
+
141
+ it("calls every quality change of a suppressed comparison not reliable", () => {
142
+ renderIn("de", VersionCompareWidget, { ...VERSION, suppressed: true })
143
+
144
+ expect(screen.getByText("Zu wenig Daten: weniger als 10 Instanzen je Version")).toBeTruthy()
145
+ const avg = deltaCell("Durchschnittliche Dauer")
146
+ expect(avg).toEqual({ verdict: "notReliable", text: "+25 %nicht belastbar", icon: null })
147
+ expect(deltaCell("Starts (Änderung je Tag)").verdict).toBe("none")
148
+ expect(screen.queryByText("schlechter")).toBeNull()
149
+ expect(screen.queryByText("besser")).toBeNull()
150
+ })
151
+ })
152
+
153
+ const engineKpi = (engineId: string, bucket: "engineA" | "engineB", over = {}) => ({
154
+ engineId,
155
+ bucket,
156
+ instance_count: 576,
157
+ completed_count: 560,
158
+ incident_count: 38,
159
+ incident_rate_pct: 6.6,
160
+ element_incident_count: null,
161
+ element_incident_rate_pct: null,
162
+ avg_duration_sec: 8340,
163
+ p95_duration_sec: 20100,
164
+ ...over,
165
+ })
166
+
167
+ const ENGINES: EngineCompareResult = {
168
+ engineA: "prod-leasing",
169
+ engineB: "test-leasing",
170
+ processDefinitionKey: "miraveloLeasing",
171
+ windowDays: 14,
172
+ activityId: null,
173
+ minBucketSize: 10,
174
+ suppressed: false,
175
+ kpis: [
176
+ engineKpi("prod-leasing", "engineA"),
177
+ engineKpi("test-leasing", "engineB", { instance_count: 88, incident_rate_pct: 6.8 }),
178
+ ],
179
+ delta: {
180
+ started_per_day_delta_pct: -84.7,
181
+ incident_rate_delta_pp: 0.2,
182
+ element_incident_rate_delta_pp: null,
183
+ avg_duration_delta_pct: -12,
184
+ p95_duration_delta_pct: null,
185
+ },
186
+ asOf: AS_OF,
187
+ }
188
+
189
+ describe("EngineCompareWidget in English", () => {
190
+ it("keeps less traffic on a test engine and a rounding-size rate change neutral", () => {
191
+ renderIn("en", EngineCompareWidget, ENGINES)
192
+
193
+ expect(deltaCell("Starts (change per day)")).toEqual({
194
+ verdict: "none",
195
+ text: "-84.7%",
196
+ icon: null,
197
+ })
198
+ expect(deltaCell("Incidents per 100 starts")).toEqual({
199
+ verdict: "none",
200
+ text: "+0.2",
201
+ icon: null,
202
+ })
203
+ const avg = deltaCell("Avg duration")
204
+ expect(avg.verdict).toBe("better")
205
+ expect(avg.text).toBe("-12%better")
206
+ expect(avg.icon).toContain("lucide-trending-down")
207
+ expect(avg.icon).toContain("text-success")
208
+ expect(screen.getByText("6.6")).toBeTruthy()
209
+ expect(metaLine()).toBe("Last 14 days · As of 14:32")
210
+ const ask = screen.getByRole("button", { name: /Assess in chat/ })
211
+ expect(ask.querySelector("svg")?.getAttribute("class")).toContain("lucide-list-checks")
212
+ })
213
+
214
+ it("shows the suppressed chip as a neutral note, not an error", () => {
215
+ renderIn("en", EngineCompareWidget, { ...ENGINES, suppressed: true, minBucketSize: 1 })
216
+ const chip = screen.getByText("Too little data: fewer than 1 instance per engine")
217
+ expect(chip.closest("[data-suppressed]")?.className).not.toMatch(/(^|\s)(bg|text)-destructive/)
218
+ expect(deltaCell("Avg duration").text).toBe("-12%not reliable")
219
+ })
220
+ })
221
+
222
+ const DEPLOYED_AT = Date.parse("2026-10-10T10:40:00.000Z")
223
+ const iso = (ms: number) => new Date(ms).toISOString()
224
+
225
+ /**
226
+ * A before/after comparison whose windows measured `beforeSec` and `afterSec`
227
+ * around the deployment: the exact bounds, and `window_days` rounded to
228
+ * 0.01 d like the query does.
229
+ */
230
+ function cluster(beforeSec: number, afterSec: number): ClusterCompareResult {
231
+ const windows = {
232
+ before: { from: DEPLOYED_AT - beforeSec * 1000, to: DEPLOYED_AT, seconds: beforeSec },
233
+ after: { from: DEPLOYED_AT, to: DEPLOYED_AT + afterSec * 1000, seconds: afterSec },
234
+ }
235
+ const days = (seconds: number) => Math.round((seconds / 86_400) * 100) / 100
236
+ return {
237
+ engines: ["prod-a", "prod-b"],
238
+ processDefinitionKey: "order",
239
+ activityId: null,
240
+ deploymentTimestamp: iso(DEPLOYED_AT),
241
+ requestedWindowDays: { before: 7, after: 7 },
242
+ windowDays: { before: days(beforeSec), after: days(afterSec) },
243
+ partial: true,
244
+ minBucketSize: 10,
245
+ suppressed: false,
246
+ kpis: (["before", "after"] as const).map((period) => ({
247
+ period,
248
+ window_from: iso(windows[period].from),
249
+ window_to: iso(windows[period].to),
250
+ window_days: days(windows[period].seconds),
251
+ instance_count: 576,
252
+ completed_count: 500,
253
+ incident_count: 38,
254
+ incident_rate_pct: 6.6,
255
+ element_incident_count: null,
256
+ element_incident_rate_pct: null,
257
+ avg_duration_sec: 8340,
258
+ p95_duration_sec: 20100,
259
+ })),
260
+ delta: DELTA,
261
+ asOf: AS_OF,
262
+ }
263
+ }
264
+
265
+ const HOUR = 3600
266
+ const DAY = 24 * HOUR
267
+ const CLUSTER = cluster(7 * DAY, 2 * HOUR)
268
+
269
+ describe("ClusterCompareWidget", () => {
270
+ it("names the measured windows, a short one in hours (de)", () => {
271
+ renderIn("de", ClusterCompareWidget, CLUSTER)
272
+ expect(metaLine()).toBe("7 Tage davor, 2 Stunden danach · 2 Engines · Stand 14:32")
273
+ expect(screen.getByText("Zeitraum gekürzt")).toBeTruthy()
274
+ expect(within(screen.getByRole("table")).getByText("Vorher")).toBeTruthy()
275
+ })
276
+
277
+ it("says the same in English", () => {
278
+ renderIn("en", ClusterCompareWidget, cluster(DAY, 1.5 * DAY))
279
+ expect(metaLine()).toBe("1 day before, 1.5 days after · 2 engines · As of 14:32")
280
+ })
281
+
282
+ it("reads a window of a few minutes from its bounds, never as a rounded-up hour", () => {
283
+ renderIn("de", ClusterCompareWidget, cluster(7 * DAY, 10 * 60))
284
+ expect(metaLine()).toBe("7 Tage davor, 10 Minuten danach · 2 Engines · Stand 14:32")
285
+ })
286
+
287
+ it("says 'under 1 minute' for a deployment compared right away", () => {
288
+ renderIn("en", ClusterCompareWidget, cluster(7 * DAY, 30))
289
+ expect(metaLine()).toBe("7 days before, under 1 minute after · 2 engines · As of 14:32")
290
+ cleanup()
291
+ renderIn("de", ClusterCompareWidget, cluster(7 * DAY, 60))
292
+ expect(metaLine()).toBe("7 Tage davor, 1 Minute danach · 2 Engines · Stand 14:32")
293
+ })
294
+
295
+ it("falls back to the rounded window length when the bounds do not parse", () => {
296
+ const unparsable = cluster(7 * DAY, 2 * HOUR)
297
+ unparsable.kpis = unparsable.kpis.map((kpi) => ({ ...kpi, window_from: "", window_to: "" }))
298
+ renderIn("de", ClusterCompareWidget, unparsable)
299
+ expect(metaLine()).toBe("7 Tage davor, 2 Stunden danach · 2 Engines · Stand 14:32")
300
+ })
301
+
302
+ it("hands off to assess the comparison with the Scale icon", () => {
303
+ renderIn("de", ClusterCompareWidget, CLUSTER)
304
+ const ask = screen.getByRole("button", { name: /Im Chat bewerten/ })
305
+ expect(ask.querySelector("svg")?.getAttribute("class")).toContain("lucide-list-checks")
306
+ })
307
+ })