decision_agent 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,225 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/monitoring/metrics_collector"
3
- require "decision_agent/monitoring/monitored_agent"
4
-
5
- RSpec.describe DecisionAgent::Monitoring::MonitoredAgent do
6
- let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(storage: :memory) }
7
- let(:evaluator) do
8
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(
9
- rules_json: {
10
- version: "1.0",
11
- ruleset: "test",
12
- rules: [{
13
- id: "test_rule",
14
- if: { field: "amount", op: "gt", value: 100 },
15
- then: { decision: "approve", weight: 0.9, reason: "Test reason" }
16
- }]
17
- },
18
- name: "test_evaluator"
19
- )
20
- end
21
- let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator]) }
22
- let(:monitored_agent) { described_class.new(agent: agent, metrics_collector: collector) }
23
-
24
- describe "#initialize" do
25
- it "wraps an agent with metrics collection" do
26
- expect(monitored_agent.agent).to eq(agent)
27
- expect(monitored_agent.metrics_collector).to eq(collector)
28
- end
29
- end
30
-
31
- describe "#decide" do
32
- let(:context) { { amount: 1000 } }
33
-
34
- it "makes a decision and records metrics" do
35
- result = monitored_agent.decide(context: context)
36
-
37
- expect(result).to be_a(DecisionAgent::Decision)
38
- expect(result.decision).to eq("approve")
39
- expect(collector.metrics_count[:decisions]).to eq(1)
40
- expect(collector.metrics_count[:evaluations]).to eq(1)
41
- expect(collector.metrics_count[:performance]).to eq(1)
42
- end
43
-
44
- it "records decision metrics with duration" do
45
- monitored_agent.decide(context: context)
46
-
47
- stats = collector.statistics
48
- expect(stats[:decisions][:total]).to eq(1)
49
- expect(stats[:decisions][:avg_duration_ms]).to be > 0
50
- end
51
-
52
- it "records evaluation metrics" do
53
- monitored_agent.decide(context: context)
54
-
55
- stats = collector.statistics
56
- expect(stats[:evaluations][:total]).to eq(1)
57
- expect(stats[:evaluations][:evaluator_distribution]["test_evaluator"]).to eq(1)
58
- end
59
-
60
- it "records performance metrics as successful" do
61
- monitored_agent.decide(context: context)
62
-
63
- stats = collector.statistics
64
- expect(stats[:performance][:total_operations]).to eq(1)
65
- expect(stats[:performance][:successful]).to eq(1)
66
- expect(stats[:performance][:success_rate]).to eq(1.0)
67
- end
68
-
69
- it "includes metadata in performance metrics" do
70
- monitored_agent.decide(context: context)
71
-
72
- collector.statistics
73
- perf_metric = collector.instance_variable_get(:@metrics)[:performance].first
74
-
75
- expect(perf_metric[:metadata][:evaluators_count]).to eq(1)
76
- expect(perf_metric[:metadata][:decision]).to eq("approve")
77
- expect(perf_metric[:metadata][:confidence]).to be_a(Float)
78
- end
79
-
80
- context "when decision fails" do
81
- before do
82
- allow(agent).to receive(:decide).and_raise(StandardError.new("Test error"))
83
- end
84
-
85
- it "records error metrics" do
86
- expect do
87
- monitored_agent.decide(context: context)
88
- end.to raise_error(StandardError, "Test error")
89
-
90
- expect(collector.metrics_count[:errors]).to eq(1)
91
- end
92
-
93
- it "records failed performance metrics" do
94
- expect do
95
- monitored_agent.decide(context: context)
96
- end.to raise_error(StandardError)
97
-
98
- stats = collector.statistics
99
- expect(stats[:performance][:total_operations]).to eq(1)
100
- expect(stats[:performance][:failed]).to eq(1)
101
- expect(stats[:performance][:success_rate]).to eq(0.0)
102
- end
103
-
104
- it "includes error details in metrics" do
105
- expect do
106
- monitored_agent.decide(context: context)
107
- end.to raise_error(StandardError)
108
-
109
- error_metric = collector.instance_variable_get(:@metrics)[:errors].first
110
- expect(error_metric[:error_class]).to eq("StandardError")
111
- expect(error_metric[:error_message]).to eq("Test error")
112
- expect(error_metric[:context]).to eq(context)
113
- end
114
-
115
- it "re-raises the error" do
116
- expect do
117
- monitored_agent.decide(context: context)
118
- end.to raise_error(StandardError, "Test error")
119
- end
120
- end
121
-
122
- it "handles Context objects" do
123
- ctx = DecisionAgent::Context.new(context)
124
- result = monitored_agent.decide(context: ctx)
125
-
126
- expect(result).to be_a(DecisionAgent::Decision)
127
- expect(collector.metrics_count[:decisions]).to eq(1)
128
- end
129
-
130
- it "handles hash contexts" do
131
- result = monitored_agent.decide(context: context)
132
-
133
- expect(result).to be_a(DecisionAgent::Decision)
134
- expect(collector.metrics_count[:decisions]).to eq(1)
135
- end
136
-
137
- it "measures decision duration accurately" do
138
- # Mock agent to introduce delay
139
- allow(agent).to receive(:decide) do |context:, **_kwargs|
140
- sleep 0.01 # 10ms delay
141
- evaluation = evaluator.evaluate(context)
142
- DecisionAgent::Decision.new(
143
- decision: "approve",
144
- confidence: 0.9,
145
- explanations: ["Test"],
146
- evaluations: [evaluation].compact, # Remove nils in case evaluation returns nil
147
- audit_payload: {}
148
- )
149
- end
150
-
151
- monitored_agent.decide(context: context)
152
-
153
- stats = collector.statistics
154
- expect(stats[:decisions][:avg_duration_ms]).to be >= 10
155
- end
156
- end
157
-
158
- describe "method delegation" do
159
- it "delegates methods to wrapped agent" do
160
- expect(monitored_agent.evaluators).to eq(agent.evaluators)
161
- expect(monitored_agent.scoring_strategy).to eq(agent.scoring_strategy)
162
- expect(monitored_agent.audit_adapter).to eq(agent.audit_adapter)
163
- end
164
-
165
- it "responds to agent methods" do
166
- expect(monitored_agent).to respond_to(:evaluators)
167
- expect(monitored_agent).to respond_to(:scoring_strategy)
168
- expect(monitored_agent).to respond_to(:audit_adapter)
169
- end
170
- end
171
-
172
- describe "thread safety" do
173
- it "handles concurrent decisions safely" do
174
- # Materialize let variables before creating threads
175
- test_context = { amount: 1000 }
176
- test_monitored_agent = monitored_agent
177
- test_collector = collector
178
-
179
- threads = 10.times.map do
180
- Thread.new do
181
- 10.times do
182
- test_monitored_agent.decide(context: test_context)
183
- end
184
- end
185
- end
186
-
187
- threads.each(&:join)
188
-
189
- expect(test_collector.metrics_count[:decisions]).to eq(100)
190
- expect(test_collector.metrics_count[:evaluations]).to eq(100)
191
- expect(test_collector.metrics_count[:performance]).to eq(100)
192
- end
193
- end
194
-
195
- describe "integration test" do
196
- it "provides comprehensive metrics for multiple decisions" do
197
- contexts = [
198
- { amount: 500 },
199
- { amount: 1500 },
200
- { amount: 2000 }
201
- ]
202
-
203
- contexts.each do |ctx|
204
- monitored_agent.decide(context: ctx)
205
- end
206
-
207
- stats = collector.statistics
208
-
209
- # Summary
210
- expect(stats[:summary][:total_decisions]).to eq(3)
211
- expect(stats[:summary][:total_evaluations]).to eq(3)
212
- expect(stats[:summary][:total_errors]).to eq(0)
213
-
214
- # Decision stats
215
- expect(stats[:decisions][:total]).to eq(3)
216
- expect(stats[:decisions][:avg_confidence]).to be > 0
217
- expect(stats[:decisions][:decision_distribution]["approve"]).to eq(3)
218
-
219
- # Performance stats
220
- expect(stats[:performance][:total_operations]).to eq(3)
221
- expect(stats[:performance][:success_rate]).to eq(1.0)
222
- expect(stats[:performance][:avg_duration_ms]).to be > 0
223
- end
224
- end
225
- end
@@ -1,242 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/monitoring/metrics_collector"
3
- require "decision_agent/monitoring/prometheus_exporter"
4
-
5
- RSpec.describe DecisionAgent::Monitoring::PrometheusExporter do
6
- let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(storage: :memory) }
7
- let(:exporter) { described_class.new(metrics_collector: collector, namespace: "test") }
8
-
9
- let(:decision) do
10
- double(
11
- "Decision",
12
- decision: "approve",
13
- confidence: 0.85,
14
- evaluations: [double("Evaluation", evaluator_name: "test_evaluator")]
15
- )
16
- end
17
- let(:context) { double("Context", to_h: { user: "test" }) }
18
-
19
- describe "#initialize" do
20
- it "initializes with metrics collector" do
21
- expect(exporter).to be_a(described_class)
22
- end
23
-
24
- it "uses default namespace" do
25
- exporter = described_class.new(metrics_collector: collector)
26
- output = exporter.export
27
- expect(output).to include("decision_agent_")
28
- end
29
-
30
- it "uses custom namespace" do
31
- output = exporter.export
32
- expect(output).to include("test_")
33
- end
34
- end
35
-
36
- describe "#export" do
37
- before do
38
- # Record some metrics
39
- 3.times { collector.record_decision(decision, context, duration_ms: 10.0) }
40
- collector.record_performance(operation: "decide", duration_ms: 15.0, success: true)
41
- collector.record_error(StandardError.new("Test error"))
42
- end
43
-
44
- it "exports in Prometheus text format" do
45
- output = exporter.export
46
-
47
- expect(output).to be_a(String)
48
- expect(output).to include("# DecisionAgent Metrics Export")
49
- end
50
-
51
- it "includes decision metrics" do
52
- output = exporter.export
53
-
54
- expect(output).to include("# HELP test_decisions_total")
55
- expect(output).to include("# TYPE test_decisions_total counter")
56
- expect(output).to include("test_decisions_total 3")
57
- end
58
-
59
- it "includes confidence metrics" do
60
- output = exporter.export
61
-
62
- expect(output).to include("# HELP test_decision_confidence_avg")
63
- expect(output).to include("# TYPE test_decision_confidence_avg gauge")
64
- expect(output).to include("test_decision_confidence_avg 0.85")
65
- end
66
-
67
- it "includes performance metrics" do
68
- output = exporter.export
69
-
70
- expect(output).to include("# HELP test_success_rate")
71
- expect(output).to include("# TYPE test_success_rate gauge")
72
- end
73
-
74
- it "includes error metrics" do
75
- output = exporter.export
76
-
77
- expect(output).to include("# HELP test_errors_total")
78
- expect(output).to include("# TYPE test_errors_total counter")
79
- expect(output).to include("test_errors_total 1")
80
- end
81
-
82
- it "includes system info" do
83
- output = exporter.export
84
-
85
- expect(output).to include("# HELP test_info")
86
- expect(output).to include("# TYPE test_info gauge")
87
- expect(output).to include("version=\"#{DecisionAgent::VERSION}\"")
88
- end
89
-
90
- it "includes decision distribution" do
91
- output = exporter.export
92
-
93
- expect(output).to include("# HELP test_decisions_by_type")
94
- expect(output).to include("test_decisions_by_type{decision=\"approve\"} 3")
95
- end
96
-
97
- it "includes error distribution by type" do
98
- output = exporter.export
99
-
100
- expect(output).to include("# HELP test_errors_by_type")
101
- expect(output).to include("test_errors_by_type{error=\"StandardError\"} 1")
102
- end
103
-
104
- it "includes metrics count" do
105
- output = exporter.export
106
-
107
- expect(output).to include("# HELP test_metrics_stored")
108
- expect(output).to include("test_metrics_stored{type=\"decisions\"} 3")
109
- expect(output).to include("test_metrics_stored{type=\"errors\"} 1")
110
- end
111
- end
112
-
113
- describe "#register_kpi" do
114
- it "registers a custom KPI" do
115
- exporter.register_kpi(
116
- name: "custom_metric",
117
- value: 42.5,
118
- help: "A custom metric"
119
- )
120
-
121
- output = exporter.export
122
- expect(output).to include("# HELP test_custom_metric A custom metric")
123
- expect(output).to include("# TYPE test_custom_metric gauge")
124
- expect(output).to include("test_custom_metric 42.5")
125
- end
126
-
127
- it "registers KPI with labels" do
128
- exporter.register_kpi(
129
- name: "requests",
130
- value: 100,
131
- labels: { endpoint: "/api/v1", method: "GET" }
132
- )
133
-
134
- output = exporter.export
135
- expect(output).to include("test_requests{endpoint=\"/api/v1\",method=\"GET\"} 100")
136
- end
137
-
138
- it "sanitizes metric names" do
139
- exporter.register_kpi(name: "my-custom.metric!", value: 10)
140
-
141
- output = exporter.export
142
- expect(output).to include("test_my_custom_metric_")
143
- end
144
-
145
- it "escapes label values" do
146
- exporter.register_kpi(
147
- name: "metric",
148
- value: 1,
149
- labels: { message: 'Contains "quotes"' }
150
- )
151
-
152
- output = exporter.export
153
- expect(output).to include('message="Contains \"quotes\""')
154
- end
155
- end
156
-
157
- describe "#metrics_hash" do
158
- before do
159
- collector.record_decision(decision, context, duration_ms: 10.0)
160
- end
161
-
162
- it "returns metrics as hash" do
163
- metrics = exporter.metrics_hash
164
-
165
- expect(metrics).to be_a(Hash)
166
- expect(metrics).to have_key(:decisions)
167
- expect(metrics).to have_key(:performance)
168
- expect(metrics).to have_key(:errors)
169
- expect(metrics).to have_key(:system)
170
- end
171
-
172
- it "includes metric types" do
173
- metrics = exporter.metrics_hash
174
-
175
- expect(metrics[:decisions][:total][:type]).to eq("counter")
176
- expect(metrics[:decisions][:avg_confidence][:type]).to eq("gauge")
177
- end
178
-
179
- it "includes metric values" do
180
- metrics = exporter.metrics_hash
181
-
182
- expect(metrics[:decisions][:total][:value]).to eq(1)
183
- expect(metrics[:decisions][:avg_confidence][:value]).to eq(0.85)
184
- end
185
- end
186
-
187
- describe "thread safety" do
188
- it "handles concurrent KPI registration" do
189
- threads = 10.times.map do |i|
190
- Thread.new do
191
- 10.times do |j|
192
- exporter.register_kpi(
193
- name: "metric_#{i}_#{j}",
194
- value: (i * 10) + j
195
- )
196
- end
197
- end
198
- end
199
-
200
- expect { threads.each(&:join) }.not_to raise_error
201
- end
202
-
203
- it "handles concurrent exports" do
204
- threads = 5.times.map do
205
- Thread.new do
206
- 10.times { exporter.export }
207
- end
208
- end
209
-
210
- expect { threads.each(&:join) }.not_to raise_error
211
- end
212
- end
213
-
214
- describe "performance metrics export" do
215
- before do
216
- 5.times do |i|
217
- collector.record_performance(
218
- operation: "decide",
219
- duration_ms: (i + 1) * 10.0,
220
- success: true
221
- )
222
- end
223
- end
224
-
225
- it "exports summary metrics" do
226
- output = exporter.export
227
-
228
- expect(output).to include("# TYPE test_operation_duration_ms summary")
229
- expect(output).to include("test_operation_duration_ms{quantile=\"0.5\"}")
230
- expect(output).to include("test_operation_duration_ms{quantile=\"0.95\"}")
231
- expect(output).to include("test_operation_duration_ms{quantile=\"0.99\"}")
232
- expect(output).to include("test_operation_duration_ms_sum")
233
- expect(output).to include("test_operation_duration_ms_count")
234
- end
235
- end
236
-
237
- describe "content type" do
238
- it "defines Prometheus content type" do
239
- expect(described_class::CONTENT_TYPE).to eq("text/plain; version=0.0.4")
240
- end
241
- end
242
- end