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.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- 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
|