decision_agent 0.1.4 → 0.1.7

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 +83 -232
  3. data/bin/decision_agent +1 -1
  4. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
  5. data/lib/decision_agent/agent.rb +5 -3
  6. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  7. data/lib/decision_agent/auth/authenticator.rb +127 -0
  8. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  9. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  10. data/lib/decision_agent/auth/permission.rb +29 -0
  11. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  12. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  13. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  14. data/lib/decision_agent/auth/role.rb +56 -0
  15. data/lib/decision_agent/auth/session.rb +33 -0
  16. data/lib/decision_agent/auth/session_manager.rb +57 -0
  17. data/lib/decision_agent/auth/user.rb +70 -0
  18. data/lib/decision_agent/context.rb +24 -4
  19. data/lib/decision_agent/decision.rb +10 -3
  20. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  21. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  22. data/lib/decision_agent/errors.rb +38 -0
  23. data/lib/decision_agent/evaluation.rb +10 -3
  24. data/lib/decision_agent/evaluation_validator.rb +8 -13
  25. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  26. data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
  27. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  28. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  29. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  30. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  31. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  32. data/lib/decision_agent/version.rb +10 -1
  33. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  34. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  35. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  36. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  37. data/lib/decision_agent/web/public/app.js +184 -29
  38. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  39. data/lib/decision_agent/web/public/index.html +38 -10
  40. data/lib/decision_agent/web/public/login.html +298 -0
  41. data/lib/decision_agent/web/public/users.html +679 -0
  42. data/lib/decision_agent/web/server.rb +873 -7
  43. data/lib/decision_agent.rb +52 -0
  44. data/lib/generators/decision_agent/install/templates/README +1 -1
  45. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  46. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  47. data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
  48. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  49. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  50. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  51. data/spec/advanced_operators_spec.rb +1003 -0
  52. data/spec/agent_spec.rb +40 -0
  53. data/spec/audit_adapters_spec.rb +18 -0
  54. data/spec/auth/access_audit_logger_spec.rb +394 -0
  55. data/spec/auth/authenticator_spec.rb +112 -0
  56. data/spec/auth/password_reset_spec.rb +294 -0
  57. data/spec/auth/permission_checker_spec.rb +207 -0
  58. data/spec/auth/permission_spec.rb +73 -0
  59. data/spec/auth/rbac_adapter_spec.rb +550 -0
  60. data/spec/auth/rbac_config_spec.rb +82 -0
  61. data/spec/auth/role_spec.rb +51 -0
  62. data/spec/auth/session_manager_spec.rb +172 -0
  63. data/spec/auth/session_spec.rb +112 -0
  64. data/spec/auth/user_spec.rb +130 -0
  65. data/spec/context_spec.rb +43 -0
  66. data/spec/decision_agent_spec.rb +96 -0
  67. data/spec/decision_spec.rb +423 -0
  68. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  69. data/spec/evaluation_spec.rb +364 -0
  70. data/spec/evaluation_validator_spec.rb +165 -0
  71. data/spec/monitoring/metrics_collector_spec.rb +220 -2
  72. data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
  73. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  74. data/spec/performance_optimizations_spec.rb +486 -0
  75. data/spec/spec_helper.rb +23 -0
  76. data/spec/testing/batch_test_importer_spec.rb +693 -0
  77. data/spec/testing/batch_test_runner_spec.rb +307 -0
  78. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  79. data/spec/testing/test_result_comparator_spec.rb +392 -0
  80. data/spec/testing/test_scenario_spec.rb +113 -0
  81. data/spec/versioning/adapter_spec.rb +156 -0
  82. data/spec/versioning_spec.rb +253 -0
  83. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  84. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  85. data/spec/web_ui_rack_spec.rb +1705 -0
  86. metadata +103 -11
  87. data/spec/examples.txt +0 -612
@@ -43,5 +43,57 @@ require_relative "decision_agent/ab_testing/ab_testing_agent"
43
43
  require_relative "decision_agent/ab_testing/storage/adapter"
44
44
  require_relative "decision_agent/ab_testing/storage/memory_adapter"
45
45
 
