decision_agent 0.1.2 → 0.1.4

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +212 -35
  3. data/bin/decision_agent +3 -8
  4. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  5. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  6. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  7. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +152 -0
  8. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  9. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  10. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  11. data/lib/decision_agent/agent.rb +19 -26
  12. data/lib/decision_agent/audit/null_adapter.rb +1 -2
  13. data/lib/decision_agent/decision.rb +3 -1
  14. data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
  15. data/lib/decision_agent/dsl/rule_parser.rb +4 -6
  16. data/lib/decision_agent/dsl/schema_validator.rb +27 -31
  17. data/lib/decision_agent/errors.rb +11 -8
  18. data/lib/decision_agent/evaluation.rb +3 -1
  19. data/lib/decision_agent/evaluation_validator.rb +78 -0
  20. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
  21. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
  22. data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
  23. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
  24. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
  25. data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
  26. data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
  27. data/lib/decision_agent/monitoring/metrics_collector.rb +423 -0
  28. data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
  29. data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
  30. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  31. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  32. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  33. data/lib/decision_agent/replay/replay.rb +12 -22
  34. data/lib/decision_agent/scoring/base.rb +1 -1
  35. data/lib/decision_agent/scoring/consensus.rb +5 -5
  36. data/lib/decision_agent/scoring/weighted_average.rb +1 -1
  37. data/lib/decision_agent/version.rb +1 -1
  38. data/lib/decision_agent/versioning/activerecord_adapter.rb +69 -33
  39. data/lib/decision_agent/versioning/adapter.rb +1 -3
  40. data/lib/decision_agent/versioning/file_storage_adapter.rb +143 -35
  41. data/lib/decision_agent/versioning/version_manager.rb +4 -12
  42. data/lib/decision_agent/web/public/index.html +1 -1
  43. data/lib/decision_agent/web/server.rb +19 -24
  44. data/lib/decision_agent.rb +14 -0
  45. data/lib/generators/decision_agent/install/install_generator.rb +42 -5
  46. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  47. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  48. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  49. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  50. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  51. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  52. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  53. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  54. data/lib/generators/decision_agent/install/templates/migration.rb +17 -6
  55. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  56. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  57. data/lib/generators/decision_agent/install/templates/rule.rb +3 -3
  58. data/lib/generators/decision_agent/install/templates/rule_version.rb +13 -7
  59. data/spec/ab_testing/ab_test_manager_spec.rb +330 -0
  60. data/spec/ab_testing/ab_test_spec.rb +270 -0
  61. data/spec/activerecord_thread_safety_spec.rb +553 -0
  62. data/spec/agent_spec.rb +13 -13
  63. data/spec/api_contract_spec.rb +16 -16
  64. data/spec/audit_adapters_spec.rb +3 -3
  65. data/spec/comprehensive_edge_cases_spec.rb +86 -86
  66. data/spec/dsl_validation_spec.rb +83 -83
  67. data/spec/edge_cases_spec.rb +23 -23
  68. data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
  69. data/spec/examples.txt +612 -0
  70. data/spec/issue_verification_spec.rb +759 -0
  71. data/spec/json_rule_evaluator_spec.rb +15 -15
  72. data/spec/monitoring/alert_manager_spec.rb +378 -0
  73. data/spec/monitoring/metrics_collector_spec.rb +281 -0
  74. data/spec/monitoring/monitored_agent_spec.rb +222 -0
  75. data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
  76. data/spec/monitoring/storage/activerecord_adapter_spec.rb +346 -0
  77. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  78. data/spec/replay_edge_cases_spec.rb +58 -58
  79. data/spec/replay_spec.rb +11 -11
  80. data/spec/rfc8785_canonicalization_spec.rb +215 -0
  81. data/spec/scoring_spec.rb +1 -1
  82. data/spec/spec_helper.rb +9 -0
  83. data/spec/thread_safety_spec.rb +482 -0
  84. data/spec/thread_safety_spec.rb.broken +878 -0
  85. data/spec/versioning_spec.rb +141 -37
  86. data/spec/web_ui_rack_spec.rb +135 -0
  87. metadata +93 -6
