decision_agent 0.2.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -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