46
+ require_relative "decision_agent/testing/test_scenario"
47
+ require_relative "decision_agent/testing/batch_test_importer"
48
+ require_relative "decision_agent/testing/batch_test_runner"
49
+ require_relative "decision_agent/testing/test_result_comparator"
50
+ require_relative "decision_agent/testing/test_coverage_analyzer"
51
+
52
+ require_relative "decision_agent/auth/user"
53
+ require_relative "decision_agent/auth/role"
54
+ require_relative "decision_agent/auth/permission"
55
+ require_relative "decision_agent/auth/session"
56
+ require_relative "decision_agent/auth/session_manager"
57
+ require_relative "decision_agent/auth/password_reset_token"
58
+ require_relative "decision_agent/auth/password_reset_manager"
59
+ require_relative "decision_agent/auth/authenticator"
60
+ require_relative "decision_agent/auth/rbac_adapter"
61
+ require_relative "decision_agent/auth/rbac_config"
62
+ require_relative "decision_agent/auth/permission_checker"
63
+ require_relative "decision_agent/auth/access_audit_logger"
64
+
46
65
  module DecisionAgent
66
+ # Global RBAC configuration
67
+ @rbac_config = Auth::RbacConfig.new
68
+
69
+ class << self
70
+ attr_reader :rbac_config
71
+
72
+ # Configure RBAC adapter
73
+ # @param adapter_type [Symbol] :default, :devise_cancan, :pundit, or :custom
74
+ # @param options [Hash] Options for the adapter
75
+ # @yield [RbacConfig] Configuration block
76
+ # @example
77
+ # DecisionAgent.configure_rbac(:devise_cancan, ability_class: Ability)
78
+ # @example
79
+ # DecisionAgent.configure_rbac(:custom) do |config|
80
+ # config.adapter = MyCustomAdapter.new
81
+ # end
82
+ def configure_rbac(adapter_type = nil, **options)
83
+ if block_given?
84
+ yield @rbac_config
85
+ elsif adapter_type
86
+ @rbac_config.use(adapter_type, **options)
87
+ end
88
+ @rbac_config
89
+ end
90
+
91
+ # Get the configured permission checker
92
+ def permission_checker
93
+ @permission_checker ||= Auth::PermissionChecker.new(adapter: @rbac_config.adapter)
94
+ end
95
+
96
+ # Set a custom permission checker
97
+ attr_writer :permission_checker
98
+ end
47
99
  end
@@ -42,6 +42,6 @@ Next steps:
42
42
  # mount DecisionAgent::Engine => '/decision_agent'
43
43
 
44
44
  For more information, visit:
45
- https://github.com/samaswin87/decision_agent
45
+ https://github.com/samaswin/decision_agent
46
46
 
47
47
  ===============================================================================
@@ -33,7 +33,7 @@ class RuleVersion < ApplicationRecord
33
33
  self.class.where(rule_id: rule_id, status: "active")
34
34
  .where.not(id: id)
35
35
  .find_each do |v|
36
- v.update!(status: "archived")
36
+ v.update!(status: "archived")
37
37
  end
38
38
 
39
39
  # Activate this version