@@ -0,0 +1,242 @@
1
+ require "spec_helper"
2
+ require "decision_agent/monitoring/metrics_collector"
3
+ require "decision_agent/monitoring/prometheus_exporter"
4
+
5
+ RSpec.describe DecisionAgent::Monitoring::PrometheusExporter do
6
+ let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(storage: :memory) }
7
+ let(:exporter) { described_class.new(metrics_collector: collector, namespace: "test") }
8
+
9
+ let(:decision) do
10
+ double(
11
+ "Decision",
12
+ decision: "approve",
13
+ confidence: 0.85,
14
+ evaluations: [double("Evaluation", evaluator_name: "test_evaluator")]
15
+ )
16
+ end
17
+ let(:context) { double("Context", to_h: { user: "test" }) }
18
+
19
+ describe "#initialize" do
20
+ it "initializes with metrics collector" do
21
+ expect(exporter).to be_a(described_class)
22
+ end
23
+
24
+ it "uses default namespace" do
25
+ exporter = described_class.new(metrics_collector: collector)
26
+ output = exporter.export
27
+ expect(output).to include("decision_agent_")
28
+ end
29
+
30
+ it "uses custom namespace" do
31
+ output = exporter.export
32
+ expect(output).to include("test_")
33
+ end
34
+ end
35
+
36
+ describe "#export" do
37
+ before do
38
+ # Record some metrics
39
+ 3.times { collector.record_decision(decision, context, duration_ms: 10.0) }
40
+ collector.record_performance(operation: "decide", duration_ms: 15.0, success: true)
41
+ collector.record_error(StandardError.new("Test error"))
42
+ end
43
+
44
+ it "exports in Prometheus text format" do
45
+ output = exporter.export
46
+
47
+ expect(output).to be_a(String)
48
+ expect(output).to include("# DecisionAgent Metrics Export")
49
+ end
50
+
51
+ it "includes decision metrics" do
52
+ output = exporter.export
53
+
54
+ expect(output).to include("# HELP test_decisions_total")
55
+ expect(output).to include("# TYPE test_decisions_total counter")
56
+ expect(output).to include("test_decisions_total 3")
57
+ end
58
+
59
+ it "includes confidence metrics" do
60
+ output = exporter.export
61
+
62
+ expect(output).to include("# HELP test_decision_confidence_avg")
63
+ expect(output).to include("# TYPE test_decision_confidence_avg gauge")
64
+ expect(output).to include("test_decision_confidence_avg 0.85")
65
+ end
66
+
67
+ it "includes performance metrics" do
68
+ output = exporter.export
69
+
70
+ expect(output).to include("# HELP test_success_rate")
71
+ expect(output).to include("# TYPE test_success_rate gauge")
72
+ end
73
+
74
+ it "includes error metrics" do
75
+ output = exporter.export
76
+
77
+ expect(output).to include("# HELP test_errors_total")
78
+ expect(output).to include("# TYPE test_errors_total counter")
79
+ expect(output).to include("test_errors_total 1")
80
+ end
81
+
82
+ it "includes system info" do
83
+ output = exporter.export
84
+
85
+ expect(output).to include("# HELP test_info")
86
+ expect(output).to include("# TYPE test_info gauge")
87
+ expect(output).to include("version=\"#{DecisionAgent::VERSION}\"")
88
+ end
89
+
90
+ it "includes decision distribution" do
91
+ output = exporter.export
92
+
93
+ expect(output).to include("# HELP test_decisions_by_type")
94
+ expect(output).to include("test_decisions_by_type{decision=\"approve\"} 3")
95
+ end
96
+
97
+ it "includes error distribution by type" do
98
+ output = exporter.export
99
+
100
+ expect(output).to include("# HELP test_errors_by_type")
101
+ expect(output).to include("test_errors_by_type{error=\"StandardError\"} 1")
102
+ end
103
+
104
+ it "includes metrics count" do
105
+ output = exporter.export
106
+
107
+ expect(output).to include("# HELP test_metrics_stored")
108
+ expect(output).to include("test_metrics_stored{type=\"decisions\"} 3")
109
+ expect(output).to include("test_metrics_stored{type=\"errors\"} 1")
110
+ end
111
+ end
112
+
113
+ describe "#register_kpi" do
114
+ it "registers a custom KPI" do
115
+ exporter.register_kpi(
116
+ name: "custom_metric",
117
+ value: 42.5,
118
+ help: "A custom metric"
119
+ )
120
+
121
+ output = exporter.export
122
+ expect(output).to include("# HELP test_custom_metric A custom metric")
123
+ expect(output).to include("# TYPE test_custom_metric gauge")
124
+ expect(output).to include("test_custom_metric 42.5")
125
+ end
126
+
127
+ it "registers KPI with labels" do
128
+ exporter.register_kpi(
129
+ name: "requests",
130
+ value: 100,
131
+ labels: { endpoint: "/api/v1", method: "GET" }
132
+ )
133
+
134
+ output = exporter.export
135
+ expect(output).to include("test_requests{endpoint=\"/api/v1\",method=\"GET\"} 100")
136
+ end
137
+
138
+ it "sanitizes metric names" do
139
+ exporter.register_kpi(name: "my-custom.metric!", value: 10)
140
+
141
+ output = exporter.export
142
+ expect(output).to include("test_my_custom_metric_")
143
+ end
144
+
145
+ it "escapes label values" do
146
+ exporter.register_kpi(
147
+ name: "metric",
148
+ value: 1,
149
+ labels: { message: 'Contains "quotes"' }
150
+ )
151
+
152
+ output = exporter.export
153
+ expect(output).to include('message="Contains \"quotes\""')
154
+ end
155
+ end
156
+
157
+ describe "#metrics_hash" do
158
+ before do
159
+ collector.record_decision(decision, context, duration_ms: 10.0)
160
+ end
161
+
162
+ it "returns metrics as hash" do
163
+ metrics = exporter.metrics_hash
164
+
165
+ expect(metrics).to be_a(Hash)
166
+ expect(metrics).to have_key(:decisions)
167
+ expect(metrics).to have_key(:performance)
168
+ expect(metrics).to have_key(:errors)
169
+ expect(metrics).to have_key(:system)
170
+ end
171
+
172
+ it "includes metric types" do
173
+ metrics = exporter.metrics_hash
174
+
175
+ expect(metrics[:decisions][:total][:type]).to eq("counter")
176
+ expect(metrics[:decisions][:avg_confidence][:type]).to eq("gauge")
177
+ end
178
+
179
+ it "includes metric values" do
180
+ metrics = exporter.metrics_hash
181
+
182
+ expect(metrics[:decisions][:total][:value]).to eq(1)
183
+ expect(metrics[:decisions][:avg_confidence][:value]).to eq(0.85)
184
+ end
185
+ end
186
+
187
+ describe "thread safety" do
188
+ it "handles concurrent KPI registration" do
189
+ threads = 10.times.map do |i|
190
+ Thread.new do
191
+ 10.times do |j|
192
+ exporter.register_kpi(
193
+ name: "metric_#{i}_#{j}",
194
+ value: (i * 10) + j
195
+ )
196
+ end
197
+ end
198
+ end
199
+
200
+ expect { threads.each(&:join) }.not_to raise_error
201
+ end
202
+
203
+ it "handles concurrent exports" do
204
+ threads = 5.times.map do
205
+ Thread.new do
206
+ 10.times { exporter.export }
207
+ end
208
+ end
209
+
210
+ expect { threads.each(&:join) }.not_to raise_error
211
+ end
212
+ end
213
+
214
+ describe "performance metrics export" do
215
+ before do
216
+ 5.times do |i|
217
+ collector.record_performance(
218
+ operation: "decide",
219
+ duration_ms: (i + 1) * 10.0,
220
+ success: true
221
+ )
222
+ end
223
+ end
224
+
225
+ it "exports summary metrics" do
226
+ output = exporter.export
227
+
228
+ expect(output).to include("# TYPE test_operation_duration_ms summary")
229
+ expect(output).to include("test_operation_duration_ms{quantile=\"0.5\"}")
230
+ expect(output).to include("test_operation_duration_ms{quantile=\"0.95\"}")
231
+ expect(output).to include("test_operation_duration_ms{quantile=\"0.99\"}")
232
+ expect(output).to include("test_operation_duration_ms_sum")
233
+ expect(output).to include("test_operation_duration_ms_count")
234
+ end
235
+ end
236
+
237
+ describe "content type" do
238
+ it "defines Prometheus content type" do
239
+ expect(described_class::CONTENT_TYPE).to eq("text/plain; version=0.0.4")
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "active_record"
5
+ require "decision_agent/monitoring/storage/activerecord_adapter"
6
+
7
+ RSpec.describe DecisionAgent::Monitoring::Storage::ActiveRecordAdapter do
8
+ # Setup in-memory SQLite database for testing
9
+ before(:all) do
10
+ ActiveRecord::Base.establish_connection(
11
+ adapter: "sqlite3",
12
+ database: ":memory:"
13
+ )
14
+
15
+ # Create tables
16
+ ActiveRecord::Schema.define do
17
+ create_table :decision_logs, force: true do |t|
18
+ t.string :decision, null: false
19
+ t.float :confidence
20
+ t.integer :evaluations_count, default: 0
21
+ t.float :duration_ms
22
+ t.string :status
23
+ t.text :context
24
+ t.text :metadata
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :evaluation_metrics, force: true do |t|
29
+ t.references :decision_log, foreign_key: true
30
+ t.string :evaluator_name, null: false
31
+ t.float :score
32
+ t.boolean :success
33
+ t.float :duration_ms
34
+ t.text :details
35
+ t.timestamps
36
+ end
37
+
38
+ create_table :performance_metrics, force: true do |t|
39
+ t.string :operation, null: false
40
+ t.float :duration_ms
41
+ t.string :status
42
+ t.text :metadata
43
+ t.timestamps
44
+ end
45
+
46
+ create_table :error_metrics, force: true do |t|
47
+ t.string :error_type, null: false
48
+ t.text :message
49
+ t.text :stack_trace
50
+ t.string :severity
51
+ t.text :context
52
+ t.timestamps
53
+ end
54
+ end
55
+
56
+ # Define models
57
+ # rubocop:disable Lint/ConstantDefinitionInBlock
58
+ class DecisionLog < ActiveRecord::Base
59
+ has_many :evaluation_metrics, dependent: :destroy
60
+
61
+ scope :recent, ->(time_range) { where("created_at >= ?", Time.now - time_range) }
62
+
63
+ def self.success_rate(time_range: 3600)
64
+ total = recent(time_range).where.not(status: nil).count
65
+ return 0.0 if total.zero?
66
+
67
+ recent(time_range).where(status: "success").count.to_f / total
68
+ end
69
+
70
+ def parsed_context
71
+ JSON.parse(context, symbolize_names: true)
72
+ rescue StandardError
73
+ {}
74
+ end
75
+ end
76
+
77
+ class EvaluationMetric < ActiveRecord::Base
78
+ belongs_to :decision_log, optional: true
79
+
80
+ scope :recent, ->(time_range) { where("created_at >= ?", Time.now - time_range) }
81
+ scope :successful, -> { where(success: true) }
82
+
83
+ def parsed_details
84
+ JSON.parse(details, symbolize_names: true)
85
+ rescue StandardError
86
+ {}
87
+ end
88
+ end
89
+
90
+ class PerformanceMetric < ActiveRecord::Base
91
+ scope :recent, ->(time_range) { where("created_at >= ?", Time.now - time_range) }
92
+
93
+ def self.average_duration(time_range: 3600)
94
+ recent(time_range).average(:duration_ms).to_f
95
+ end
96
+
97
+ def self.p50(time_range: 3600)
98
+ percentile(0.50, time_range: time_range)
99
+ end
100
+
101
+ def self.p95(time_range: 3600)
102
+ percentile(0.95, time_range: time_range)
103
+ end
104
+
105
+ def self.p99(time_range: 3600)
106
+ percentile(0.99, time_range: time_range)
107
+ end
108
+
109
+ def self.percentile(pct, time_range: 3600)
110
+ durations = recent(time_range).where.not(duration_ms: nil).order(:duration_ms).pluck(:duration_ms)
111
+ return 0.0 if durations.empty?
112
+
113
+ durations[(durations.length * pct).ceil - 1].to_f
114
+ end
115
+
116
+ def self.success_rate(time_range: 3600)
117
+ total = recent(time_range).where.not(status: nil).count
118
+ return 0.0 if total.zero?
119
+
120
+ recent(time_range).where(status: "success").count.to_f / total
121
+ end
122
+ end
123
+
124
+ class ErrorMetric < ActiveRecord::Base
125
+ scope :recent, ->(time_range) { where("created_at >= ?", Time.now - time_range) }
126
+ scope :critical, -> { where(severity: "critical") }
127
+
128
+ def parsed_context
129
+ JSON.parse(context, symbolize_names: true)
130
+ rescue StandardError
131
+ {}
132
+ end
133
+ end
134
+ # rubocop:enable Lint/ConstantDefinitionInBlock
135
+ end
136
+
137
+ before do
138
+ DecisionLog.delete_all
139
+ EvaluationMetric.delete_all
140
+ PerformanceMetric.delete_all
141
+ ErrorMetric.delete_all
142
+ end
143
+
144
+ let(:adapter) { described_class.new }
145
+
146
+ describe ".available?" do
147
+ it "returns true when ActiveRecord and models are defined" do
148
+ expect(described_class.available?).to be_truthy
149
+ end
150
+ end
151
+
152
+ describe "#record_decision" do
153
+ it "creates a decision log record" do
154
+ expect do
155
+ adapter.record_decision(
156
+ "approve_payment",
157
+ { user_id: 123, amount: 500 },
158
+ confidence: 0.85,
159
+ evaluations_count: 3,
160
+ duration_ms: 45.5,
161
+ status: "success"
162
+ )
163
+ end.to change(DecisionLog, :count).by(1)
164
+
165
+ log = DecisionLog.last
166
+ expect(log.decision).to eq("approve_payment")
167
+ expect(log.confidence).to eq(0.85)
168
+ expect(log.evaluations_count).to eq(3)
169
+ expect(log.duration_ms).to eq(45.5)
170
+ expect(log.status).to eq("success")
171
+ expect(log.parsed_context).to eq(user_id: 123, amount: 500)
172
+ end
173
+ end
174
+
175
+ describe "#record_evaluation" do
176
+ it "creates an evaluation metric record" do
177
+ expect do
178
+ adapter.record_evaluation(
179
+ "FraudDetector",
180
+ score: 0.92,
181
+ success: true,
182
+ duration_ms: 12.3,
183
+ details: { risk_level: "low" }
184
+ )
185
+ end.to change(EvaluationMetric, :count).by(1)
186
+
187
+ metric = EvaluationMetric.last
188
+ expect(metric.evaluator_name).to eq("FraudDetector")
189
+ expect(metric.score).to eq(0.92)
190
+ expect(metric.success).to be true
191
+ expect(metric.duration_ms).to eq(12.3)
192
+ expect(metric.parsed_details).to eq(risk_level: "low")
193
+ end
194
+ end
195
+
196
+ describe "#record_performance" do
197
+ it "creates a performance metric record" do
198
+ expect do
199
+ adapter.record_performance(
200
+ "api_call",
201
+ duration_ms: 250.5,
202
+ status: "success",
203
+ metadata: { endpoint: "/api/v1/users" }
204
+ )
205
+ end.to change(PerformanceMetric, :count).by(1)
206
+
207
+ metric = PerformanceMetric.last
208
+ expect(metric.operation).to eq("api_call")
209
+ expect(metric.duration_ms).to eq(250.5)
210
+ expect(metric.status).to eq("success")
211
+ end
212
+ end
213
+
214
+ describe "#record_error" do
215
+ it "creates an error metric record" do
216
+ expect do
217
+ adapter.record_error(
218
+ "RuntimeError",
219
+ message: "Something went wrong",
220
+ stack_trace: ["line 1", "line 2"],
221
+ severity: "critical",
222
+ context: { user_id: 456 }
223
+ )
224
+ end.to change(ErrorMetric, :count).by(1)
225
+
226
+ error = ErrorMetric.last
227
+ expect(error.error_type).to eq("RuntimeError")
228
+ expect(error.message).to eq("Something went wrong")
229
+ expect(error.severity).to eq("critical")
230
+ expect(error.parsed_context).to eq(user_id: 456)
231
+ end
232
+ end
233
+
234
+ describe "#statistics" do
235
+ before do
236
+ # Create test data
237
+ 3.times do |i|
238
+ adapter.record_decision(
239
+ "decision_#{i}",
240
+ { index: i },
241
+ confidence: 0.5 + (i * 0.1),
242
+ evaluations_count: 2,
243
+ duration_ms: 100 + (i * 10),
244
+ status: "success"
245
+ )
246
+ end
247
+
248
+ 2.times do |i|
249
+ adapter.record_evaluation(
250
+ "Evaluator#{i}",
251
+ score: 0.8,
252
+ success: true,
253
+ duration_ms: 50
254
+ )
255
+ end
256
+
257
+ 4.times do |i|
258
+ adapter.record_performance(
259
+ "operation_#{i}",
260
+ duration_ms: 100 + (i * 50),
261
+ status: i.even? ? "success" : "failure"
262
+ )
263
+ end
264
+
265
+ adapter.record_error("TestError", severity: "critical")
266
+ end
267
+
268
+ it "returns comprehensive statistics" do
269
+ stats = adapter.statistics(time_range: 3600)
270
+
271
+ expect(stats[:decisions][:total]).to eq(3)
272
+ expect(stats[:decisions][:average_confidence]).to be_within(0.01).of(0.6)
273
+ expect(stats[:evaluations][:total]).to eq(2)
274
+ expect(stats[:performance][:total]).to eq(4)
275
+ expect(stats[:errors][:total]).to eq(1)
276
+ expect(stats[:errors][:critical_count]).to eq(1)
277
+ end
278
+ end
279
+
280
+ describe "#time_series" do
281
+ before do
282
+ # Create metrics at different times
283
+ [10, 70, 130].each do |seconds_ago|
284
+ travel_back = Time.now - seconds_ago
285
+ DecisionLog.create!(
286
+ decision: "test",
287
+ confidence: 0.8,
288
+ created_at: travel_back
289
+ )
290
+ end
291
+ end
292
+
293
+ it "returns time series data grouped by buckets" do
294
+ series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
295
+
296
+ expect(series[:timestamps]).to be_an(Array)
297
+ expect(series[:data]).to be_an(Array)
298
+ expect(series[:data].sum).to eq(3)
299
+ end
300
+ end
301
+
302
+ describe "#metrics_count" do
303
+ before do
304
+ adapter.record_decision("test", {}, confidence: 0.8)
305
+ adapter.record_evaluation("TestEval", score: 0.9)
306
+ adapter.record_performance("test_op", duration_ms: 100)
307
+ adapter.record_error("TestError")
308
+ end
309
+
310
+ it "returns count of all metric types" do
311
+ counts = adapter.metrics_count
312
+
313
+ expect(counts[:decisions]).to eq(1)
314
+ expect(counts[:evaluations]).to eq(1)
315
+ expect(counts[:performance]).to eq(1)
316
+ expect(counts[:errors]).to eq(1)
317
+ end
318
+ end
319
+
320
+ describe "#cleanup" do
321
+ before do
322
+ # Create old metrics
323
+ old_time = Time.now - 8.days
324
+ DecisionLog.create!(decision: "old", confidence: 0.8, created_at: old_time)
325
+ EvaluationMetric.create!(evaluator_name: "old", created_at: old_time)
326
+ PerformanceMetric.create!(operation: "old", created_at: old_time)
327
+ ErrorMetric.create!(error_type: "old", created_at: old_time)
328
+
329
+ # Create recent metrics
330
+ adapter.record_decision("recent", {}, confidence: 0.8)
331
+ adapter.record_evaluation("recent", score: 0.9)
332
+ adapter.record_performance("recent", duration_ms: 100)
333
+ adapter.record_error("recent")
334
+ end
335
+
336
+ it "removes old metrics and keeps recent ones" do
337
+ count = adapter.cleanup(older_than: 7.days.to_i)
338
+
339
+ expect(count).to eq(4) # 4 old metrics removed
340
+ expect(DecisionLog.count).to eq(1)
341
+ expect(EvaluationMetric.count).to eq(1)
342
+ expect(PerformanceMetric.count).to eq(1)
343
+ expect(ErrorMetric.count).to eq(1)
344
+ end
345
+ end
346
+ end