@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.
- package/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/bpmn-name-lookup.d.ts +72 -0
- package/dist/bpmn-name-lookup.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.js +123 -0
- package/dist/bpmn-name-lookup.js.map +1 -0
- package/dist/bpmn-name-lookup.test.d.ts +2 -0
- package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.test.js +287 -0
- package/dist/bpmn-name-lookup.test.js.map +1 -0
- package/dist/bpmn-names.d.ts +38 -0
- package/dist/bpmn-names.d.ts.map +1 -0
- package/dist/bpmn-names.js +22 -0
- package/dist/bpmn-names.js.map +1 -0
- package/dist/brand-gates.test.d.ts +2 -0
- package/dist/brand-gates.test.d.ts.map +1 -0
- package/dist/brand-gates.test.js +58 -0
- package/dist/brand-gates.test.js.map +1 -0
- package/dist/builder-texts.d.ts +4 -0
- package/dist/builder-texts.d.ts.map +1 -0
- package/dist/builder-texts.js +14 -0
- package/dist/builder-texts.js.map +1 -0
- package/dist/builder-texts.test.d.ts +2 -0
- package/dist/builder-texts.test.d.ts.map +1 -0
- package/dist/builder-texts.test.js +32 -0
- package/dist/builder-texts.test.js.map +1 -0
- package/dist/definition.d.ts +3 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +151 -0
- package/dist/definition.js.map +1 -0
- package/dist/engine-ids.d.ts +42 -0
- package/dist/engine-ids.d.ts.map +1 -0
- package/dist/engine-ids.js +50 -0
- package/dist/engine-ids.js.map +1 -0
- package/dist/engine-scope.test.d.ts +2 -0
- package/dist/engine-scope.test.d.ts.map +1 -0
- package/dist/engine-scope.test.js +260 -0
- package/dist/engine-scope.test.js.map +1 -0
- package/dist/heatmap-data.d.ts +15 -0
- package/dist/heatmap-data.d.ts.map +1 -0
- package/dist/heatmap-data.js +2 -0
- package/dist/heatmap-data.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +8 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -0
- package/dist/messages/de.ask-ai.d.ts +9 -0
- package/dist/messages/de.ask-ai.d.ts.map +1 -0
- package/dist/messages/de.ask-ai.js +18 -0
- package/dist/messages/de.ask-ai.js.map +1 -0
- package/dist/messages/de.builder.d.ts +12 -0
- package/dist/messages/de.builder.d.ts.map +1 -0
- package/dist/messages/de.builder.js +53 -0
- package/dist/messages/de.builder.js.map +1 -0
- package/dist/messages/de.server.d.ts +8 -0
- package/dist/messages/de.server.d.ts.map +1 -0
- package/dist/messages/de.server.js +26 -0
- package/dist/messages/de.server.js.map +1 -0
- package/dist/messages/de.sweep.d.ts +10 -0
- package/dist/messages/de.sweep.d.ts.map +1 -0
- package/dist/messages/de.sweep.js +203 -0
- package/dist/messages/de.sweep.js.map +1 -0
- package/dist/messages/en.ask-ai.d.ts +25 -0
- package/dist/messages/en.ask-ai.d.ts.map +1 -0
- package/dist/messages/en.ask-ai.js +24 -0
- package/dist/messages/en.ask-ai.js.map +1 -0
- package/dist/messages/en.builder.d.ts +12 -0
- package/dist/messages/en.builder.d.ts.map +1 -0
- package/dist/messages/en.builder.js +53 -0
- package/dist/messages/en.builder.js.map +1 -0
- package/dist/messages/en.server.d.ts +8 -0
- package/dist/messages/en.server.d.ts.map +1 -0
- package/dist/messages/en.server.js +26 -0
- package/dist/messages/en.server.js.map +1 -0
- package/dist/messages/en.sweep.d.ts +11 -0
- package/dist/messages/en.sweep.d.ts.map +1 -0
- package/dist/messages/en.sweep.js +204 -0
- package/dist/messages/en.sweep.js.map +1 -0
- package/dist/messages/glossary.d.ts +26 -0
- package/dist/messages/glossary.d.ts.map +1 -0
- package/dist/messages/glossary.js +37 -0
- package/dist/messages/glossary.js.map +1 -0
- package/dist/messages/glossary.test.d.ts +2 -0
- package/dist/messages/glossary.test.d.ts.map +1 -0
- package/dist/messages/glossary.test.js +109 -0
- package/dist/messages/glossary.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +23 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/plural.d.ts +10 -0
- package/dist/messages/plural.d.ts.map +1 -0
- package/dist/messages/plural.js +17 -0
- package/dist/messages/plural.js.map +1 -0
- package/dist/messages/use-t.d.ts +10 -0
- package/dist/messages/use-t.d.ts.map +1 -0
- package/dist/messages/use-t.js +10 -0
- package/dist/messages/use-t.js.map +1 -0
- package/dist/module.d.ts +48 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +174 -0
- package/dist/module.js.map +1 -0
- package/dist/module.test.d.ts +2 -0
- package/dist/module.test.d.ts.map +1 -0
- package/dist/module.test.js +230 -0
- package/dist/module.test.js.map +1 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.d.ts.map +1 -0
- package/dist/named-views.js +73 -0
- package/dist/named-views.js.map +1 -0
- package/dist/named-views.test.d.ts +2 -0
- package/dist/named-views.test.d.ts.map +1 -0
- package/dist/named-views.test.js +110 -0
- package/dist/named-views.test.js.map +1 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +52 -0
- package/dist/plugin.js.map +1 -0
- package/dist/prometheus-wiring.test.d.ts +2 -0
- package/dist/prometheus-wiring.test.d.ts.map +1 -0
- package/dist/prometheus-wiring.test.js +142 -0
- package/dist/prometheus-wiring.test.js.map +1 -0
- package/dist/server-locale.d.ts +48 -0
- package/dist/server-locale.d.ts.map +1 -0
- package/dist/server-locale.js +33 -0
- package/dist/server-locale.js.map +1 -0
- package/dist/settings-tools.d.ts +23 -0
- package/dist/settings-tools.d.ts.map +1 -0
- package/dist/settings-tools.js +100 -0
- package/dist/settings-tools.js.map +1 -0
- package/dist/settings.d.ts +77 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +99 -0
- package/dist/settings.js.map +1 -0
- package/dist/settings.test.d.ts +2 -0
- package/dist/settings.test.d.ts.map +1 -0
- package/dist/settings.test.js +299 -0
- package/dist/settings.test.js.map +1 -0
- package/dist/steps/app-config.d.ts +30 -0
- package/dist/steps/app-config.d.ts.map +1 -0
- package/dist/steps/app-config.js +46 -0
- package/dist/steps/app-config.js.map +1 -0
- package/dist/steps/dashboard.d.ts +12 -0
- package/dist/steps/dashboard.d.ts.map +1 -0
- package/dist/steps/dashboard.js +58 -0
- package/dist/steps/dashboard.js.map +1 -0
- package/dist/steps/failure-dashboard.d.ts +10 -0
- package/dist/steps/failure-dashboard.d.ts.map +1 -0
- package/dist/steps/failure-dashboard.js +32 -0
- package/dist/steps/failure-dashboard.js.map +1 -0
- package/dist/steps/index.d.ts +3 -0
- package/dist/steps/index.d.ts.map +1 -0
- package/dist/steps/index.js +3 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/steps.test.d.ts +2 -0
- package/dist/steps/steps.test.d.ts.map +1 -0
- package/dist/steps/steps.test.js +153 -0
- package/dist/steps/steps.test.js.map +1 -0
- package/dist/tool-names.d.ts +13 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +27 -0
- package/dist/tool-names.js.map +1 -0
- package/dist/tools/cluster-compare.d.ts +8 -0
- package/dist/tools/cluster-compare.d.ts.map +1 -0
- package/dist/tools/cluster-compare.js +17 -0
- package/dist/tools/cluster-compare.js.map +1 -0
- package/dist/tools/element.d.ts +8 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +25 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/engine-compare.d.ts +8 -0
- package/dist/tools/engine-compare.d.ts.map +1 -0
- package/dist/tools/engine-compare.js +18 -0
- package/dist/tools/engine-compare.js.map +1 -0
- package/dist/tools/engine-compare.test.d.ts +2 -0
- package/dist/tools/engine-compare.test.d.ts.map +1 -0
- package/dist/tools/engine-compare.test.js +166 -0
- package/dist/tools/engine-compare.test.js.map +1 -0
- package/dist/tools/engine-landscape.d.ts +7 -0
- package/dist/tools/engine-landscape.d.ts.map +1 -0
- package/dist/tools/engine-landscape.js +13 -0
- package/dist/tools/engine-landscape.js.map +1 -0
- package/dist/tools/engine-landscape.test.d.ts +2 -0
- package/dist/tools/engine-landscape.test.d.ts.map +1 -0
- package/dist/tools/engine-landscape.test.js +110 -0
- package/dist/tools/engine-landscape.test.js.map +1 -0
- package/dist/tools/failures.d.ts +7 -0
- package/dist/tools/failures.d.ts.map +1 -0
- package/dist/tools/failures.js +13 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/health.d.ts +7 -0
- package/dist/tools/health.d.ts.map +1 -0
- package/dist/tools/health.js +23 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/health.test.d.ts +2 -0
- package/dist/tools/health.test.d.ts.map +1 -0
- package/dist/tools/health.test.js +119 -0
- package/dist/tools/health.test.js.map +1 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +35 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance.d.ts +8 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +25 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/performance.test.d.ts +2 -0
- package/dist/tools/performance.test.d.ts.map +1 -0
- package/dist/tools/performance.test.js +176 -0
- package/dist/tools/performance.test.js.map +1 -0
- package/dist/tools/version-compare.d.ts +8 -0
- package/dist/tools/version-compare.d.ts.map +1 -0
- package/dist/tools/version-compare.js +17 -0
- package/dist/tools/version-compare.js.map +1 -0
- package/dist/tools/version-compare.test.d.ts +2 -0
- package/dist/tools/version-compare.test.d.ts.map +1 -0
- package/dist/tools/version-compare.test.js +82 -0
- package/dist/tools/version-compare.test.js.map +1 -0
- package/dist/toolsets.d.ts +17 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +47 -0
- package/dist/toolsets.js.map +1 -0
- package/dist/toolsets.test.d.ts +2 -0
- package/dist/toolsets.test.d.ts.map +1 -0
- package/dist/toolsets.test.js +91 -0
- package/dist/toolsets.test.js.map +1 -0
- package/dist/view-title.test.d.ts +2 -0
- package/dist/view-title.test.d.ts.map +1 -0
- package/dist/view-title.test.js +91 -0
- package/dist/view-title.test.js.map +1 -0
- package/dist/widget-tools/comparisons.d.ts +13 -0
- package/dist/widget-tools/comparisons.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.js +177 -0
- package/dist/widget-tools/comparisons.js.map +1 -0
- package/dist/widget-tools/comparisons.test.d.ts +2 -0
- package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.test.js +123 -0
- package/dist/widget-tools/comparisons.test.js.map +1 -0
- package/dist/widget-tools/shared.d.ts +36 -0
- package/dist/widget-tools/shared.d.ts.map +1 -0
- package/dist/widget-tools/shared.js +39 -0
- package/dist/widget-tools/shared.js.map +1 -0
- package/dist/widget-tools.d.ts +25 -0
- package/dist/widget-tools.d.ts.map +1 -0
- package/dist/widget-tools.js +269 -0
- package/dist/widget-tools.js.map +1 -0
- package/dist/widget-tools.test.d.ts +2 -0
- package/dist/widget-tools.test.d.ts.map +1 -0
- package/dist/widget-tools.test.js +272 -0
- package/dist/widget-tools.test.js.map +1 -0
- package/package.json +91 -5
- package/src/bpmn-name-lookup.test.ts +333 -0
- package/src/bpmn-name-lookup.ts +197 -0
- package/src/bpmn-names.ts +51 -0
- package/src/brand-gates.test.ts +69 -0
- package/src/builder-texts.test.ts +40 -0
- package/src/builder-texts.ts +15 -0
- package/src/definition.ts +181 -0
- package/src/engine-ids.ts +85 -0
- package/src/engine-scope.test.ts +324 -0
- package/src/heatmap-data.ts +14 -0
- package/src/index.ts +4 -0
- package/src/instructions.ts +33 -0
- package/src/messages/GLOSSARY.md +88 -0
- package/src/messages/de.ask-ai.ts +28 -0
- package/src/messages/de.builder.ts +77 -0
- package/src/messages/de.server.ts +39 -0
- package/src/messages/de.sweep.ts +244 -0
- package/src/messages/en.ask-ai.ts +34 -0
- package/src/messages/en.builder.ts +76 -0
- package/src/messages/en.server.ts +39 -0
- package/src/messages/en.sweep.ts +240 -0
- package/src/messages/glossary.test.ts +122 -0
- package/src/messages/glossary.ts +62 -0
- package/src/messages/index.ts +24 -0
- package/src/messages/plural.ts +18 -0
- package/src/messages/use-t.ts +12 -0
- package/src/module.test.ts +319 -0
- package/src/module.ts +211 -0
- package/src/named-views.test.ts +141 -0
- package/src/named-views.ts +97 -0
- package/src/plugin.ts +94 -0
- package/src/prometheus-wiring.test.ts +174 -0
- package/src/server-locale.ts +59 -0
- package/src/settings-tools.ts +170 -0
- package/src/settings.test.ts +402 -0
- package/src/settings.ts +114 -0
- package/src/steps/app-config.ts +75 -0
- package/src/steps/dashboard.ts +73 -0
- package/src/steps/failure-dashboard.ts +38 -0
- package/src/steps/index.ts +2 -0
- package/src/steps/steps.test.ts +216 -0
- package/src/tool-names.ts +26 -0
- package/src/tools/cluster-compare.ts +28 -0
- package/src/tools/element.ts +35 -0
- package/src/tools/engine-compare.test.ts +210 -0
- package/src/tools/engine-compare.ts +30 -0
- package/src/tools/engine-landscape.test.ts +138 -0
- package/src/tools/engine-landscape.ts +21 -0
- package/src/tools/failures.ts +19 -0
- package/src/tools/health.test.ts +156 -0
- package/src/tools/health.ts +29 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/performance.test.ts +210 -0
- package/src/tools/performance.ts +38 -0
- package/src/tools/version-compare.test.ts +97 -0
- package/src/tools/version-compare.ts +28 -0
- package/src/toolsets.test.ts +107 -0
- package/src/toolsets.ts +56 -0
- package/src/view-title.test.ts +123 -0
- package/src/widget-tools/comparisons.test.ts +170 -0
- package/src/widget-tools/comparisons.ts +229 -0
- package/src/widget-tools/shared.ts +67 -0
- package/src/widget-tools.test.ts +306 -0
- package/src/widget-tools.ts +375 -0
- package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
- package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
- package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
- package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
- package/src/widgets/analytics-dashboard/lib.ts +43 -0
- package/src/widgets/bpmn-heatmap.test.tsx +75 -0
- package/src/widgets/bpmn-heatmap.tsx +127 -0
- package/src/widgets/catalogue-sync.test.ts +16 -0
- package/src/widgets/cluster-compare.tsx +148 -0
- package/src/widgets/comparison-shared.test.ts +237 -0
- package/src/widgets/comparison-shared.tsx +371 -0
- package/src/widgets/comparison-widgets.test.tsx +307 -0
- package/src/widgets/dashboard-widgets.test.tsx +397 -0
- package/src/widgets/engine-compare.tsx +87 -0
- package/src/widgets/engine-landscape.tsx +352 -0
- package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
- package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
- package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
- package/src/widgets/failure-dashboard/lib.ts +32 -0
- package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
- package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
- package/src/widgets/figure.tsx +79 -0
- package/src/widgets/hand-off.test.ts +126 -0
- package/src/widgets/hand-off.ts +102 -0
- package/src/widgets/index.ts +124 -0
- package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
- package/src/widgets/model-descriptions.test.ts +251 -0
- package/src/widgets/model-descriptions.ts +428 -0
- package/src/widgets/names.test.ts +32 -0
- package/src/widgets/names.ts +39 -0
- package/src/widgets/query-gate.test.tsx +57 -0
- package/src/widgets/query-gate.tsx +50 -0
- package/src/widgets/settings-section.test.tsx +174 -0
- package/src/widgets/settings-section.tsx +244 -0
- package/src/widgets/version-compare.test.ts +68 -0
- package/src/widgets/version-compare.tsx +96 -0
- package/src/widgets/view-meta.ts +18 -0
- package/src/widgets/widget-render.test-support.tsx +103 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest"
|
|
2
|
+
import type { PrometheusClient, VersionCompareResult } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { RegisteredToolMeta, ToolConfig } from "@miragon/mcp-toolkit-core/tools"
|
|
4
|
+
import { createEngineScope } from "../engine-ids.js"
|
|
5
|
+
import { registerVersionCompareTools } from "./version-compare.js"
|
|
6
|
+
|
|
7
|
+
type Config = ToolConfig<PrometheusClient>
|
|
8
|
+
|
|
9
|
+
/** Registrar stand-in that captures the tool's full config instead of an MCPServer. */
|
|
10
|
+
function captureConfig(): Config {
|
|
11
|
+
const configs: Config[] = []
|
|
12
|
+
const register = Object.assign((config: Config) => configs.push(config), {
|
|
13
|
+
getRegisteredTools: (): RegisteredToolMeta[] => [],
|
|
14
|
+
})
|
|
15
|
+
registerVersionCompareTools(register as never, createEngineScope(["prod-a"]))
|
|
16
|
+
expect(configs.map((c) => c.name)).toEqual(["analytics_version_compare"])
|
|
17
|
+
return configs[0]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** PrometheusClient that records every instant PromQL string and answers 20 per query. */
|
|
21
|
+
function recordingClient(): { client: PrometheusClient; queries: string[] } {
|
|
22
|
+
const queries: string[] = []
|
|
23
|
+
return {
|
|
24
|
+
client: {
|
|
25
|
+
instant: (query: string) => {
|
|
26
|
+
queries.push(query)
|
|
27
|
+
return Promise.resolve([{ metric: {}, value: 20 }])
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
queries,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = { processDefinitionKey: "order", versionA: 1, versionB: 2, windowDays: 14 }
|
|
35
|
+
|
|
36
|
+
describe("analytics_version_compare", () => {
|
|
37
|
+
// Captured per test (not at collection time), so a registration that
|
|
38
|
+
// registers nothing fails a test instead of the file's collection.
|
|
39
|
+
let config: Config
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
config = captureConfig()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("reads incidents per version, on the version label (#337)", async () => {
|
|
45
|
+
const { client, queries } = recordingClient()
|
|
46
|
+
const res = (await config.handler(client, { ...args })) as VersionCompareResult
|
|
47
|
+
|
|
48
|
+
const incidentQueries = queries.filter((q) => q.includes("camunda_incident_created_total"))
|
|
49
|
+
expect(incidentQueries).toHaveLength(2)
|
|
50
|
+
expect(incidentQueries.map((q) => /process_definition_version="(\d+)"/.exec(q)?.[1])).toEqual([
|
|
51
|
+
"1",
|
|
52
|
+
"2",
|
|
53
|
+
])
|
|
54
|
+
for (const kpi of res.kpis) {
|
|
55
|
+
expect(kpi).toMatchObject({
|
|
56
|
+
instance_count: 20,
|
|
57
|
+
incident_count: 20,
|
|
58
|
+
incident_rate_pct: 100,
|
|
59
|
+
element_incident_count: null,
|
|
60
|
+
element_incident_rate_pct: null,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
expect(res.delta.incident_rate_delta_pp).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("tells the model the incident figures are per version and the comparison process-wide", () => {
|
|
67
|
+
expect(config.description).toContain("process-instance and incident series")
|
|
68
|
+
expect(config.description).toContain("element_incident_* fields stay null")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("accepts the version-compare input plus the optional minBucketSize override", () => {
|
|
72
|
+
expect(Object.keys(config.inputSchema ?? {})).toEqual(
|
|
73
|
+
expect.arrayContaining(["processDefinitionKey", "versionA", "versionB", "minBucketSize"]),
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("registers as a read-only external read under the analytics category", () => {
|
|
78
|
+
expect(config.category).toBe("analytics")
|
|
79
|
+
expect(config.annotations).toEqual({
|
|
80
|
+
readOnlyHint: true,
|
|
81
|
+
idempotentHint: true,
|
|
82
|
+
openWorldHint: true,
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("prefers an explicit minBucketSize over the resolved setting", async () => {
|
|
87
|
+
const { client } = recordingClient()
|
|
88
|
+
const explicit = (await config.handler(client, {
|
|
89
|
+
...args,
|
|
90
|
+
minBucketSize: 3,
|
|
91
|
+
})) as VersionCompareResult
|
|
92
|
+
const fallback = (await config.handler(client, { ...args })) as VersionCompareResult
|
|
93
|
+
expect(explicit.minBucketSize).toBe(3)
|
|
94
|
+
// Without a store the registrar path lands on the schema default.
|
|
95
|
+
expect(fallback.minBucketSize).toBe(10)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
2
|
+
import { schemas, queries } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { createToolRegistrar } from "@miragon/mcp-toolkit-core/tools"
|
|
4
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
5
|
+
import { optionalMinBucketSize, settingsFor } from "../settings.js"
|
|
6
|
+
import { withEngineScope, type AnalyticsEngineScope } from "../engine-ids.js"
|
|
7
|
+
|
|
8
|
+
type Register = ReturnType<typeof createToolRegistrar<PrometheusClient>>
|
|
9
|
+
|
|
10
|
+
export function registerVersionCompareTools(
|
|
11
|
+
register: Register,
|
|
12
|
+
engineScope: AnalyticsEngineScope,
|
|
13
|
+
profileStore?: ProfileSource,
|
|
14
|
+
) {
|
|
15
|
+
register({
|
|
16
|
+
name: "analytics_version_compare",
|
|
17
|
+
category: "analytics",
|
|
18
|
+
description:
|
|
19
|
+
"Compare KPIs between two deployed process definition versions of the same processDefinitionKey over a shared rolling window, from metrics. The version is an exact label on the process-instance and incident series, so instance counts, completed counts, incidents, the incident rate and durations (avg, p95) per version plus their deltas are precise. Process-wide: the element_incident_* fields stay null. Flagged `suppressed` when either version has fewer than minBucketSize started or completed instances. Pair with camunda7_list_process_definitions to discover versions.",
|
|
20
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
21
|
+
inputSchema: { ...schemas.versionCompareInput.shape, minBucketSize: optionalMinBucketSize },
|
|
22
|
+
handler: async (ch, args, ctx) =>
|
|
23
|
+
queries.versionCompare(ch, {
|
|
24
|
+
...withEngineScope(engineScope, args),
|
|
25
|
+
minBucketSize: args.minBucketSize ?? (await settingsFor(profileStore, ctx)).minBucketSize,
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
allowsDurableWrites,
|
|
4
|
+
ANALYTICS_TOOLSETS,
|
|
5
|
+
analyticsToolsets,
|
|
6
|
+
isAnalyticsToolset,
|
|
7
|
+
} from "./toolsets.js"
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe("analyticsToolsets (the vocabulary the module declares)", () => {
|
|
14
|
+
it("declares exactly read-only and standard — analytics has no admin tier", () => {
|
|
15
|
+
expect(ANALYTICS_TOOLSETS).toEqual(["read-only", "standard"])
|
|
16
|
+
expect(analyticsToolsets.module).toBe("analytics")
|
|
17
|
+
expect(analyticsToolsets.names).toEqual(["read-only", "standard"])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("floors at read-only and defaults to standard under OAuth", () => {
|
|
21
|
+
expect(analyticsToolsets.fallback).toBe("read-only")
|
|
22
|
+
expect(analyticsToolsets.authenticatedDefault).toBe("standard")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("no suffix: read-only without OAuth, standard with it — never 'everything'", () => {
|
|
26
|
+
expect(analyticsToolsets.effective(undefined, { authenticated: false })).toEqual({
|
|
27
|
+
toolset: "read-only",
|
|
28
|
+
source: "default",
|
|
29
|
+
})
|
|
30
|
+
expect(analyticsToolsets.effective(undefined, { authenticated: true })).toEqual({
|
|
31
|
+
toolset: "standard",
|
|
32
|
+
source: "default",
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("a named suffix wins over the auth-dependent default, in both directions", () => {
|
|
37
|
+
expect(analyticsToolsets.effective("standard", { authenticated: false })).toEqual({
|
|
38
|
+
toolset: "standard",
|
|
39
|
+
source: "suffix",
|
|
40
|
+
})
|
|
41
|
+
expect(analyticsToolsets.effective("read-only", { authenticated: true })).toEqual({
|
|
42
|
+
toolset: "read-only",
|
|
43
|
+
source: "suffix",
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("an empty or unknown suffix (incl. camunda7's 'admin') falls back to the floor, with a warning", () => {
|
|
48
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
49
|
+
for (const suffix of ["", "admin"]) {
|
|
50
|
+
expect(analyticsToolsets.effective(suffix, { authenticated: true })).toEqual({
|
|
51
|
+
toolset: "read-only",
|
|
52
|
+
source: "fallback",
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
expect(warn).toHaveBeenCalledTimes(2)
|
|
56
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[analytics]"))
|
|
57
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Unknown toolset "admin"'))
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("isAnalyticsToolset", () => {
|
|
62
|
+
it("accepts every declared name and nothing else", () => {
|
|
63
|
+
for (const toolset of ANALYTICS_TOOLSETS) {
|
|
64
|
+
expect(isAnalyticsToolset(toolset)).toBe(true)
|
|
65
|
+
}
|
|
66
|
+
expect(isAnalyticsToolset("admin")).toBe(false)
|
|
67
|
+
expect(isAnalyticsToolset("")).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("allowsDurableWrites", () => {
|
|
72
|
+
it("forbids writes when no toolset is configured — a missing toolset is the read-only floor", () => {
|
|
73
|
+
// Fail-closed: a direct `createPlugin` caller that passes no toolset gets
|
|
74
|
+
// the floor, never the full surface.
|
|
75
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
76
|
+
expect(allowsDurableWrites(undefined)).toBe(false)
|
|
77
|
+
expect(allowsDurableWrites()).toBe(false)
|
|
78
|
+
expect(warn).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("forbids writes in the module's read-only toolset", () => {
|
|
82
|
+
expect(allowsDurableWrites("read-only")).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("permits writes in the standard toolset (the user's own settings save)", () => {
|
|
86
|
+
expect(allowsDurableWrites("standard")).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("fails CLOSED on an unknown toolset name, with a warning", () => {
|
|
90
|
+
// A typo'd `analytics:standrad`, or camunda7's `operations`, must not
|
|
91
|
+
// grant the one write — unknown names degrade to `read-only`.
|
|
92
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
93
|
+
expect(allowsDurableWrites("operations")).toBe(false)
|
|
94
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
95
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Unknown toolset "operations"'))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("decides by declared name, not by an ad-hoc string compare", () => {
|
|
99
|
+
// The regression this guards: a `toolset === "read-only"` check silently
|
|
100
|
+
// fails open for every name that is NOT that literal. Every declared
|
|
101
|
+
// toolset is a deliberate decision: only the floor forbids the write.
|
|
102
|
+
expect(ANALYTICS_TOOLSETS.map((toolset) => [toolset, allowsDurableWrites(toolset)])).toEqual([
|
|
103
|
+
["read-only", false],
|
|
104
|
+
["standard", true],
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
})
|
package/src/toolsets.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The analytics module's toolset vocabulary. Module peers own their toolset
|
|
3
|
+
* NAMES; the fail-closed SEMANTICS are shared (`createToolsetVocabulary` from
|
|
4
|
+
* `@miragon/pulse-widget-shell/server`):
|
|
5
|
+
*
|
|
6
|
+
* - `read-only` — the floor: every analytics read, no durable write (the
|
|
7
|
+
* settings save tool is not registered);
|
|
8
|
+
* - `standard` — the full analytics surface, including the caller's own
|
|
9
|
+
* settings save. Analytics has no admin tier: it reads Prometheus and never
|
|
10
|
+
* writes to an engine, so there is nothing wider to name.
|
|
11
|
+
*
|
|
12
|
+
* Which one a boot runs with (`MCP_ACTIVE_MODULES=analytics:<toolset>`):
|
|
13
|
+
*
|
|
14
|
+
* - no suffix → `read-only` on an unauthenticated boot, `standard` when the
|
|
15
|
+
* root installed OAuth;
|
|
16
|
+
* - an empty or unknown suffix → warns and degrades to `read-only` — a suffix
|
|
17
|
+
* always meant to restrict, so a typo never grants the write it tried to
|
|
18
|
+
* take away;
|
|
19
|
+
* - nothing ever resolves to "everything".
|
|
20
|
+
*
|
|
21
|
+
* The composition root resolves the suffix once per boot and hands the module
|
|
22
|
+
* a concrete name; a direct `createPlugin` caller that passes none gets the
|
|
23
|
+
* floor.
|
|
24
|
+
*/
|
|
25
|
+
import { createToolsetVocabulary } from "@miragon/pulse-widget-shell/server"
|
|
26
|
+
|
|
27
|
+
export const ANALYTICS_TOOLSETS = ["read-only", "standard"] as const
|
|
28
|
+
export type AnalyticsToolset = (typeof ANALYTICS_TOOLSETS)[number]
|
|
29
|
+
|
|
30
|
+
/** The vocabulary the module declares to the composition root (`analyticsModule.toolsets`). */
|
|
31
|
+
export const analyticsToolsets = createToolsetVocabulary(
|
|
32
|
+
"analytics",
|
|
33
|
+
ANALYTICS_TOOLSETS,
|
|
34
|
+
"read-only",
|
|
35
|
+
{
|
|
36
|
+
authenticatedDefault: "standard",
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export function isAnalyticsToolset(value: string): value is AnalyticsToolset {
|
|
41
|
+
return analyticsToolsets.isKnown(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the toolset permits the module's durable writes
|
|
46
|
+
* (`analytics_save_settings`). Registered outside the tool registrar, that save
|
|
47
|
+
* has to gate itself — no registrar filter ever sees it.
|
|
48
|
+
*
|
|
49
|
+
* Decided by the vocabulary, never by an ad-hoc `toolset === "read-only"`
|
|
50
|
+
* compare (which fails open for every other name): a missing toolset resolves
|
|
51
|
+
* to the read-only floor, an unknown one warns and degrades to it, and only a
|
|
52
|
+
* toolset above the floor (`standard`) writes.
|
|
53
|
+
*/
|
|
54
|
+
export function allowsDurableWrites(toolset?: string): boolean {
|
|
55
|
+
return analyticsToolsets.allowsDurableWrites(analyticsToolsets.resolve(toolset))
|
|
56
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import type { MCPServer } from "mcp-use"
|
|
3
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
4
|
+
import { createInMemoryProfileStore } from "@miragon/pulse-widget-shell/server"
|
|
5
|
+
import { createEngineScope } from "./engine-ids.js"
|
|
6
|
+
import { registerSettingsTools } from "./settings-tools.js"
|
|
7
|
+
import { registerWidgetTools } from "./widget-tools.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Structural guard for #322 U3 over EVERY analytics show tool (each tool with
|
|
11
|
+
* a view binding), the same rule camunda7's show tools follow (the kit's
|
|
12
|
+
* `createLocalizeViewFor`): the server titles a view only in a language the
|
|
13
|
+
* caller's profile names. "system" follows the host's locale, which the
|
|
14
|
+
* server never sees, so the view carries no title and the widget's own
|
|
15
|
+
* heading names it; an English title would sit over a German view. A new
|
|
16
|
+
* show tool that titles with `t(…)` instead of `title(…)` fails here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type ToolCallback = (args: Record<string, unknown>, ctx?: unknown) => Promise<unknown>
|
|
20
|
+
type ViewResult = {
|
|
21
|
+
isError?: boolean
|
|
22
|
+
content?: unknown
|
|
23
|
+
structuredContent?: { title?: unknown; layout?: unknown }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Prometheus without samples: every view renders its empty state. */
|
|
27
|
+
const prometheus: PrometheusClient = { instant: () => Promise.resolve([]) }
|
|
28
|
+
|
|
29
|
+
/** The required arguments per show tool; the tool's own input schema fills in its defaults. */
|
|
30
|
+
const ARGS: Record<string, Record<string, unknown>> = {
|
|
31
|
+
analytics_show_bpmn_heatmap: { processDefinitionKey: "order" },
|
|
32
|
+
// Inside Prometheus' retention, with a post-deployment window to measure.
|
|
33
|
+
analytics_show_cluster_compare: {
|
|
34
|
+
deploymentTimestamp: new Date(Date.now() - 10 * 86_400_000).toISOString(),
|
|
35
|
+
},
|
|
36
|
+
analytics_show_version_compare: { processDefinitionKey: "order", versionA: 1, versionB: 2 },
|
|
37
|
+
analytics_show_engine_compare: {
|
|
38
|
+
processDefinitionKey: "order",
|
|
39
|
+
engineA: "prod-a",
|
|
40
|
+
engineB: "prod-b",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const callerCtx = (id: string) => ({ auth: { user: { id } } })
|
|
45
|
+
|
|
46
|
+
interface ToolDefinition {
|
|
47
|
+
name: string
|
|
48
|
+
view?: unknown
|
|
49
|
+
inputSchema: { parse: (args: unknown) => Record<string, unknown> }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The show tools, each called the way the server does: arguments parsed by its input schema. */
|
|
53
|
+
async function showTools() {
|
|
54
|
+
const tools = new Map<string, ToolCallback>()
|
|
55
|
+
const server = {
|
|
56
|
+
tool: (definition: ToolDefinition, callback: ToolCallback) => {
|
|
57
|
+
if (definition.view) {
|
|
58
|
+
tools.set(definition.name, (args, ctx) => callback(definition.inputSchema.parse(args), ctx))
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
} as unknown as MCPServer
|
|
62
|
+
const profileStore = createInMemoryProfileStore()
|
|
63
|
+
await profileStore.save("de-user", { language: "de" })
|
|
64
|
+
await profileStore.save("en-user", { language: "en" })
|
|
65
|
+
await profileStore.save("system-user", { language: "system" })
|
|
66
|
+
registerWidgetTools(server, prometheus, {
|
|
67
|
+
engineScope: createEngineScope(["prod-a", "prod-b"]),
|
|
68
|
+
profileStore,
|
|
69
|
+
})
|
|
70
|
+
registerSettingsTools(server, profileStore, "standard")
|
|
71
|
+
return tools
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Each show tool's view title for one caller; every show tool must render its view. */
|
|
75
|
+
async function titlesFor(tools: Map<string, ToolCallback>, ctx: unknown) {
|
|
76
|
+
const titles = new Map<string, unknown>()
|
|
77
|
+
for (const [name, call] of tools) {
|
|
78
|
+
const result = (await call(ARGS[name] ?? {}, ctx)) as ViewResult
|
|
79
|
+
expect(result.isError, `${name}: ${JSON.stringify(result.content)}`).not.toBe(true)
|
|
80
|
+
expect(result.structuredContent?.layout, name).toBeTruthy()
|
|
81
|
+
titles.set(name, result.structuredContent?.title)
|
|
82
|
+
}
|
|
83
|
+
return titles
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("analytics show tools title their view only in a language the profile names", () => {
|
|
87
|
+
it("covers every analytics show tool (the sweep is not vacuous)", async () => {
|
|
88
|
+
expect([...(await showTools()).keys()].sort()).toEqual([
|
|
89
|
+
"analytics_show_bpmn_heatmap",
|
|
90
|
+
"analytics_show_cluster_compare",
|
|
91
|
+
"analytics_show_dashboard",
|
|
92
|
+
"analytics_show_engine_compare",
|
|
93
|
+
"analytics_show_engine_landscape",
|
|
94
|
+
"analytics_show_failure_dashboard",
|
|
95
|
+
"analytics_show_settings",
|
|
96
|
+
"analytics_show_version_compare",
|
|
97
|
+
])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("no title for an anonymous caller or one whose profile follows the host", async () => {
|
|
101
|
+
const tools = await showTools()
|
|
102
|
+
// `{}`: a handler ctx without a caller identity (no OAuth).
|
|
103
|
+
for (const ctx of [{}, callerCtx("system-user"), callerCtx("nobody-saved-yet")]) {
|
|
104
|
+
const titles = await titlesFor(tools, ctx)
|
|
105
|
+
expect(Object.fromEntries([...titles].filter(([, title]) => title !== undefined))).toEqual({})
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("a named language titles every view, each in that language", async () => {
|
|
110
|
+
const tools = await showTools()
|
|
111
|
+
const de = await titlesFor(tools, callerCtx("de-user"))
|
|
112
|
+
const en = await titlesFor(tools, callerCtx("en-user"))
|
|
113
|
+
for (const [name] of tools) {
|
|
114
|
+
expect(de.get(name), name).toEqual(expect.any(String))
|
|
115
|
+
expect(en.get(name), name).toEqual(expect.any(String))
|
|
116
|
+
expect(de.get(name), name).not.toBe(en.get(name))
|
|
117
|
+
}
|
|
118
|
+
expect(de.get("analytics_show_settings")).toBe("Analyse-Einstellungen")
|
|
119
|
+
expect(en.get("analytics_show_settings")).toBe("Analytics settings")
|
|
120
|
+
expect(de.get("analytics_show_dashboard")).toBe("Prozessanalyse")
|
|
121
|
+
expect(en.get("analytics_show_dashboard")).toBe("Process analytics")
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
|
|
2
|
+
import type { MCPServer } from "mcp-use"
|
|
3
|
+
import type {
|
|
4
|
+
ClusterCompareResult,
|
|
5
|
+
PrometheusClient,
|
|
6
|
+
VersionCompareResult,
|
|
7
|
+
} from "@miragon/pulse-analytics-client"
|
|
8
|
+
import { createEngineScope } from "../engine-ids.js"
|
|
9
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
10
|
+
import { registerComparisonWidgetTools } from "./comparisons.js"
|
|
11
|
+
|
|
12
|
+
type ToolResult = {
|
|
13
|
+
isError?: boolean
|
|
14
|
+
content: Array<{ type: string; text: string }>
|
|
15
|
+
structuredContent: { context: { stepData: { result: { data: unknown } } } }
|
|
16
|
+
}
|
|
17
|
+
type Handler = (args: Record<string, unknown>, ctx: unknown) => Promise<ToolResult>
|
|
18
|
+
|
|
19
|
+
const engineScope = createEngineScope(["prod-a", "prod-b"])
|
|
20
|
+
|
|
21
|
+
/** Registers the comparison widget tools on a stand-in server and returns their handlers. */
|
|
22
|
+
function handlers(ch: PrometheusClient): Map<string, Handler> {
|
|
23
|
+
const byName = new Map<string, Handler>()
|
|
24
|
+
const server = {
|
|
25
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
26
|
+
byName.set(definition.name, handler)
|
|
27
|
+
},
|
|
28
|
+
} as unknown as MCPServer
|
|
29
|
+
registerComparisonWidgetTools({ server, ch, engineScope })
|
|
30
|
+
return byName
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dataOf = <T>(result: ToolResult) => result.structuredContent.context.stepData.result.data as T
|
|
34
|
+
|
|
35
|
+
/** Every query answers 20, so instance counts and durations are equal across versions. */
|
|
36
|
+
const ch: PrometheusClient = { instant: () => Promise.resolve([{ metric: {}, value: 20 }]) }
|
|
37
|
+
|
|
38
|
+
describe("analytics_show_version_compare", () => {
|
|
39
|
+
it("summarises the measured incident-rate delta per version (#337)", async () => {
|
|
40
|
+
const show = handlers(ch).get("analytics_show_version_compare")!
|
|
41
|
+
const result = await show(
|
|
42
|
+
{ processDefinitionKey: "order", versionA: 1, versionB: 2, windowDays: 14 },
|
|
43
|
+
{},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(result.content[0].text).toContain("incident rate 0pp")
|
|
47
|
+
const data = dataOf<VersionCompareResult>(result)
|
|
48
|
+
expect(data.kpis.map((k) => k.incident_rate_pct)).toEqual([100, 100])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("names the engines a fleet comparison aggregates (K33)", async () => {
|
|
52
|
+
const show = handlers(ch).get("analytics_show_version_compare")!
|
|
53
|
+
const result = await show(
|
|
54
|
+
{ processDefinitionKey: "order", versionA: 1, versionB: 2, windowDays: 14 },
|
|
55
|
+
{},
|
|
56
|
+
)
|
|
57
|
+
expect(result.content[0].text).toContain(
|
|
58
|
+
'across all 2 configured engines ("prod-a", "prod-b"), aggregated',
|
|
59
|
+
)
|
|
60
|
+
expect(dataOf<VersionCompareResult>(result).engines).toEqual(["prod-a", "prod-b"])
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe("analytics_show_cluster_compare — clamped windows (N79)", () => {
|
|
65
|
+
const NOW = Date.parse("2026-10-09T12:00:00Z")
|
|
66
|
+
beforeAll(() => {
|
|
67
|
+
vi.useFakeTimers({ toFake: ["Date"], now: NOW })
|
|
68
|
+
})
|
|
69
|
+
afterAll(() => {
|
|
70
|
+
vi.useRealTimers()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("measures a recent deployment's post window up to now and says so", async () => {
|
|
74
|
+
const sent: string[] = []
|
|
75
|
+
const recording: PrometheusClient = {
|
|
76
|
+
instant: (q) => {
|
|
77
|
+
sent.push(q)
|
|
78
|
+
return Promise.resolve([{ metric: {}, value: 20 }])
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
const deployedTwoHoursAgo = new Date(NOW - 2 * 3_600_000).toISOString()
|
|
82
|
+
const result = await handlers(recording).get("analytics_show_cluster_compare")!(
|
|
83
|
+
{ deploymentTimestamp: deployedTwoHoursAgo, windowBeforeDays: 7, windowAfterDays: 7 },
|
|
84
|
+
{},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// No `@` anchor beyond now — the post window is 2 h, not 7 d.
|
|
88
|
+
const anchors = sent.flatMap((q) => [...q.matchAll(/@ (\d+)/g)].map((m) => Number(m[1])))
|
|
89
|
+
expect(Math.max(...anchors)).toBe(NOW / 1000)
|
|
90
|
+
const data = dataOf<ClusterCompareResult>(result)
|
|
91
|
+
expect(data).toMatchObject({
|
|
92
|
+
partial: true,
|
|
93
|
+
requestedWindowDays: { before: 7, after: 7 },
|
|
94
|
+
windowDays: { before: 7, after: 0.08 },
|
|
95
|
+
})
|
|
96
|
+
expect(result.content[0].text).toContain("measured -7d / +0.08d, partial")
|
|
97
|
+
// 20 starts in 2 h vs 20 in 7 d is a huge rise PER DAY — never a flat 0 %.
|
|
98
|
+
expect(data.delta.started_per_day_delta_pct).toBe(8300)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("refuses a deployment in the future instead of comparing an empty window", async () => {
|
|
102
|
+
const result = await handlers(ch).get("analytics_show_cluster_compare")!(
|
|
103
|
+
{ deploymentTimestamp: new Date(NOW + 3_600_000).toISOString() },
|
|
104
|
+
{},
|
|
105
|
+
)
|
|
106
|
+
expect(result.isError).toBe(true)
|
|
107
|
+
expect(result.content[0].text).toContain("deploymentTimestamp lies in the future")
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("analytics_show_engine_compare — engine scope (N138)", () => {
|
|
112
|
+
it("refuses an engine outside the configured ones", async () => {
|
|
113
|
+
const result = await handlers(ch).get("analytics_show_engine_compare")!(
|
|
114
|
+
{ processDefinitionKey: "order", engineA: "prod-a", engineB: "tenant-x" },
|
|
115
|
+
{},
|
|
116
|
+
)
|
|
117
|
+
expect(result.isError).toBe(true)
|
|
118
|
+
expect(result.content[0].text).toContain('Unknown engine "tenant-x" in engineB')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe("view titles (U3): the toolbar speaks the caller's language", () => {
|
|
123
|
+
const CTX = { auth: { user: { id: "user-1" } } }
|
|
124
|
+
const titledHandlers = (language: string) => {
|
|
125
|
+
const byName = new Map<string, Handler>()
|
|
126
|
+
const server = {
|
|
127
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
128
|
+
byName.set(definition.name, handler)
|
|
129
|
+
},
|
|
130
|
+
} as unknown as MCPServer
|
|
131
|
+
const profileStore: ProfileSource = { get: () => Promise.resolve({ language }) }
|
|
132
|
+
registerComparisonWidgetTools({ server, ch, engineScope, profileStore })
|
|
133
|
+
return byName
|
|
134
|
+
}
|
|
135
|
+
const titleOf = (result: ToolResult) =>
|
|
136
|
+
(result.structuredContent as unknown as { title?: string }).title
|
|
137
|
+
|
|
138
|
+
const CALLS: Array<[string, Record<string, unknown>]> = [
|
|
139
|
+
// Handlers called directly get no schema defaults: pass the window.
|
|
140
|
+
[
|
|
141
|
+
"analytics_show_version_compare",
|
|
142
|
+
{ processDefinitionKey: "o", versionA: 1, versionB: 2, windowDays: 14 },
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
"analytics_show_engine_compare",
|
|
146
|
+
{ processDefinitionKey: "o", engineA: "prod-a", engineB: "prod-b", windowDays: 14 },
|
|
147
|
+
],
|
|
148
|
+
["analytics_show_engine_landscape", {}],
|
|
149
|
+
]
|
|
150
|
+
const titles = async (language: string) => {
|
|
151
|
+
const tools = titledHandlers(language)
|
|
152
|
+
return Promise.all(CALLS.map(([name, args]) => tools.get(name)!(args, CTX).then(titleOf)))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
it("titles each comparison and the landscape in German", async () => {
|
|
156
|
+
expect(await titles("de")).toEqual([
|
|
157
|
+
"Versionsvergleich",
|
|
158
|
+
"Engine-Vergleich",
|
|
159
|
+
"Engine-übergreifende Übersicht",
|
|
160
|
+
])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("and in English, in sentence case", async () => {
|
|
164
|
+
expect(await titles("en")).toEqual([
|
|
165
|
+
"Version comparison",
|
|
166
|
+
"Engine comparison",
|
|
167
|
+
"Cross-engine landscape",
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
})
|