decision_agent 0.2.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -1,499 +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(:decision) do
7
- double(
8
- "Decision",
9
- decision: "approve",
10
- confidence: 0.85,
11
- evaluations: [
12
- double("Evaluation", evaluator_name: "test_evaluator")
13
- ]
14
- )
15
- end
16
- let(:context) { double("Context", to_h: { user: "test" }) }
17
-
18
- describe "#initialize" do
19
- it "initializes with default window size" do
20
- collector = described_class.new
21
- expect(collector.window_size).to eq(3600)
22
- end
23
-
24
- it "initializes with custom window size" do
25
- expect(collector.window_size).to eq(60)
26
- end
27
-
28
- it "initializes empty metrics" do
29
- counts = collector.metrics_count
30
- expect(counts[:decisions]).to eq(0)
31
- expect(counts[:evaluations]).to eq(0)
32
- expect(counts[:performance]).to eq(0)
33
- expect(counts[:errors]).to eq(0)
34
- end
35
- end
36
-
37
- describe "#record_decision" do
38
- it "records a decision metric" do
39
- metric = collector.record_decision(decision, context, duration_ms: 10.5)
40
-
41
- expect(metric[:decision]).to eq("approve")
42
- expect(metric[:confidence]).to eq(0.85)
43
- expect(metric[:duration_ms]).to eq(10.5)
44
- expect(metric[:context_size]).to eq(1)
45
- expect(metric[:evaluations_count]).to eq(1)
46
- expect(metric[:evaluator_names]).to eq(["test_evaluator"])
47
- end
48
-
49
- it "increments decision count" do
50
- expect do
51
- collector.record_decision(decision, context)
52
- end.to change { collector.metrics_count[:decisions] }.by(1)
53
- end
54
-
55
- it "notifies observers" do
56
- observed = []
57
- collector.add_observer do |type, metric|
58
- observed << [type, metric]
59
- end
60
-
61
- collector.record_decision(decision, context)
62
-
63
- expect(observed.size).to eq(1)
64
- expect(observed[0][0]).to eq(:decision)
65
- expect(observed[0][1][:decision]).to eq("approve")
66
- end
67
- end
68
-
69
- describe "#record_evaluation" do
70
- let(:evaluation) do
71
- double(
72
- "Evaluation",
73
- decision: "approve",
74
- weight: 0.9,
75
- evaluator_name: "test_evaluator"
76
- )
77
- end
78
-
79
- it "records an evaluation metric" do
80
- metric = collector.record_evaluation(evaluation)
81
-
82
- expect(metric[:decision]).to eq("approve")
83
- expect(metric[:weight]).to eq(0.9)
84
- expect(metric[:evaluator_name]).to eq("test_evaluator")
85
- end
86
-
87
- it "increments evaluation count" do
88
- expect do
89
- collector.record_evaluation(evaluation)
90
- end.to change { collector.metrics_count[:evaluations] }.by(1)
91
- end
92
- end
93
-
94
- describe "#record_performance" do
95
- it "records performance metrics" do
96
- metric = collector.record_performance(
97
- operation: "decide",
98
- duration_ms: 25.5,
99
- success: true,
100
- metadata: { evaluators: 2 }
101
- )
102
-
103
- expect(metric[:operation]).to eq("decide")
104
- expect(metric[:duration_ms]).to eq(25.5)
105
- expect(metric[:success]).to be true
106
- expect(metric[:metadata]).to eq({ evaluators: 2 })
107
- end
108
-
109
- it "records failed operations" do
110
- metric = collector.record_performance(
111
- operation: "decide",
112
- duration_ms: 10.0,
113
- success: false
114
- )
115
-
116
- expect(metric[:success]).to be false
117
- end
118
- end
119
-
120
- describe "#record_error" do
121
- let(:error) { StandardError.new("Test error") }
122
-
123
- it "records error metrics" do
124
- metric = collector.record_error(error, context: { user_id: 123 })
125
-
126
- expect(metric[:error_class]).to eq("StandardError")
127
- expect(metric[:error_message]).to eq("Test error")
128
- expect(metric[:context]).to eq({ user_id: 123 })
129
- end
130
-
131
- it "increments error count" do
132
- expect do
133
- collector.record_error(error)
134
- end.to change { collector.metrics_count[:errors] }.by(1)
135
- end
136
- end
137
-
138
- describe "#statistics" do
139
- before do
140
- # Record some metrics
141
- 5.times do |i|
142
- collector.record_decision(decision, context, duration_ms: (i + 1) * 10)
143
- end
144
-
145
- 2.times do
146
- collector.record_performance(operation: "decide", duration_ms: 15.0, success: true)
147
- end
148
- collector.record_performance(operation: "decide", duration_ms: 20.0, success: false)
149
-
150
- collector.record_error(StandardError.new("Error 1"))
151
- end
152
-
153
- it "returns summary statistics" do
154
- stats = collector.statistics
155
-
156
- expect(stats[:summary][:total_decisions]).to eq(5)
157
- expect(stats[:summary][:total_evaluations]).to eq(0)
158
- expect(stats[:summary][:total_errors]).to eq(1)
159
- end
160
-
161
- it "computes decision statistics" do
162
- stats = collector.statistics
163
-
164
- expect(stats[:decisions][:total]).to eq(5)
165
- expect(stats[:decisions][:avg_confidence]).to eq(0.85)
166
- expect(stats[:decisions][:min_confidence]).to eq(0.85)
167
- expect(stats[:decisions][:max_confidence]).to eq(0.85)
168
- expect(stats[:decisions][:avg_duration_ms]).to be_within(0.1).of(30.0)
169
- end
170
-
171
- it "computes performance statistics" do
172
- stats = collector.statistics
173
-
174
- expect(stats[:performance][:total_operations]).to eq(3)
175
- expect(stats[:performance][:successful]).to eq(2)
176
- expect(stats[:performance][:failed]).to eq(1)
177
- expect(stats[:performance][:success_rate]).to be_within(0.01).of(0.6667)
178
- end
179
-
180
- it "computes error statistics" do
181
- stats = collector.statistics
182
-
183
- expect(stats[:errors][:total]).to eq(1)
184
- expect(stats[:errors][:by_type]["StandardError"]).to eq(1)
185
- end
186
-
187
- it "filters by time range" do
188
- stats = collector.statistics(time_range: 30)
189
- expect(stats[:summary][:time_range]).to eq("Last 30s")
190
- end
191
- end
192
-
193
- describe "#time_series" do
194
- before do
195
- 10.times do
196
- collector.record_decision(decision, context)
197
- sleep 0.01 # Small delay to ensure different buckets
198
- end
199
- end
200
-
201
- it "returns time series data" do
202
- series = collector.time_series(metric_type: :decisions, bucket_size: 1, time_range: 60)
203
-
204
- expect(series).to be_an(Array)
205
- expect(series.first).to have_key(:timestamp)
206
- expect(series.first).to have_key(:count)
207
- expect(series.first).to have_key(:metrics)
208
- end
209
-
210
- it "buckets metrics by time" do
211
- series = collector.time_series(metric_type: :decisions, bucket_size: 60, time_range: 3600)
212
-
213
- total_count = series.sum { |s| s[:count] }
214
- expect(total_count).to eq(10)
215
- end
216
- end
217
-
218
- describe "#clear!" do
219
- before do
220
- collector.record_decision(decision, context)
221
- collector.record_error(StandardError.new("Test"))
222
- end
223
-
224
- it "clears all metrics" do
225
- collector.clear!
226
-
227
- counts = collector.metrics_count
228
- expect(counts[:decisions]).to eq(0)
229
- expect(counts[:errors]).to eq(0)
230
- end
231
- end
232
-
233
- describe "thread safety" do
234
- it "handles concurrent writes safely" do
235
- threads = 10.times.map do
236
- Thread.new do
237
- 10.times do
238
- collector.record_decision(decision, context)
239
- end
240
- end
241
- end
242
-
243
- threads.each(&:join)
244
-
245
- expect(collector.metrics_count[:decisions]).to eq(100)
246
- end
247
-
248
- it "handles concurrent reads and writes" do
249
- writer = Thread.new do
250
- 50.times do
251
- collector.record_decision(decision, context)
252
- sleep 0.001
253
- end
254
- end
255
-
256
- reader = Thread.new do
257
- 50.times do
258
- collector.statistics
259
- sleep 0.001
260
- end
261
- end
262
-
263
- expect { writer.join && reader.join }.not_to raise_error
264
- end
265
- end
266
-
267
- describe "metric cleanup" do
268
- it "removes old metrics outside window" do
269
- collector = described_class.new(window_size: 1, storage: :memory, cleanup_threshold: 1)
270
-
271
- collector.record_decision(decision, context)
272
- expect(collector.metrics_count[:decisions]).to eq(1)
273
-
274
- sleep 1.5
275
-
276
- collector.record_decision(decision, context)
277
- # Old metric should be cleaned up (threshold=1 means cleanup on every record)
278
- expect(collector.metrics_count[:decisions]).to eq(1)
279
- end
280
- end
281
-
282
- describe "#record_evaluation" do
283
- let(:evaluation) do
284
- double(
285
- "Evaluation",
286
- decision: "approve",
287
- weight: 0.9,
288
- evaluator_name: "test_evaluator"
289
- )
290
- end
291
-
292
- it "notifies observers" do
293
- observed = []
294
- collector.add_observer do |type, metric|
295
- observed << [type, metric]
296
- end
297
-
298
- collector.record_evaluation(evaluation)
299
-
300
- expect(observed.size).to eq(1)
301
- expect(observed[0][0]).to eq(:evaluation)
302
- expect(observed[0][1][:decision]).to eq("approve")
303
- end
304
- end
305
-
306
- describe "#record_performance" do
307
- it "notifies observers" do
308
- observed = []
309
- collector.add_observer do |type, metric|
310
- observed << [type, metric]
311
- end
312
-
313
- collector.record_performance(operation: "test", duration_ms: 10.0, success: true)
314
-
315
- expect(observed.size).to eq(1)
316
- expect(observed[0][0]).to eq(:performance)
317
- expect(observed[0][1][:operation]).to eq("test")
318
- end
319
- end
320
-
321
- describe "#record_error" do
322
- it "notifies observers" do
323
- observed = []
324
- collector.add_observer do |type, metric|
325
- observed << [type, metric]
326
- end
327
-
328
- collector.record_error(StandardError.new("Test"))
329
-
330
- expect(observed.size).to eq(1)
331
- expect(observed[0][0]).to eq(:error)
332
- expect(observed[0][1][:error_class]).to eq("StandardError")
333
- end
334
-
335
- it "handles different error types" do
336
- expect { collector.record_error(ArgumentError.new("Arg error")) }.not_to raise_error
337
- expect { collector.record_error(TypeError.new("Type error")) }.not_to raise_error
338
- expect { collector.record_error(Exception.new("Exception")) }.not_to raise_error
339
- end
340
- end
341
-
342
- describe "#add_observer" do
343
- it "adds an observer callback" do
344
- callback = proc { |type, metric| }
345
- collector.add_observer(&callback)
346
- # Observer should be stored
347
- expect(collector.instance_variable_get(:@observers)).to include(callback)
348
- end
349
-
350
- it "handles observer errors gracefully" do
351
- # Add observer that raises error
352
- collector.add_observer do |_type, _metric|
353
- raise "Observer error"
354
- end
355
-
356
- # Should not raise, just warn
357
- expect { collector.record_decision(decision, context) }.not_to raise_error
358
- end
359
- end
360
-
361
- describe "#statistics" do
362
- before do
363
- 3.times do
364
- evaluation = double("Evaluation", decision: "approve", weight: 0.8, evaluator_name: "eval1")
365
- collector.record_evaluation(evaluation)
366
- end
367
- 2.times do
368
- evaluation = double("Evaluation", decision: "reject", weight: 0.6, evaluator_name: "eval2")
369
- collector.record_evaluation(evaluation)
370
- end
371
- end
372
-
373
- it "computes evaluation statistics" do
374
- stats = collector.statistics
375
- expect(stats[:evaluations][:total]).to eq(5)
376
- expect(stats[:evaluations][:avg_weight]).to be_within(0.01).of(0.72)
377
- end
378
-
379
- it "handles empty decisions gracefully" do
380
- empty_collector = described_class.new(storage: :memory)
381
- stats = empty_collector.statistics
382
- expect(stats[:decisions]).to eq({})
383
- end
384
-
385
- it "handles decisions without duration_ms" do
386
- decision_no_duration = double(
387
- "Decision",
388
- decision: "approve",
389
- confidence: 0.5,
390
- evaluations: []
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 = double(
465
- "Decision",
466
- decision: "approve",
467
- confidence: 0.9,
468
- evaluations: []
469
- )
470
- collector.record_decision(high_conf_decision, context)
471
- # Just verify it records successfully
472
- expect(collector.metrics_count[:decisions]).to eq(1)
473
- end
474
-
475
- it "determines status for low confidence decisions" do
476
- low_conf_decision = double(
477
- "Decision",
478
- decision: "approve",
479
- confidence: 0.2,
480
- evaluations: []
481
- )
482
- collector.record_decision(low_conf_decision, context)
483
- expect(collector.metrics_count[:decisions]).to eq(1)
484
- end
485
- end
486
-
487
- describe "#compute_performance_stats" do
488
- it "computes percentile statistics" do
489
- durations = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
490
- durations.each do |duration|
491
- collector.record_performance(operation: "test", duration_ms: duration, success: true)
492
- end
493
-
494
- stats = collector.statistics
495
- expect(stats[:performance][:p95_duration_ms]).to be >= 90
496
- expect(stats[:performance][:p99_duration_ms]).to be >= 95
497
- end
498
- end
499
- end