decision_agent 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/README.md +234 -919
- data/bin/decision_agent +5 -5
- data/lib/decision_agent/agent.rb +19 -26
- data/lib/decision_agent/audit/null_adapter.rb +1 -2
- data/lib/decision_agent/decision.rb +3 -1
- data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
- data/lib/decision_agent/dsl/rule_parser.rb +4 -6
- data/lib/decision_agent/dsl/schema_validator.rb +27 -31
- data/lib/decision_agent/errors.rb +21 -6
- data/lib/decision_agent/evaluation.rb +3 -1
- data/lib/decision_agent/evaluation_validator.rb +78 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
- data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
- data/lib/decision_agent/replay/replay.rb +12 -22
- data/lib/decision_agent/scoring/base.rb +1 -1
- data/lib/decision_agent/scoring/consensus.rb +5 -5
- data/lib/decision_agent/scoring/weighted_average.rb +1 -1
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +141 -0
- data/lib/decision_agent/versioning/adapter.rb +100 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +290 -0
- data/lib/decision_agent/versioning/version_manager.rb +127 -0
- data/lib/decision_agent/web/public/app.js +318 -0
- data/lib/decision_agent/web/public/index.html +56 -1
- data/lib/decision_agent/web/public/styles.css +219 -0
- data/lib/decision_agent/web/server.rb +169 -9
- data/lib/decision_agent.rb +11 -0
- data/lib/generators/decision_agent/install/install_generator.rb +40 -0
- data/lib/generators/decision_agent/install/templates/README +47 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +37 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +66 -0
- data/spec/activerecord_thread_safety_spec.rb +553 -0
- data/spec/agent_spec.rb +13 -13
- data/spec/api_contract_spec.rb +16 -16
- data/spec/audit_adapters_spec.rb +3 -3
- data/spec/comprehensive_edge_cases_spec.rb +86 -86
- data/spec/dsl_validation_spec.rb +83 -83
- data/spec/edge_cases_spec.rb +23 -23
- data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
- data/spec/examples.txt +548 -0
- data/spec/issue_verification_spec.rb +685 -0
- data/spec/json_rule_evaluator_spec.rb +15 -15
- data/spec/monitoring/alert_manager_spec.rb +378 -0
- data/spec/monitoring/metrics_collector_spec.rb +281 -0
- data/spec/monitoring/monitored_agent_spec.rb +222 -0
- data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
- data/spec/replay_edge_cases_spec.rb +58 -58
- data/spec/replay_spec.rb +11 -11
- data/spec/rfc8785_canonicalization_spec.rb +215 -0
- data/spec/scoring_spec.rb +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/thread_safety_spec.rb +482 -0
- data/spec/thread_safety_spec.rb.broken +878 -0
- data/spec/versioning_spec.rb +777 -0
- data/spec/web_ui_rack_spec.rb +135 -0
- metadata +84 -11
|
@@ -302,7 +302,7 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
302
302
|
rules: [
|
|
303
303
|
{
|
|
304
304
|
id: "rule_1",
|
|
305
|
-
if: { field: "status", op: "in", value: [
|
|
305
|
+
if: { field: "status", op: "in", value: %w[open pending review] },
|
|
306
306
|
then: { decision: "active" }
|
|
307
307
|
}
|
|
308
308
|
]
|
|
@@ -418,25 +418,25 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
418
418
|
|
|
419
419
|
describe "invalid DSL handling" do
|
|
420
420
|
it "raises InvalidRuleDslError for malformed JSON" do
|
|
421
|
-
expect
|
|
421
|
+
expect do
|
|
422
422
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: "{ invalid json")
|
|
423
|
-
|
|
423
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /Invalid JSON/)
|
|
424
424
|
end
|
|
425
425
|
|
|
426
426
|
it "raises InvalidRuleDslError when version is missing" do
|
|
427
427
|
rules = { rules: [] }
|
|
428
428
|
|
|
429
|
-
expect
|
|
429
|
+
expect do
|
|
430
430
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
431
|
-
|
|
431
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /version/)
|
|
432
432
|
end
|
|
433
433
|
|
|
434
434
|
it "raises InvalidRuleDslError when rules array is missing" do
|
|
435
435
|
rules = { version: "1.0" }
|
|
436
436
|
|
|
437
|
-
expect
|
|
437
|
+
expect do
|
|
438
438
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
439
|
-
|
|
439
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /rules/)
|
|
440
440
|
end
|
|
441
441
|
|
|
442
442
|
it "raises InvalidRuleDslError when rule is missing id" do
|
|
@@ -450,9 +450,9 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
450
450
|
]
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
expect
|
|
453
|
+
expect do
|
|
454
454
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
455
|
-
|
|
455
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'id'/)
|
|
456
456
|
end
|
|
457
457
|
|
|
458
458
|
it "raises InvalidRuleDslError when rule is missing if clause" do
|
|
@@ -466,9 +466,9 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
466
466
|
]
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
expect
|
|
469
|
+
expect do
|
|
470
470
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
471
|
-
|
|
471
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'if'/)
|
|
472
472
|
end
|
|
473
473
|
|
|
474
474
|
it "raises InvalidRuleDslError when rule is missing then clause" do
|
|
@@ -482,9 +482,9 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
482
482
|
]
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
expect
|
|
485
|
+
expect do
|
|
486
486
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
487
|
-
|
|
487
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'then'/)
|
|
488
488
|
end
|
|
489
489
|
|
|
490
490
|
it "raises InvalidRuleDslError when then clause is missing decision" do
|
|
@@ -499,9 +499,9 @@ RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
|
|
|
499
499
|
]
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
expect
|
|
502
|
+
expect do
|
|
503
503
|
DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
504
|
-
|
|
504
|
+
end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'decision'/)
|
|
505
505
|
end
|
|
506
506
|
end
|
|
507
507
|
|
|
@@ -0,0 +1,378 @@
|
|
|
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
|