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,501 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/monitoring/metrics_collector"
3
-
4
- RSpec.describe DecisionAgent::Monitoring::MetricsCollector do
5
- let(:collector) { described_class.new(window_size: 60, storage: :memory) }
6
- let(:evaluation) do
7
- DecisionAgent::Evaluation.new(
8
- decision: "approve",
9
- weight: 0.9,
10
- reason: "Test reason",
11
- evaluator_name: "test_evaluator"
12
- )
13
- end
14
- let(:decision) do
15
- DecisionAgent::Decision.new(
16
- decision: "approve",
17
- confidence: 0.85,
18
- explanations: ["Test explanation"],
19
- evaluations: [evaluation],
20
- audit_payload: { timestamp: Time.now.utc.iso8601 }
21
- )
22
- end
23
- let(:context) { DecisionAgent::Context.new({ user: "test" }) }
24
-
25
- describe "#initialize" do
26
- it "initializes with default window size" do
27
- collector = described_class.new
28
- expect(collector.window_size).to eq(3600)
29
- end
30
-
31
- it "initializes with custom window size" do
32
- expect(collector.window_size).to eq(60)
33
- end
34
-
35
- it "initializes empty metrics" do
36
- counts = collector.metrics_count
37
- expect(counts[:decisions]).to eq(0)
38
- expect(counts[:evaluations]).to eq(0)
39
- expect(counts[:performance]).to eq(0)
40
- expect(counts[:errors]).to eq(0)
41
- end
42
- end
43
-
44
- describe "#record_decision" do
45
- it "records a decision metric" do
46
- metric = collector.record_decision(decision, context, duration_ms: 10.5)
47
-
48
- expect(metric[:decision]).to eq("approve")
49
- expect(metric[:confidence]).to eq(0.85)
50
- expect(metric[:duration_ms]).to eq(10.5)
51
- expect(metric[:context_size]).to eq(1)
52
- expect(metric[:evaluations_count]).to eq(1)
53
- expect(metric[:evaluator_names]).to eq(["test_evaluator"])
54
- end
55
-
56
- it "increments decision count" do
57
- expect do
58
- collector.record_decision(decision, context)
59
- end.to change { collector.metrics_count[:decisions] }.by(1)
60
- end
61
-
62
- it "notifies observers" do
63
- observed = []
64
- collector.add_observer do |type, metric|
65
- observed << [type, metric]
66
- end
67
-
68
- collector.record_decision(decision, context)
69
-
70
- expect(observed.size).to eq(1)
71
- expect(observed[0][0]).to eq(:decision)
72
- expect(observed[0][1][:decision]).to eq("approve")
73
- end
74
- end
75
-
76
- describe "#record_evaluation" do
77
- it "records an evaluation metric" do
78
- metric = collector.record_evaluation(evaluation)
79
-
80
- expect(metric[:decision]).to eq("approve")
81
- expect(metric[:weight]).to eq(0.9)
82
- expect(metric[:evaluator_name]).to eq("test_evaluator")
83
- end
84
-
85
- it "increments evaluation count" do
86
- expect do
87
- collector.record_evaluation(evaluation)
88
- end.to change { collector.metrics_count[:evaluations] }.by(1)
89
- end
90
- end
91
-
92
- describe "#record_performance" do
93
- it "records performance metrics" do
94
- metric = collector.record_performance(
95
- operation: "decide",
96
- duration_ms: 25.5,
97
- success: true,
98
- metadata: { evaluators: 2 }
99
- )
100
-
101
- expect(metric[:operation]).to eq("decide")
102
- expect(metric[:duration_ms]).to eq(25.5)
103
- expect(metric[:success]).to be true
104
- expect(metric[:metadata]).to eq({ evaluators: 2 })
105
- end
106
-
107
- it "records failed operations" do
108
- metric = collector.record_performance(
109
- operation: "decide",
110
- duration_ms: 10.0,
111
- success: false
112
- )
113
-
114
- expect(metric[:success]).to be false
115
- end
116
- end
117
-
118
- describe "#record_error" do
119
- let(:error) { StandardError.new("Test error") }
120
-
121
- it "records error metrics" do
122
- metric = collector.record_error(error, context: { user_id: 123 })
123
-
124
- expect(metric[:error_class]).to eq("StandardError")
125
- expect(metric[:error_message]).to eq("Test error")
126
- expect(metric[:context]).to eq({ user_id: 123 })
127
- end
128
-
129
- it "increments error count" do
130
- expect do
131
- collector.record_error(error)
132
- end.to change { collector.metrics_count[:errors] }.by(1)
133
- end
134
- end
135
-
136
- describe "#statistics" do
137
- before do
138
- # Record some metrics
139
- 5.times do |i|
140
- collector.record_decision(decision, context, duration_ms: (i + 1) * 10)
141
- end
142
-
143
- 2.times do
144
- collector.record_performance(operation: "decide", duration_ms: 15.0, success: true)
145
- end
146
- collector.record_performance(operation: "decide", duration_ms: 20.0, success: false)
147
-
148
- collector.record_error(StandardError.new("Error 1"))
149
- end
150
-
151
- it "returns summary statistics" do
152
- stats = collector.statistics
153
-
154
- expect(stats[:summary][:total_decisions]).to eq(5)
155
- expect(stats[:summary][:total_evaluations]).to eq(0)
156
- expect(stats[:summary][:total_errors]).to eq(1)
157
- end
158
-
159
- it "computes decision statistics" do
160
- stats = collector.statistics
161
-
162
- expect(stats[:decisions][:total]).to eq(5)
163
- expect(stats[:decisions][:avg_confidence]).to eq(0.85)
164
- expect(stats[:decisions][:min_confidence]).to eq(0.85)
165
- expect(stats[:decisions][:max_confidence]).to eq(0.85)
166
- expect(stats[:decisions][:avg_duration_ms]).to be_within(0.1).of(30.0)
167
- end
168
-
169
- it "computes performance statistics" do
170
- stats = collector.statistics
171
-
172
- expect(stats[:performance][:total_operations]).to eq(3)
173
- expect(stats[:performance][:successful]).to eq(2)
174
- expect(stats[:performance][:failed]).to eq(1)
175
- expect(stats[:performance][:success_rate]).to be_within(0.01).of(0.6667)
176
- end
177
-
178
- it "computes error statistics" do
179
- stats = collector.statistics
180
-
181
- expect(stats[:errors][:total]).to eq(1)
182
- expect(stats[:errors][:by_type]["StandardError"]).to eq(1)
183
- end
184
-
185
- it "filters by time range" do
186
- stats = collector.statistics(time_range: 30)
187
- expect(stats[:summary][:time_range]).to eq("Last 30s")
188
- end
189
- end
190
-
191
- describe "#time_series" do
192
- before do
193
- 10.times do
194
- collector.record_decision(decision, context)
195
- sleep 0.01 # Small delay to ensure different buckets
196
- end
197
- end
198
-
199
- it "returns time series data" do
200
- series = collector.time_series(metric_type: :decisions, bucket_size: 1, time_range: 60)
201
-
202
- expect(series).to be_an(Array)
203
- expect(series.first).to have_key(:timestamp)
204
- expect(series.first).to have_key(:count)
205
- expect(series.first).to have_key(:metrics)
206
- end
207
-
208
- it "buckets metrics by time" do
209
- series = collector.time_series(metric_type: :decisions, bucket_size: 60, time_range: 3600)
210
-
211
- total_count = series.sum { |s| s[:count] }
212
- expect(total_count).to eq(10)
213
- end
214
- end
215
-
216
- describe "#clear!" do
217
- before do
218
- collector.record_decision(decision, context)
219
- collector.record_error(StandardError.new("Test"))
220
- end
221
-
222
- it "clears all metrics" do
223
- collector.clear!
224
-
225
- counts = collector.metrics_count
226
- expect(counts[:decisions]).to eq(0)
227
- expect(counts[:errors]).to eq(0)
228
- end
229
- end
230
-
231
- describe "thread safety" do
232
- it "handles concurrent writes safely" do
233
- threads = 10.times.map do
234
- Thread.new do
235
- 10.times do
236
- collector.record_decision(decision, context)
237
- end
238
- end
239
- end
240
-
241
- threads.each(&:join)
242
-
243
- expect(collector.metrics_count[:decisions]).to eq(100)
244
- end
245
-
246
- it "handles concurrent reads and writes" do
247
- writer = Thread.new do
248
- 50.times do
249
- collector.record_decision(decision, context)
250
- sleep 0.001
251
- end
252
- end
253
-
254
- reader = Thread.new do
255
- 50.times do
256
- collector.statistics
257
- sleep 0.001
258
- end
259
- end
260
-
261
- expect { writer.join && reader.join }.not_to raise_error
262
- end
263
- end
264
-
265
- describe "metric cleanup" do
266
- it "removes old metrics outside window" do
267
- collector = described_class.new(window_size: 1, storage: :memory, cleanup_threshold: 1)
268
-
269
- collector.record_decision(decision, context)
270
- expect(collector.metrics_count[:decisions]).to eq(1)
271
-
272
- sleep 1.5
273
-
274
- collector.record_decision(decision, context)
275
- # Old metric should be cleaned up (threshold=1 means cleanup on every record)
276
- expect(collector.metrics_count[:decisions]).to eq(1)
277
- end
278
- end
279
-
280
- describe "#record_evaluation" do
281
- it "notifies observers" do
282
- observed = []
283
- collector.add_observer do |type, metric|
284
- observed << [type, metric]
285
- end
286
-
287
- collector.record_evaluation(evaluation)
288
-
289
- expect(observed.size).to eq(1)
290
- expect(observed[0][0]).to eq(:evaluation)
291
- expect(observed[0][1][:decision]).to eq("approve")
292
- end
293
- end
294
-
295
- describe "#record_performance" do
296
- it "notifies observers" do
297
- observed = []
298
- collector.add_observer do |type, metric|
299
- observed << [type, metric]
300
- end
301
-
302
- collector.record_performance(operation: "test", duration_ms: 10.0, success: true)
303
-
304
- expect(observed.size).to eq(1)
305
- expect(observed[0][0]).to eq(:performance)
306
- expect(observed[0][1][:operation]).to eq("test")
307
- end
308
- end
309
-
310
- describe "#record_error" do
311
- it "notifies observers" do
312
- observed = []
313
- collector.add_observer do |type, metric|
314
- observed << [type, metric]
315
- end
316
-
317
- collector.record_error(StandardError.new("Test"))
318
-
319
- expect(observed.size).to eq(1)
320
- expect(observed[0][0]).to eq(:error)
321
- expect(observed[0][1][:error_class]).to eq("StandardError")
322
- end
323
-
324
- it "handles different error types" do
325
- expect { collector.record_error(ArgumentError.new("Arg error")) }.not_to raise_error
326
- expect { collector.record_error(TypeError.new("Type error")) }.not_to raise_error
327
- expect { collector.record_error(Exception.new("Exception")) }.not_to raise_error
328
- end
329
- end
330
-
331
- describe "#add_observer" do
332
- it "adds an observer callback" do
333
- callback = proc { |type, metric| }
334
- collector.add_observer(&callback)
335
- # Observer should be stored
336
- expect(collector.instance_variable_get(:@observers)).to include(callback)
337
- end
338
-
339
- it "handles observer errors gracefully" do
340
- # Add observer that raises error
341
- collector.add_observer do |_type, _metric|
342
- raise "Observer error"
343
- end
344
-
345
- # Should not raise, just warn
346
- expect { collector.record_decision(decision, context) }.not_to raise_error
347
- end
348
- end
349
-
350
- describe "#statistics" do
351
- before do
352
- 3.times do
353
- evaluation = DecisionAgent::Evaluation.new(
354
- decision: "approve",
355
- weight: 0.8,
356
- reason: "Test reason",
357
- evaluator_name: "eval1"
358
- )
359
- collector.record_evaluation(evaluation)
360
- end
361
- 2.times do
362
- evaluation = DecisionAgent::Evaluation.new(
363
- decision: "reject",
364
- weight: 0.6,
365
- reason: "Test reason",
366
- evaluator_name: "eval2"
367
- )
368
- collector.record_evaluation(evaluation)
369
- end
370
- end
371
-
372
- it "computes evaluation statistics" do
373
- stats = collector.statistics
374
- expect(stats[:evaluations][:total]).to eq(5)
375
- expect(stats[:evaluations][:avg_weight]).to be_within(0.01).of(0.72)
376
- end
377
-
378
- it "handles empty decisions gracefully" do
379
- empty_collector = described_class.new(storage: :memory)
380
- stats = empty_collector.statistics
381
- expect(stats[:decisions]).to eq({})
382
- end
383
-
384
- it "handles decisions without duration_ms" do
385
- decision_no_duration = DecisionAgent::Decision.new(
386
- decision: "approve",
387
- confidence: 0.5,
388
- explanations: [],
389
- evaluations: [],
390
- audit_payload: {}
391
- )
392
- collector.record_decision(decision_no_duration, context)
393
- stats = collector.statistics
394
- expect(stats[:decisions][:avg_duration_ms]).to be_nil
395
- end
396
- end
397
-
398
- describe "#time_series" do
399
- it "handles empty metric types" do
400
- series = collector.time_series(metric_type: :nonexistent, bucket_size: 60, time_range: 3600)
401
- expect(series).to eq([])
402
- end
403
-
404
- it "filters metrics by time range" do
405
- # Record some old metrics (simulated)
406
- old_time = Time.now.utc - 7200
407
- allow(Time).to receive(:now).and_return(Time.at(old_time.to_i))
408
- 5.times { collector.record_decision(decision, context) }
409
-
410
- # Record new metrics
411
- allow(Time).to receive(:now).and_call_original
412
- 3.times { collector.record_decision(decision, context) }
413
-
414
- series = collector.time_series(metric_type: :decisions, bucket_size: 60, time_range: 3600)
415
- # Should only include recent metrics
416
- total = series.sum { |s| s[:count] }
417
- expect(total).to be <= 3
418
- end
419
- end
420
-
421
- describe "#cleanup_old_metrics_from_storage" do
422
- it "delegates to storage adapter if it has cleanup method" do
423
- # Using memory adapter which doesn't have cleanup
424
- expect(collector.cleanup_old_metrics_from_storage(older_than: 3600)).to eq(0)
425
- end
426
- end
427
-
428
- describe "#initialize_storage_adapter" do
429
- it "uses memory storage when :memory specified" do
430
- collector = described_class.new(storage: :memory)
431
- expect(collector.storage_adapter).to be_a(DecisionAgent::Monitoring::Storage::MemoryAdapter)
432
- end
433
-
434
- it "raises error for unknown storage option" do
435
- expect do
436
- described_class.new(storage: :unknown)
437
- end.to raise_error(ArgumentError, /Unknown storage option/)
438
- end
439
- end
440
-
441
- describe "error severity determination" do
442
- it "determines severity for ArgumentError as medium" do
443
- error = ArgumentError.new("test")
444
- collector.record_error(error)
445
- # Just verify it doesn't raise
446
- expect(collector.metrics_count[:errors]).to eq(1)
447
- end
448
-
449
- it "determines severity for TypeError as medium" do
450
- error = TypeError.new("test")
451
- collector.record_error(error)
452
- expect(collector.metrics_count[:errors]).to eq(1)
453
- end
454
-
455
- it "determines severity for Exception as critical" do
456
- error = Exception.new("test")
457
- collector.record_error(error)
458
- expect(collector.metrics_count[:errors]).to eq(1)
459
- end
460
- end
461
-
462
- describe "decision status determination" do
463
- it "determines status for high confidence decisions" do
464
- high_conf_decision = DecisionAgent::Decision.new(
465
- decision: "approve",
466
- confidence: 0.9,
467
- explanations: [],
468
- evaluations: [],
469
- audit_payload: {}
470
- )
471
- collector.record_decision(high_conf_decision, context)
472
- # Just verify it records successfully
473
- expect(collector.metrics_count[:decisions]).to eq(1)
474
- end
475
-
476
- it "determines status for low confidence decisions" do
477
- low_conf_decision = DecisionAgent::Decision.new(
478
- decision: "approve",
479
- confidence: 0.2,
480
- explanations: [],
481
- evaluations: [],
482
- audit_payload: {}
483
- )
484
- collector.record_decision(low_conf_decision, context)
485
- expect(collector.metrics_count[:decisions]).to eq(1)
486
- end
487
- end
488
-
489
- describe "#compute_performance_stats" do
490
- it "computes percentile statistics" do
491
- durations = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
492
- durations.each do |duration|
493
- collector.record_performance(operation: "test", duration_ms: duration, success: true)
494
- end
495
-
496
- stats = collector.statistics
497
- expect(stats[:performance][:p95_duration_ms]).to be >= 90
498
- expect(stats[:performance][:p99_duration_ms]).to be >= 95
499
- end
500
- end
501
- end