@@ -0,0 +1,253 @@
1
+ require "spec_helper"
2
+ require "decision_agent/ab_testing/ab_test_assignment"
3
+
4
+ RSpec.describe DecisionAgent::ABTesting::ABTestAssignment do
5
+ describe "#initialize" do
6
+ it "creates an assignment with required fields" do
7
+ assignment = described_class.new(
8
+ ab_test_id: "test_1",
9
+ variant: :champion,
10
+ version_id: "v1"
11
+ )
12
+
13
+ expect(assignment.ab_test_id).to eq("test_1")
14
+ expect(assignment.variant).to eq(:champion)
15
+ expect(assignment.version_id).to eq("v1")
16
+ expect(assignment.timestamp).to be_a(Time)
17
+ end
18
+
19
+ it "accepts optional user_id" do
20
+ assignment = described_class.new(
21
+ ab_test_id: "test_1",
22
+ variant: :champion,
23
+ version_id: "v1",
24
+ user_id: "user_123"
25
+ )
26
+
27
+ expect(assignment.user_id).to eq("user_123")
28
+ end
29
+
30
+ it "accepts optional timestamp" do
31
+ custom_time = Time.new(2024, 1, 1, 12, 0, 0, "+00:00")
32
+ assignment = described_class.new(
33
+ ab_test_id: "test_1",
34
+ variant: :champion,
35
+ version_id: "v1",
36
+ timestamp: custom_time
37
+ )
38
+
39
+ expect(assignment.timestamp).to eq(custom_time)
40
+ end
41
+
42
+ it "accepts optional decision_result and confidence" do
43
+ assignment = described_class.new(
44
+ ab_test_id: "test_1",
45
+ variant: :champion,
46
+ version_id: "v1",
47
+ decision_result: "approve",
48
+ confidence: 0.95
49
+ )
50
+
51
+ expect(assignment.decision_result).to eq("approve")
52
+ expect(assignment.confidence).to eq(0.95)
53
+ end
54
+
55
+ it "accepts optional context" do
56
+ context = { user_type: "premium", region: "us" }
57
+ assignment = described_class.new(
58
+ ab_test_id: "test_1",
59
+ variant: :champion,
60
+ version_id: "v1",
61
+ context: context
62
+ )
63
+
64
+ expect(assignment.context).to eq(context)
65
+ end
66
+
67
+ it "defaults context to empty hash" do
68
+ assignment = described_class.new(
69
+ ab_test_id: "test_1",
70
+ variant: :champion,
71
+ version_id: "v1"
72
+ )
73
+
74
+ expect(assignment.context).to eq({})
75
+ end
76
+
77
+ it "accepts optional id" do
78
+ assignment = described_class.new(
79
+ ab_test_id: "test_1",
80
+ variant: :champion,
81
+ version_id: "v1",
82
+ id: "assign_123"
83
+ )
84
+
85
+ expect(assignment.id).to eq("assign_123")
86
+ end
87
+
88
+ it "raises error if ab_test_id is nil" do
89
+ expect do
90
+ described_class.new(
91
+ ab_test_id: nil,
92
+ variant: :champion,
93
+ version_id: "v1"
94
+ )
95
+ end.to raise_error(DecisionAgent::ValidationError, /AB test ID is required/)
96
+ end
97
+
98
+ it "raises error if variant is nil" do
99
+ expect do
100
+ described_class.new(
101
+ ab_test_id: "test_1",
102
+ variant: nil,
103
+ version_id: "v1"
104
+ )
105
+ end.to raise_error(DecisionAgent::ValidationError, /Variant is required/)
106
+ end
107
+
108
+ it "raises error if version_id is nil" do
109
+ expect do
110
+ described_class.new(
111
+ ab_test_id: "test_1",
112
+ variant: :champion,
113
+ version_id: nil
114
+ )
115
+ end.to raise_error(DecisionAgent::ValidationError, /Version ID is required/)
116
+ end
117
+
118
+ it "raises error if variant is not :champion or :challenger" do
119
+ expect do
120
+ described_class.new(
121
+ ab_test_id: "test_1",
122
+ variant: :invalid,
123
+ version_id: "v1"
124
+ )
125
+ end.to raise_error(DecisionAgent::ValidationError, /Variant must be :champion or :challenger/)
126
+ end
127
+
128
+ it "raises error if confidence is negative" do
129
+ expect do
130
+ described_class.new(
131
+ ab_test_id: "test_1",
132
+ variant: :champion,
133
+ version_id: "v1",
134
+ confidence: -0.1
135
+ )
136
+ end.to raise_error(DecisionAgent::ValidationError, /Confidence must be between 0 and 1/)
137
+ end
138
+
139
+ it "raises error if confidence is greater than 1" do
140
+ expect do
141
+ described_class.new(
142
+ ab_test_id: "test_1",
143
+ variant: :champion,
144
+ version_id: "v1",
145
+ confidence: 1.5
146
+ )
147
+ end.to raise_error(DecisionAgent::ValidationError, /Confidence must be between 0 and 1/)
148
+ end
149
+
150
+ it "accepts confidence of 0" do
151
+ assignment = described_class.new(
152
+ ab_test_id: "test_1",
153
+ variant: :champion,
154
+ version_id: "v1",
155
+ confidence: 0.0
156
+ )
157
+
158
+ expect(assignment.confidence).to eq(0.0)
159
+ end
160
+
161
+ it "accepts confidence of 1" do
162
+ assignment = described_class.new(
163
+ ab_test_id: "test_1",
164
+ variant: :champion,
165
+ version_id: "v1",
166
+ confidence: 1.0
167
+ )
168
+
169
+ expect(assignment.confidence).to eq(1.0)
170
+ end
171
+
172
+ it "accepts challenger variant" do
173
+ assignment = described_class.new(
174
+ ab_test_id: "test_1",
175
+ variant: :challenger,
176
+ version_id: "v2"
177
+ )
178
+
179
+ expect(assignment.variant).to eq(:challenger)
180
+ end
181
+ end
182
+
183
+ describe "#record_decision" do
184
+ let(:assignment) do
185
+ described_class.new(
186
+ ab_test_id: "test_1",
187
+ variant: :champion,
188
+ version_id: "v1"
189
+ )
190
+ end
191
+
192
+ it "updates decision_result and confidence" do
193
+ assignment.record_decision("approve", 0.95)
194
+
195
+ expect(assignment.decision_result).to eq("approve")
196
+ expect(assignment.confidence).to eq(0.95)
197
+ end
198
+
199
+ it "can update multiple times" do
200
+ assignment.record_decision("approve", 0.95)
201
+ assignment.record_decision("reject", 0.85)
202
+
203
+ expect(assignment.decision_result).to eq("reject")
204
+ expect(assignment.confidence).to eq(0.85)
205
+ end
206
+ end
207
+
208
+ describe "#to_h" do
209
+ it "converts assignment to hash with all fields" do
210
+ assignment = described_class.new(
211
+ ab_test_id: "test_1",
212
+ variant: :champion,
213
+ version_id: "v1",
214
+ id: "assign_123",
215
+ user_id: "user_456",
216
+ decision_result: "approve",
217
+ confidence: 0.95,
218
+ context: { region: "us" },
219
+ timestamp: Time.new(2024, 1, 1, 12, 0, 0, "+00:00")
220
+ )
221
+
222
+ hash = assignment.to_h
223
+
224
+ expect(hash).to eq({
225
+ id: "assign_123",
226
+ ab_test_id: "test_1",
227
+ user_id: "user_456",
228
+ variant: :champion,
229
+ version_id: "v1",
230
+ timestamp: Time.new(2024, 1, 1, 12, 0, 0, "+00:00"),
231
+ decision_result: "approve",
232
+ confidence: 0.95,
233
+ context: { region: "us" }
234
+ })
235
+ end
236
+
237
+ it "includes nil values in hash" do
238
+ assignment = described_class.new(
239
+ ab_test_id: "test_1",
240
+ variant: :champion,
241
+ version_id: "v1"
242
+ )
243
+
244
+ hash = assignment.to_h
245
+
246
+ expect(hash[:id]).to be_nil
247
+ expect(hash[:user_id]).to be_nil
248
+ expect(hash[:decision_result]).to be_nil
249
+ expect(hash[:confidence]).to be_nil
250
+ expect(hash[:context]).to eq({})
251
+ end
252
+ end
253
+ end
@@ -75,6 +75,32 @@ RSpec.describe DecisionAgent::ABTesting::ABTestManager do
75
75
 
