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,378 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/monitoring/metrics_collector"
3
- require "decision_agent/monitoring/alert_manager"
4
-
5
- RSpec.describe DecisionAgent::Monitoring::AlertManager do
6
- let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new }
7
- let(:manager) { described_class.new(metrics_collector: collector) }
8
-
9
- describe "#initialize" do
10
- it "initializes with empty rules and alerts" do
11
- expect(manager.rules).to be_empty
12
- expect(manager.alerts).to be_empty
13
- end
14
- end
15
-
16
- describe "#add_rule" do
17
- it "adds an alert rule" do
18
- rule = manager.add_rule(
19
- name: "High Error Rate",
20
- condition: ->(stats) { stats.dig(:errors, :total).to_i > 10 },
21
- severity: :critical
22
- )
23
-
24
- expect(rule).to be_a(Hash)
25
- expect(rule[:name]).to eq("High Error Rate")
26
- expect(rule[:severity]).to eq(:critical)
27
- expect(rule[:enabled]).to be true
28
- end
29
-
30
- it "generates unique rule ID" do
31
- rule1 = manager.add_rule(name: "Rule 1", condition: ->(_) { false })
32
- rule2 = manager.add_rule(name: "Rule 1", condition: ->(_) { false })
33
-
34
- expect(rule1[:id]).not_to eq(rule2[:id])
35
- end
36
-
37
- it "sets default values" do
38
- rule = manager.add_rule(name: "Test", condition: ->(_) { false })
39
-
40
- expect(rule[:severity]).to eq(:warning)
41
- expect(rule[:cooldown]).to eq(300)
42
- expect(rule[:message]).to eq("Alert: Test")
43
- end
44
-
45
- it "allows custom message and cooldown" do
46
- rule = manager.add_rule(
47
- name: "Test",
48
- condition: ->(_) { false },
49
- message: "Custom message",
50
- cooldown: 600
51
- )
52
-
53
- expect(rule[:message]).to eq("Custom message")
54
- expect(rule[:cooldown]).to eq(600)
55
- end
56
- end
57
-
58
- describe "#remove_rule" do
59
- it "removes a rule by ID" do
60
- rule = manager.add_rule(name: "Test", condition: ->(_) { false })
61
- rule_id = rule[:id]
62
-
63
- manager.remove_rule(rule_id)
64
-
65
- expect(manager.rules).not_to include(rule)
66
- end
67
- end
68
-
69
- describe "#toggle_rule" do
70
- it "enables and disables rules" do
71
- rule = manager.add_rule(name: "Test", condition: ->(_) { false })
72
-
73
- manager.toggle_rule(rule[:id], false)
74
- expect(manager.rules.first[:enabled]).to be false
75
-
76
- manager.toggle_rule(rule[:id], true)
77
- expect(manager.rules.first[:enabled]).to be true
78
- end
79
- end
80
-
81
- describe "#check_rules" do
82
- before do
83
- # Setup metrics
84
- 5.times { collector.record_error(StandardError.new("Test")) }
85
- end
86
-
87
- it "evaluates all enabled rules" do
88
- triggered = false
89
-
90
- manager.add_rule(
91
- name: "Error Threshold",
92
- condition: ->(stats) { stats.dig(:errors, :total).to_i > 3 }
93
- )
94
-
95
- manager.add_handler { |_| triggered = true }
96
- manager.check_rules
97
-
98
- expect(triggered).to be true
99
- expect(manager.active_alerts.size).to eq(1)
100
- end
101
-
102
- it "skips disabled rules" do
103
- rule = manager.add_rule(
104
- name: "Error Threshold",
105
- condition: ->(stats) { stats.dig(:errors, :total).to_i > 3 }
106
- )
107
-
108
- manager.toggle_rule(rule[:id], false)
109
- manager.check_rules
110
-
111
- expect(manager.active_alerts).to be_empty
112
- end
113
-
114
- it "respects cooldown period" do
115
- manager.add_rule(
116
- name: "Error Threshold",
117
- condition: ->(stats) { stats.dig(:errors, :total).to_i > 3 },
118
- cooldown: 60
119
- )
120
-
121
- manager.check_rules
122
- expect(manager.active_alerts.size).to eq(1)
123
-
124
- # Immediate second check should not trigger due to cooldown
125
- manager.check_rules
126
- expect(manager.active_alerts.size).to eq(1)
127
- end
128
-
129
- it "triggers multiple rules" do
130
- manager.add_rule(
131
- name: "Rule 1",
132
- condition: ->(_) { true }
133
- )
134
- manager.add_rule(
135
- name: "Rule 2",
136
- condition: ->(_) { true }
137
- )
138
-
139
- manager.check_rules
140
-
141
- expect(manager.active_alerts.size).to eq(2)
142
- end
143
- end
144
-
145
- describe "#add_handler" do
146
- it "registers alert handlers" do
147
- alerts_received = []
148
-
149
- manager.add_handler do |alert|
150
- alerts_received << alert
151
- end
152
-
153
- manager.add_rule(name: "Test", condition: ->(_) { true })
154
- manager.check_rules
155
-
156
- expect(alerts_received.size).to eq(1)
157
- expect(alerts_received.first[:rule_name]).to eq("Test")
158
- end
159
-
160
- it "calls multiple handlers" do
161
- count1 = 0
162
- count2 = 0
163
-
164
- manager.add_handler { count1 += 1 }
165
- manager.add_handler { count2 += 1 }
166
-
167
- manager.add_rule(name: "Test", condition: ->(_) { true })
168
- manager.check_rules
169
-
170
- expect(count1).to eq(1)
171
- expect(count2).to eq(1)
172
- end
173
-
174
- it "continues if a handler fails" do
175
- successful_handler_called = false
176
-
177
- manager.add_handler { raise "Handler error" }
178
- manager.add_handler { successful_handler_called = true }
179
-
180
- manager.add_rule(name: "Test", condition: ->(_) { true })
181
-
182
- expect { manager.check_rules }.not_to raise_error
183
- expect(successful_handler_called).to be true
184
- end
185
- end
186
-
187
- describe "#acknowledge_alert" do
188
- before do
189
- manager.add_rule(name: "Test", condition: ->(_) { true })
190
- manager.check_rules
191
- end
192
-
193
- it "acknowledges an alert" do
194
- alert = manager.active_alerts.first
195
- manager.acknowledge_alert(alert[:id], acknowledged_by: "admin")
196
-
197
- acknowledged = manager.all_alerts.find { |a| a[:id] == alert[:id] }
198
- expect(acknowledged[:status]).to eq(:acknowledged)
199
- expect(acknowledged[:acknowledged_by]).to eq("admin")
200
- expect(acknowledged[:acknowledged_at]).to be_a(Time)
201
- end
202
- end
203
-
204
- describe "#resolve_alert" do
205
- before do
206
- manager.add_rule(name: "Test", condition: ->(_) { true })
207
- manager.check_rules
208
- end
209
-
210
- it "resolves an alert" do
211
- alert = manager.active_alerts.first
212
- manager.resolve_alert(alert[:id], resolved_by: "admin")
213
-
214
- resolved = manager.all_alerts.find { |a| a[:id] == alert[:id] }
215
- expect(resolved[:status]).to eq(:resolved)
216
- expect(resolved[:resolved_by]).to eq("admin")
217
- expect(resolved[:resolved_at]).to be_a(Time)
218
- end
219
-
220
- it "removes from active alerts" do
221
- alert = manager.active_alerts.first
222
- manager.resolve_alert(alert[:id])
223
-
224
- expect(manager.active_alerts).to be_empty
225
- end
226
- end
227
-
228
- describe "#clear_old_alerts" do
229
- it "clears old resolved alerts" do
230
- manager.add_rule(name: "Test", condition: ->(_) { true })
231
- manager.check_rules
232
-
233
- alert = manager.active_alerts.first
234
- manager.resolve_alert(alert[:id])
235
-
236
- # Manually set old timestamp
237
- manager.alerts.first[:triggered_at] = Time.now.utc - 90_000
238
-
239
- manager.clear_old_alerts(older_than: 86_400)
240
-
241
- expect(manager.all_alerts).to be_empty
242
- end
243
-
244
- it "keeps active alerts regardless of age" do
245
- manager.add_rule(name: "Test", condition: ->(_) { true })
246
- manager.check_rules
247
-
248
- # Manually set old timestamp
249
- manager.alerts.first[:triggered_at] = Time.now.utc - 90_000
250
-
251
- manager.clear_old_alerts(older_than: 86_400)
252
-
253
- expect(manager.active_alerts.size).to eq(1)
254
- end
255
- end
256
-
257
- describe "built-in conditions" do
258
- describe ".high_error_rate" do
259
- it "detects high error rate" do
260
- condition = described_class.high_error_rate(threshold: 0.1)
261
-
262
- # Low error rate
263
- stats = { performance: { total_operations: 100, success_rate: 0.95 } }
264
- expect(condition.call(stats)).to be false
265
-
266
- # High error rate
267
- stats = { performance: { total_operations: 100, success_rate: 0.80 } }
268
- expect(condition.call(stats)).to be true
269
- end
270
- end
271
-
272
- describe ".low_confidence" do
273
- it "detects low confidence" do
274
- condition = described_class.low_confidence(threshold: 0.5)
275
-
276
- stats = { decisions: { avg_confidence: 0.8 } }
277
- expect(condition.call(stats)).to be false
278
-
279
- stats = { decisions: { avg_confidence: 0.3 } }
280
- expect(condition.call(stats)).to be true
281
- end
282
- end
283
-
284
- describe ".high_latency" do
285
- it "detects high latency" do
286
- condition = described_class.high_latency(threshold_ms: 1000)
287
-
288
- stats = { performance: { p95_duration_ms: 500 } }
289
- expect(condition.call(stats)).to be false
290
-
291
- stats = { performance: { p95_duration_ms: 1500 } }
292
- expect(condition.call(stats)).to be true
293
- end
294
- end
295
-
296
- describe ".error_spike" do
297
- it "detects error spikes" do
298
- condition = described_class.error_spike(threshold: 10)
299
-
300
- stats = { errors: { total: 5 } }
301
- expect(condition.call(stats)).to be false
302
-
303
- stats = { errors: { total: 15 } }
304
- expect(condition.call(stats)).to be true
305
- end
306
- end
307
- end
308
-
309
- describe "hash-based conditions" do
310
- it "evaluates hash conditions" do
311
- manager.add_rule(
312
- name: "Hash Condition",
313
- condition: { metric: "errors.total", op: "gt", value: 5 }
314
- )
315
-
316
- 5.times { collector.record_error(StandardError.new("Test")) }
317
- manager.check_rules
318
- expect(manager.active_alerts).to be_empty
319
-
320
- collector.record_error(StandardError.new("Test"))
321
- manager.check_rules
322
- expect(manager.active_alerts.size).to eq(1)
323
- end
324
-
325
- it "supports different operators" do
326
- # Greater than
327
- condition = { metric: "errors.total", op: "gt", value: 5 }
328
- stats = { errors: { total: 10 } }
329
- expect(manager.send(:evaluate_hash_condition, condition, stats)).to be true
330
-
331
- # Less than
332
- condition = { metric: "errors.total", op: "lt", value: 5 }
333
- expect(manager.send(:evaluate_hash_condition, condition, stats)).to be false
334
-
335
- # Equal
336
- condition = { metric: "errors.total", op: "eq", value: 10 }
337
- expect(manager.send(:evaluate_hash_condition, condition, stats)).to be true
338
- end
339
- end
340
-
341
- describe "#start_monitoring and #stop_monitoring" do
342
- it "starts background monitoring" do
343
- manager.start_monitoring(interval: 1)
344
- sleep 0.1
345
-
346
- expect(manager.instance_variable_get(:@monitoring_thread)).to be_alive
347
-
348
- manager.stop_monitoring
349
- sleep 0.1
350
-
351
- expect(manager.instance_variable_get(:@monitoring_thread)).to be_nil
352
- end
353
- end
354
-
355
- describe "thread safety" do
356
- it "handles concurrent rule additions" do
357
- threads = 10.times.map do |i|
358
- Thread.new do
359
- manager.add_rule(name: "Rule #{i}", condition: ->(_) { false })
360
- end
361
- end
362
-
363
- threads.each(&:join)
364
-
365
- expect(manager.rules.size).to eq(10)
366
- end
367
-
368
- it "handles concurrent alert checks" do
369
- manager.add_rule(name: "Test", condition: ->(_) { true })
370
-
371
- threads = 5.times.map do
372
- Thread.new { manager.check_rules }
373
- end
374
-
375
- expect { threads.each(&:join) }.not_to raise_error
376
- end
377
- end
378
- end