decision_agent 0.1.4 → 0.1.6
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 +84 -233
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +52 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -612
- data/spec/monitoring/metrics_collector_spec.rb +220 -2
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- metadata +99 -6
|
@@ -266,7 +266,7 @@ RSpec.describe DecisionAgent::Monitoring::MetricsCollector do
|
|
|
266
266
|
|
|
267
267
|
describe "metric cleanup" do
|
|
268
268
|
it "removes old metrics outside window" do
|
|
269
|
-
collector = described_class.new(window_size: 1, storage: :memory)
|
|
269
|
+
collector = described_class.new(window_size: 1, storage: :memory, cleanup_threshold: 1)
|
|
270
270
|
|
|
271
271
|
collector.record_decision(decision, context)
|
|
272
272
|
expect(collector.metrics_count[:decisions]).to eq(1)
|
|
@@ -274,8 +274,226 @@ RSpec.describe DecisionAgent::Monitoring::MetricsCollector do
|
|
|
274
274
|
sleep 1.5
|
|
275
275
|
|
|
276
276
|
collector.record_decision(decision, context)
|
|
277
|
-
# Old metric should be cleaned up
|
|
277
|
+
# Old metric should be cleaned up (threshold=1 means cleanup on every record)
|
|
278
278
|
expect(collector.metrics_count[:decisions]).to eq(1)
|
|
279
279
|
end
|
|
280
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
|
|
281
499
|
end
|
|
@@ -170,6 +170,13 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
170
170
|
expect(log.status).to eq("success")
|
|
171
171
|
expect(log.parsed_context).to eq(user_id: 123, amount: 500)
|
|
172
172
|
end
|
|
173
|
+
|
|
174
|
+
it "handles database errors gracefully" do
|
|
175
|
+
allow(DecisionLog).to receive(:create!).and_raise(StandardError.new("DB error"))
|
|
176
|
+
expect do
|
|
177
|
+
adapter.record_decision("test", {})
|
|
178
|
+
end.not_to raise_error
|
|
179
|
+
end
|
|
173
180
|
end
|
|
174
181
|
|
|
175
182
|
describe "#record_evaluation" do
|
|
@@ -191,6 +198,13 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
191
198
|
expect(metric.duration_ms).to eq(12.3)
|
|
192
199
|
expect(metric.parsed_details).to eq(risk_level: "low")
|
|
193
200
|
end
|
|
201
|
+
|
|
202
|
+
it "handles database errors gracefully" do
|
|
203
|
+
allow(EvaluationMetric).to receive(:create!).and_raise(StandardError.new("DB error"))
|
|
204
|
+
expect do
|
|
205
|
+
adapter.record_evaluation("test")
|
|
206
|
+
end.not_to raise_error
|
|
207
|
+
end
|
|
194
208
|
end
|
|
195
209
|
|
|
196
210
|
describe "#record_performance" do
|
|
@@ -209,6 +223,13 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
209
223
|
expect(metric.duration_ms).to eq(250.5)
|
|
210
224
|
expect(metric.status).to eq("success")
|
|
211
225
|
end
|
|
226
|
+
|
|
227
|
+
it "handles database errors gracefully" do
|
|
228
|
+
allow(PerformanceMetric).to receive(:create!).and_raise(StandardError.new("DB error"))
|
|
229
|
+
expect do
|
|
230
|
+
adapter.record_performance("test")
|
|
231
|
+
end.not_to raise_error
|
|
232
|
+
end
|
|
212
233
|
end
|
|
213
234
|
|
|
214
235
|
describe "#record_error" do
|
|
@@ -229,6 +250,19 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
229
250
|
expect(error.severity).to eq("critical")
|
|
230
251
|
expect(error.parsed_context).to eq(user_id: 456)
|
|
231
252
|
end
|
|
253
|
+
|
|
254
|
+
it "handles nil stack_trace" do
|
|
255
|
+
adapter.record_error("TestError", stack_trace: nil)
|
|
256
|
+
error = ErrorMetric.last
|
|
257
|
+
expect(error.stack_trace).to be_nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "handles database errors gracefully" do
|
|
261
|
+
allow(ErrorMetric).to receive(:create!).and_raise(StandardError.new("DB error"))
|
|
262
|
+
expect do
|
|
263
|
+
adapter.record_error("test")
|
|
264
|
+
end.not_to raise_error
|
|
265
|
+
end
|
|
232
266
|
end
|
|
233
267
|
|
|
234
268
|
describe "#statistics" do
|
|
@@ -275,6 +309,37 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
275
309
|
expect(stats[:errors][:total]).to eq(1)
|
|
276
310
|
expect(stats[:errors][:critical_count]).to eq(1)
|
|
277
311
|
end
|
|
312
|
+
|
|
313
|
+
it "handles empty statistics" do
|
|
314
|
+
DecisionLog.delete_all
|
|
315
|
+
EvaluationMetric.delete_all
|
|
316
|
+
PerformanceMetric.delete_all
|
|
317
|
+
ErrorMetric.delete_all
|
|
318
|
+
|
|
319
|
+
stats = adapter.statistics(time_range: 3600)
|
|
320
|
+
|
|
321
|
+
expect(stats[:decisions][:total]).to eq(0)
|
|
322
|
+
expect(stats[:decisions][:average_confidence]).to eq(0.0)
|
|
323
|
+
expect(stats[:evaluations][:total]).to eq(0)
|
|
324
|
+
expect(stats[:performance][:total]).to eq(0)
|
|
325
|
+
expect(stats[:errors][:total]).to eq(0)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "handles decisions without confidence" do
|
|
329
|
+
DecisionLog.delete_all
|
|
330
|
+
adapter.record_decision("test", {}, confidence: nil)
|
|
331
|
+
|
|
332
|
+
stats = adapter.statistics(time_range: 3600)
|
|
333
|
+
expect(stats[:decisions][:average_confidence]).to eq(0.0)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it "handles database errors gracefully" do
|
|
337
|
+
allow(DecisionLog).to receive(:recent).and_raise(StandardError.new("DB error"))
|
|
338
|
+
stats = adapter.statistics(time_range: 3600)
|
|
339
|
+
|
|
340
|
+
expect(stats[:decisions][:total]).to eq(0)
|
|
341
|
+
expect(stats[:evaluations][:total]).to eq(0)
|
|
342
|
+
end
|
|
278
343
|
end
|
|
279
344
|
|
|
280
345
|
describe "#time_series" do
|
|
@@ -290,13 +355,77 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
290
355
|
end
|
|
291
356
|
end
|
|
292
357
|
|
|
293
|
-
it "returns time series data grouped by buckets" do
|
|
358
|
+
it "returns time series data grouped by buckets for decisions" do
|
|
294
359
|
series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
|
|
295
360
|
|
|
296
361
|
expect(series[:timestamps]).to be_an(Array)
|
|
297
362
|
expect(series[:data]).to be_an(Array)
|
|
298
363
|
expect(series[:data].sum).to eq(3)
|
|
299
364
|
end
|
|
365
|
+
|
|
366
|
+
it "returns time series data for evaluations" do
|
|
367
|
+
[10, 70].each do |seconds_ago|
|
|
368
|
+
travel_back = Time.now - seconds_ago
|
|
369
|
+
EvaluationMetric.create!(
|
|
370
|
+
evaluator_name: "test",
|
|
371
|
+
score: 0.8,
|
|
372
|
+
created_at: travel_back
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
series = adapter.time_series(:evaluations, bucket_size: 60, time_range: 200)
|
|
377
|
+
|
|
378
|
+
expect(series[:timestamps]).to be_an(Array)
|
|
379
|
+
expect(series[:data]).to be_an(Array)
|
|
380
|
+
expect(series[:data].sum).to eq(2)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it "returns time series data for performance" do
|
|
384
|
+
[10, 70].each do |seconds_ago|
|
|
385
|
+
travel_back = Time.now - seconds_ago
|
|
386
|
+
PerformanceMetric.create!(
|
|
387
|
+
operation: "test",
|
|
388
|
+
duration_ms: 100,
|
|
389
|
+
created_at: travel_back
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
series = adapter.time_series(:performance, bucket_size: 60, time_range: 200)
|
|
394
|
+
|
|
395
|
+
expect(series[:timestamps]).to be_an(Array)
|
|
396
|
+
expect(series[:data]).to be_an(Array)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it "returns time series data for errors" do
|
|
400
|
+
[10, 70].each do |seconds_ago|
|
|
401
|
+
travel_back = Time.now - seconds_ago
|
|
402
|
+
ErrorMetric.create!(
|
|
403
|
+
error_type: "TestError",
|
|
404
|
+
created_at: travel_back
|
|
405
|
+
)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
series = adapter.time_series(:errors, bucket_size: 60, time_range: 200)
|
|
409
|
+
|
|
410
|
+
expect(series[:timestamps]).to be_an(Array)
|
|
411
|
+
expect(series[:data]).to be_an(Array)
|
|
412
|
+
expect(series[:data].sum).to eq(2)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "returns empty data for unknown metric type" do
|
|
416
|
+
series = adapter.time_series(:unknown, bucket_size: 60, time_range: 200)
|
|
417
|
+
|
|
418
|
+
expect(series[:timestamps]).to eq([])
|
|
419
|
+
expect(series[:data]).to eq([])
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it "handles database errors gracefully" do
|
|
423
|
+
allow(DecisionLog).to receive(:recent).and_raise(StandardError.new("DB error"))
|
|
424
|
+
series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
|
|
425
|
+
|
|
426
|
+
expect(series[:timestamps]).to eq([])
|
|
427
|
+
expect(series[:data]).to eq([])
|
|
428
|
+
end
|
|
300
429
|
end
|
|
301
430
|
|
|
302
431
|
describe "#metrics_count" do
|
|
@@ -315,6 +444,16 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
315
444
|
expect(counts[:performance]).to eq(1)
|
|
316
445
|
expect(counts[:errors]).to eq(1)
|
|
317
446
|
end
|
|
447
|
+
|
|
448
|
+
it "handles database errors gracefully" do
|
|
449
|
+
allow(DecisionLog).to receive(:count).and_raise(StandardError.new("DB error"))
|
|
450
|
+
counts = adapter.metrics_count
|
|
451
|
+
|
|
452
|
+
expect(counts[:decisions]).to eq(0)
|
|
453
|
+
expect(counts[:evaluations]).to eq(0)
|
|
454
|
+
expect(counts[:performance]).to eq(0)
|
|
455
|
+
expect(counts[:errors]).to eq(0)
|
|
456
|
+
end
|
|
318
457
|
end
|
|
319
458
|
|
|
320
459
|
describe "#cleanup" do
|
|
@@ -342,5 +481,18 @@ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
|
|
|
342
481
|
expect(PerformanceMetric.count).to eq(1)
|
|
343
482
|
expect(ErrorMetric.count).to eq(1)
|
|
344
483
|
end
|
|
484
|
+
|
|
485
|
+
it "handles database errors gracefully" do
|
|
486
|
+
allow(DecisionLog).to receive(:where).and_raise(StandardError.new("DB error"))
|
|
487
|
+
count = adapter.cleanup(older_than: 7.days.to_i)
|
|
488
|
+
|
|
489
|
+
expect(count).to eq(0)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
describe "#initialize" do
|
|
494
|
+
it "validates required models exist" do
|
|
495
|
+
expect { described_class.new }.not_to raise_error
|
|
496
|
+
end
|
|
345
497
|
end
|
|
346
498
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe DecisionAgent::Monitoring::Storage::BaseAdapter do
|
|
4
|
+
let(:adapter) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe "abstract methods" do
|
|
7
|
+
it "raises NotImplementedError for record_decision" do
|
|
8
|
+
expect do
|
|
9
|
+
adapter.record_decision("approve", {})
|
|
10
|
+
end.to raise_error(NotImplementedError)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "raises NotImplementedError for record_evaluation" do
|
|
14
|
+
expect do
|
|
15
|
+
adapter.record_evaluation("evaluator1")
|
|
16
|
+
end.to raise_error(NotImplementedError)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "raises NotImplementedError for record_performance" do
|
|
20
|
+
expect do
|
|
21
|
+
adapter.record_performance("operation")
|
|
22
|
+
end.to raise_error(NotImplementedError)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "raises NotImplementedError for record_error" do
|
|
26
|
+
expect do
|
|
27
|
+
adapter.record_error("ErrorType")
|
|
28
|
+
end.to raise_error(NotImplementedError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "raises NotImplementedError for statistics" do
|
|
32
|
+
expect do
|
|
33
|
+
adapter.statistics
|
|
34
|
+
end.to raise_error(NotImplementedError)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "raises NotImplementedError for time_series" do
|
|
38
|
+
expect do
|
|
39
|
+
adapter.time_series(:decisions)
|
|
40
|
+
end.to raise_error(NotImplementedError)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "raises NotImplementedError for metrics_count" do
|
|
44
|
+
expect do
|
|
45
|
+
adapter.metrics_count
|
|
46
|
+
end.to raise_error(NotImplementedError)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "raises NotImplementedError for cleanup" do
|
|
50
|
+
expect do
|
|
51
|
+
adapter.cleanup(older_than: 3600)
|
|
52
|
+
end.to raise_error(NotImplementedError)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "raises NotImplementedError for available?" do
|
|
56
|
+
expect do
|
|
57
|
+
described_class.available?
|
|
58
|
+
end.to raise_error(NotImplementedError)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|