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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. 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