76
76
  expect(test.traffic_split).to eq({ champion: 70, challenger: 30 })
77
77
  end
78
+
79
+ it "accepts start_date and end_date" do
80
+ start_date = Time.now.utc + 3600
81
+ end_date = Time.now.utc + 7200
82
+ test = manager.create_test(
83
+ name: "Scheduled Test",
84
+ champion_version_id: @champion[:id],
85
+ challenger_version_id: @challenger[:id],
86
+ start_date: start_date,
87
+ end_date: end_date
88
+ )
89
+
90
+ expect(test.start_date).to eq(start_date)
91
+ expect(test.end_date).to eq(end_date)
92
+ end
93
+
94
+ it "sets status to running if start_date is in the past" do
95
+ test = manager.create_test(
96
+ name: "Immediate Test",
97
+ champion_version_id: @champion[:id],
98
+ challenger_version_id: @challenger[:id],
99
+ start_date: Time.now.utc - 3600
100
+ )
101
+
102
+ expect(test.status).to eq("running")
103
+ end
78
104
  end
79
105
 
80
106
  describe "#get_test" do
@@ -326,5 +352,261 @@ RSpec.describe DecisionAgent::ABTesting::ABTestManager do
326
352
 
327
353
  expect(results[:comparison][:statistical_significance]).to eq("not_significant")
328
354
  end
