decision_agent 0.3.0 → 1.0.1
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 +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- 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
|