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,612 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/ab_testing/ab_test_manager"
3
- require "decision_agent/ab_testing/storage/memory_adapter"
4
- require "decision_agent/versioning/file_storage_adapter"
5
-
6
- RSpec.describe DecisionAgent::ABTesting::ABTestManager do
7
- let(:version_manager) do
8
- DecisionAgent::Versioning::VersionManager.new(
9
- adapter: DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: "/tmp/spec_ab_test_versions")
10
- )
11
- end
12
-
13
- let(:storage_adapter) { DecisionAgent::ABTesting::Storage::MemoryAdapter.new }
14
- let(:manager) { described_class.new(storage_adapter: storage_adapter, version_manager: version_manager) }
15
-
16
- before do
17
- # Create test versions
18
- @champion = version_manager.save_version(
19
- rule_id: "test_rule",
20
- rule_content: { rules: [{ decision: "approve", weight: 1.0 }] },
21
- created_by: "spec"
22
- )
23
-
24
- @challenger = version_manager.save_version(
25
- rule_id: "test_rule",
26
- rule_content: { rules: [{ decision: "reject", weight: 1.0 }] },
27
- created_by: "spec"
28
- )
29
- end
30
-
31
- after do
32
- FileUtils.rm_rf("/tmp/spec_ab_test_versions")
33
- end
34
-
35
- describe "#create_test" do
36
- it "creates a new A/B test" do
37
- test = manager.create_test(
38
- name: "Test A vs B",
39
- champion_version_id: @champion[:id],
40
- challenger_version_id: @challenger[:id]
41
- )
42
-
43
- expect(test).to be_a(DecisionAgent::ABTesting::ABTest)
44
- expect(test.name).to eq("Test A vs B")
45
- expect(test.id).not_to be_nil
46
- end
47
-
48
- it "validates that champion version exists" do
49
- expect do
50
- manager.create_test(
51
- name: "Test",
52
- champion_version_id: "nonexistent",
53
- challenger_version_id: @challenger[:id]
54
- )
55
- end.to raise_error(DecisionAgent::ABTesting::VersionNotFoundError, /Champion/)
56
- end
57
-
58
- it "validates that challenger version exists" do
59
- expect do
60
- manager.create_test(
61
- name: "Test",
62
- champion_version_id: @champion[:id],
63
- challenger_version_id: "nonexistent"
64
- )
65
- end.to raise_error(DecisionAgent::ABTesting::VersionNotFoundError, /Challenger/)
66
- end
67
-
68
- it "accepts custom traffic split" do
69
- test = manager.create_test(
70
- name: "Custom Split",
71
- champion_version_id: @champion[:id],
72
- challenger_version_id: @challenger[:id],
73
- traffic_split: { champion: 70, challenger: 30 }
74
- )
75
-
76
- expect(test.traffic_split).to eq({ champion: 70, challenger: 30 })
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
104
- end
105
-
106
- describe "#get_test" do
107
- it "retrieves a test by ID" do
108
- created_test = manager.create_test(
109
- name: "Test",
110
- champion_version_id: @champion[:id],
111
- challenger_version_id: @challenger[:id]
112
- )
113
-
114
- retrieved_test = manager.get_test(created_test.id)
115
-
116
- expect(retrieved_test).not_to be_nil
117
- expect(retrieved_test.id).to eq(created_test.id)
118
- expect(retrieved_test.name).to eq("Test")
119
- end
120
-
121
- it "returns nil for nonexistent test" do
122
- test = manager.get_test(99_999)
123
- expect(test).to be_nil
124
- end
125
- end
126
-
127
- describe "#assign_variant" do
128
- let(:test) do
129
- manager.create_test(
130
- name: "Test",
131
- champion_version_id: @champion[:id],
132
- challenger_version_id: @challenger[:id],
133
- start_date: Time.now.utc + 3600
134
- )
135
- end
136
-
137
- before do
138
- manager.start_test(test.id)
139
- end
140
-
141
- it "assigns a variant and returns assignment details" do
142
- assignment = manager.assign_variant(test_id: test.id, user_id: "user_123")
143
-
144
- expect(assignment[:test_id]).to eq(test.id)
145
- expect(%i[champion challenger]).to include(assignment[:variant])
146
- expect([@champion[:id], @challenger[:id]]).to include(assignment[:version_id])
147
- expect(assignment[:assignment_id]).not_to be_nil
148
- end
149
-
150
- it "assigns same variant to same user" do
151
- user_id = "consistent_user"
152
- assignment1 = manager.assign_variant(test_id: test.id, user_id: user_id)
153
- assignment2 = manager.assign_variant(test_id: test.id, user_id: user_id)
154
-
155
- expect(assignment1[:variant]).to eq(assignment2[:variant])
156
- end
157
-
158
- it "raises error for nonexistent test" do
159
- expect do
160
- manager.assign_variant(test_id: 99_999)
161
- end.to raise_error(DecisionAgent::ABTesting::TestNotFoundError)
162
- end
163
- end
164
-
165
- describe "#record_decision" do
166
- let(:test) do
167
- test = manager.create_test(
168
- name: "Test",
169
- champion_version_id: @champion[:id],
170
- challenger_version_id: @challenger[:id],
171
- start_date: Time.now.utc + 3600
172
- )
173
- manager.start_test(test.id)
174
- test
175
- end
176
-
177
- it "records decision result for an assignment" do
178
- assignment = manager.assign_variant(test_id: test.id)
179
-
180
- expect do
181
- manager.record_decision(
182
- assignment_id: assignment[:assignment_id],
183
- decision: "approve",
184
- confidence: 0.95
185
- )
186
- end.not_to raise_error
187
- end
188
- end
189
-
190
- describe "#get_results" do
191
- let(:test) do
192
- test = manager.create_test(
193
- name: "Test",
194
- champion_version_id: @champion[:id],
195
- challenger_version_id: @challenger[:id],
196
- start_date: Time.now.utc + 3600
197
- )
198
- manager.start_test(test.id)
199
- test
200
- end
201
-
202
- it "returns results with statistics" do
203
- # Create some assignments and record decisions
204
- 10.times do |i|
205
- assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
206
- manager.record_decision(
207
- assignment_id: assignment[:assignment_id],
208
- decision: "approve",
209
- confidence: 0.8 + (rand * 0.2)
210
- )
211
- end
212
-
213
- results = manager.get_results(test.id)
214
-
215
- expect(results[:test]).to be_a(Hash)
216
- expect(results[:champion]).to be_a(Hash)
217
- expect(results[:challenger]).to be_a(Hash)
218
- expect(results[:comparison]).to be_a(Hash)
219
- expect(results[:total_assignments]).to eq(10)
220
- end
221
-
222
- it "handles tests with no assignments" do
223
- results = manager.get_results(test.id)
224
-
225
- expect(results[:total_assignments]).to eq(0)
226
- expect(results[:champion][:decisions_recorded]).to eq(0)
227
- expect(results[:challenger][:decisions_recorded]).to eq(0)
228
- end
229
- end
230
-
231
- describe "#active_tests" do
232
- it "returns only running tests" do
233
- test1 = manager.create_test(
234
- name: "Running Test",
235
- champion_version_id: @champion[:id],
236
- challenger_version_id: @challenger[:id],
237
- start_date: Time.now.utc + 3600
238
- )
239
- manager.start_test(test1.id)
240
-
241
- manager.create_test(
242
- name: "Scheduled Test",
243
- champion_version_id: @champion[:id],
244
- challenger_version_id: @challenger[:id],
245
- start_date: Time.now.utc + 3600
246
- )
247
-
248
- active = manager.active_tests
249
-
250
- expect(active.size).to eq(1)
251
- expect(active.first.id).to eq(test1.id)
252
- end
253
-
254
- it "caches active tests for performance" do
255
- test = manager.create_test(
256
- name: "Test",
257
- champion_version_id: @champion[:id],
258
- challenger_version_id: @challenger[:id],
259
- start_date: Time.now.utc + 3600
260
- )
261
- manager.start_test(test.id)
262
-
263
- # First call
264
- manager.active_tests
265
-
266
- # Expect storage adapter not to be called again (cached)
267
- expect(storage_adapter).not_to receive(:list_tests)
268
- manager.active_tests
269
- end
270
- end
271
-
272
- describe "test lifecycle" do
273
- let(:test) do
274
- manager.create_test(
275
- name: "Test",
276
- champion_version_id: @champion[:id],
277
- challenger_version_id: @challenger[:id],
278
- start_date: Time.now.utc + 3600
279
- )
280
- end
281
-
282
- it "starts a scheduled test" do
283
- manager.start_test(test.id)
284
- updated_test = manager.get_test(test.id)
285
-
286
- expect(updated_test.status).to eq("running")
287
- end
288
-
289
- it "completes a running test" do
290
- manager.start_test(test.id)
291
- manager.complete_test(test.id)
292
- updated_test = manager.get_test(test.id)
293
-
294
- expect(updated_test.status).to eq("completed")
295
- end
296
-
297
- it "cancels a test" do
298
- manager.cancel_test(test.id)
299
- updated_test = manager.get_test(test.id)
300
-
301
- expect(updated_test.status).to eq("cancelled")
302
- end
303
- end
304
-
305
- describe "statistical analysis" do
306
- let(:test) do
307
- test = manager.create_test(
308
- name: "Statistical Test",
309
- champion_version_id: @champion[:id],
310
- challenger_version_id: @challenger[:id],
311
- traffic_split: { champion: 50, challenger: 50 },
312
- start_date: Time.now.utc + 3600
313
- )
314
- manager.start_test(test.id)
315
- test
316
- end
317
-
318
- it "calculates improvement percentage" do
319
- # Create assignments with different confidence levels
320
- 50.times do |i|
321
- assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
322
-
323
- # Champion: avg 0.7, Challenger: avg 0.9
324
- confidence = assignment[:variant] == :champion ? 0.7 : 0.9
325
-
326
- manager.record_decision(
327
- assignment_id: assignment[:assignment_id],
328
- decision: "approve",
329
- confidence: confidence
330
- )
331
- end
332
-
333
- results = manager.get_results(test.id)
334
-
335
- # Challenger should have higher avg confidence (0.9 vs 0.7)
336
- expect(results[:comparison][:improvement_percentage]).to be > 0
337
- expect(%w[champion challenger inconclusive]).to include(results[:comparison][:winner])
338
- end
339
-
340
- it "indicates insufficient data when sample is too small" do
341
- # Create only a few assignments
342
- 5.times do |i|
343
- assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
344
- manager.record_decision(
345
- assignment_id: assignment[:assignment_id],
346
- decision: "approve",
347
- confidence: 0.8
348
- )
349
- end
350
-
351
- results = manager.get_results(test.id)
352
-
353
- expect(results[:comparison][:statistical_significance]).to eq("not_significant")
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
611
- end
612
- end