355
+
356
+ it "handles tests with assignments but no decisions" do
357
+ # Create assignments without recording decisions
358
+ 10.times do |i|
359
+ manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
360
+ end
361
+
362
+ results = manager.get_results(test.id)
363
+
364
+ expect(results[:champion][:decisions_recorded]).to eq(0)
365
+ expect(results[:challenger][:decisions_recorded]).to eq(0)
366
+ expect(results[:comparison][:statistical_significance]).to eq("insufficient_data")
367
+ end
368
+
369
+ it "calculates statistical significance with sufficient data" do
370
+ # Create 30+ assignments for each variant to trigger statistical significance
371
+ champion_count = 0
372
+ challenger_count = 0
373
+
374
+ 100.times do |i|
375
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
376
+ if assignment[:variant] == :champion
377
+ champion_count += 1
378
+ confidence = 0.7
379
+ else
380
+ challenger_count += 1
381
+ confidence = 0.9
382
+ end
383
+
384
+ manager.record_decision(
385
+ assignment_id: assignment[:assignment_id],
386
+ decision: "approve",
387
+ confidence: confidence
388
+ )
389
+ end
390
+
391
+ results = manager.get_results(test.id)
392
+
393
+ # Should have enough data for statistical significance
394
+ expect(results[:champion][:decisions_recorded]).to be >= 30
395
+ expect(results[:challenger][:decisions_recorded]).to be >= 30
396
+ expect(%w[significant not_significant]).to include(results[:comparison][:statistical_significance])
397
+ end
398
+
399
+ it "calculates different confidence levels based on t-statistic" do
400
+ # Create data that will result in different t-statistic values
401
+ # High t-statistic (> 2.576) should give 99% confidence
402
+ 50.times do |i|
403
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
404
+ # Large difference to get high t-statistic
405
+ confidence = assignment[:variant] == :champion ? 0.5 : 0.95
406
+ manager.record_decision(
407
+ assignment_id: assignment[:assignment_id],
408
+ decision: "approve",
409
+ confidence: confidence
410
+ )
411
+ end
412
+
413
+ results = manager.get_results(test.id)
414
+ expect(results[:comparison][:confidence_level]).to be_a(Numeric)
415
+ expect(results[:comparison][:confidence_level]).to be >= 0.0
416
+ end
417
+
418
+ it "determines winner correctly when challenger is better" do
419
+ 50.times do |i|
420
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
421
+ confidence = assignment[:variant] == :champion ? 0.6 : 0.9
422
+ manager.record_decision(
423
+ assignment_id: assignment[:assignment_id],
424
+ decision: "approve",
425
+ confidence: confidence
426
+ )
427
+ end
428
+
429
+ results = manager.get_results(test.id)
430
+ expect(%w[champion challenger inconclusive]).to include(results[:comparison][:winner])
431
+ end
432
+
433
+ it "generates appropriate recommendations" do
434
+ # Test different improvement scenarios
435
+ 50.times do |i|
436
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
437
+ # Challenger significantly better (>5% improvement)
438
+ confidence = assignment[:variant] == :champion ? 0.7 : 0.8
439
+ manager.record_decision(
440
+ assignment_id: assignment[:assignment_id],
441
+ decision: "approve",
442
+ confidence: confidence
443
+ )
444
+ end
445
+
446
+ results = manager.get_results(test.id)
447
+ expect(results[:comparison][:recommendation]).to be_a(String)
448
+ expect(results[:comparison][:recommendation]).not_to be_empty
449
+ end
450
+
451
+ it "handles champion better than challenger scenario" do
452
+ 50.times do |i|
453
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
454
+ # Champion better
455
+ confidence = assignment[:variant] == :champion ? 0.9 : 0.7
456
+ manager.record_decision(
457
+ assignment_id: assignment[:assignment_id],
458
+ decision: "approve",
459
+ confidence: confidence
460
+ )
461
+ end
462
+
463
+ results = manager.get_results(test.id)
464
+ expect(results[:comparison][:improvement_percentage]).to be < 0
465
+ end
466
+
467
+ it "handles similar performance scenario" do
468
+ 50.times do |i|
469
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
470
+ # Similar performance
471
+ confidence = assignment[:variant] == :champion ? 0.75 : 0.76
472
+ manager.record_decision(
473
+ assignment_id: assignment[:assignment_id],
474
+ decision: "approve",
475
+ confidence: confidence
476
+ )
477
+ end
478
+
479
+ results = manager.get_results(test.id)
480
+ expect(results[:comparison][:improvement_percentage]).to be_between(-5, 5)
481
+ end
482
+ end
483
+
484
+ describe "#list_tests" do
485
+ before do
486
+ manager.create_test(
487
+ name: "Test 1",
488
+ champion_version_id: @champion[:id],
489
+ challenger_version_id: @challenger[:id]
490
+ )
491
+ manager.create_test(
492
+ name: "Test 2",
493
+ champion_version_id: @champion[:id],
494
+ challenger_version_id: @challenger[:id]
495
+ )
496
+ end
497
+
498
+ it "lists all tests when no filters" do
499
+ tests = manager.list_tests
500
+ expect(tests.size).to be >= 2
501
+ end
502
+
503
+ it "filters by status" do
504
+ tests = manager.list_tests(status: "scheduled")
505
+ expect(tests).to all(have_attributes(status: "scheduled"))
506
+ end
507
+
508
+ it "respects limit parameter" do
509
+ tests = manager.list_tests(limit: 1)
510
+ expect(tests.size).to eq(1)
511
+ end
512
+ end
513
+
514
+ describe "#initialize" do
515
+ it "uses default storage adapter when not provided" do
516
+ manager = described_class.new(version_manager: version_manager)
517
+ expect(manager.storage_adapter).to be_a(DecisionAgent::ABTesting::Storage::MemoryAdapter)
518
+ end
519
+
520
+ it "uses default version manager when not provided" do
521
+ manager = described_class.new
522
+ expect(manager.version_manager).to be_a(DecisionAgent::Versioning::VersionManager)
523
+ end
524
+ end
525
+
526
+ describe "cache behavior" do
527
+ let(:test) do
528
+ manager.create_test(
529
+ name: "Test",
530
+ champion_version_id: @champion[:id],
531
+ challenger_version_id: @challenger[:id],
532
+ start_date: Time.now.utc + 3600
533
+ )
534
+ end
535
+
536
+ it "invalidates cache when test is started" do
537
+ manager.active_tests # Populate cache
538
+ manager.start_test(test.id)
539
+ # Cache should be invalidated, so next call should hit storage
540
+ expect(storage_adapter).to receive(:list_tests).and_call_original
541
+ manager.active_tests
542
+ end
543
+
544
+ it "invalidates cache when test is completed" do
545
+ manager.active_tests # Populate cache
546
+ manager.start_test(test.id) # Start the test first so it can be completed
547
+ manager.complete_test(test.id)
548
+ expect(storage_adapter).to receive(:list_tests).and_call_original
549
+ manager.active_tests
550
+ end
551
+
552
+ it "invalidates cache when test is cancelled" do
553
+ manager.active_tests # Populate cache
554
+ manager.cancel_test(test.id)
555
+ expect(storage_adapter).to receive(:list_tests).and_call_original
556
+ manager.active_tests
557
+ end
558
+
559
+ it "cache expires after 60 seconds" do
560
+ manager.active_tests # Populate cache
561
+ # Simulate time passing
562
+ allow(Time).to receive(:now).and_return(Time.now.utc + 61)
563
+ expect(storage_adapter).to receive(:list_tests).and_call_original
564
+ manager.active_tests
565
+ end
566
+ end
567
+
568
+ describe "#get_results edge cases" do
569
+ let(:test) do
570
+ test = manager.create_test(
571
+ name: "Test",
572
+ champion_version_id: @champion[:id],
573
+ challenger_version_id: @challenger[:id],
574
+ start_date: Time.now.utc + 3600
575
+ )
576
+ manager.start_test(test.id)
577
+ test
578
+ end
579
+
580
+ it "handles assignments with different decision results" do
581
+ 20.times do |i|
582
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
583
+ decision = i.even? ? "approve" : "reject"
584
+ manager.record_decision(
585
+ assignment_id: assignment[:assignment_id],
586
+ decision: decision,
587
+ confidence: 0.8
588
+ )
589
+ end
590
+
591
+ results = manager.get_results(test.id)
592
+ expect(results[:champion][:decision_distribution]).to be_a(Hash)
593
+ expect(results[:challenger][:decision_distribution]).to be_a(Hash)
594
+ end
595
+
596
+ it "calculates min and max confidence correctly" do
597
+ 20.times do |i|
598
+ assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
599
+ confidence = 0.5 + (i * 0.02) # Range from 0.5 to 0.88
600
+ manager.record_decision(
601
+ assignment_id: assignment[:assignment_id],
602
+ decision: "approve",
603
+ confidence: confidence
604
+ )
605
+ end
606
+
607
+ results = manager.get_results(test.id)
608
+ expect(results[:champion][:min_confidence]).to be_a(Numeric).or be_nil
609
+ expect(results[:champion][:max_confidence]).to be_a(Numeric).or be_nil
610
+ end
329
611
  end
330
612
  end