decision_agent 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +152 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +148 -3
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent.rb +7 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +330 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/examples.txt +612 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +2 -2
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +346 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- metadata +26 -2
|
@@ -0,0 +1,330 @@
|
|
|
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
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "#get_test" do
|
|
81
|
+
it "retrieves a test by ID" do
|
|
82
|
+
created_test = manager.create_test(
|
|
83
|
+
name: "Test",
|
|
84
|
+
champion_version_id: @champion[:id],
|
|
85
|
+
challenger_version_id: @challenger[:id]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
retrieved_test = manager.get_test(created_test.id)
|
|
89
|
+
|
|
90
|
+
expect(retrieved_test).not_to be_nil
|
|
91
|
+
expect(retrieved_test.id).to eq(created_test.id)
|
|
92
|
+
expect(retrieved_test.name).to eq("Test")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "returns nil for nonexistent test" do
|
|
96
|
+
test = manager.get_test(99_999)
|
|
97
|
+
expect(test).to be_nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe "#assign_variant" do
|
|
102
|
+
let(:test) do
|
|
103
|
+
manager.create_test(
|
|
104
|
+
name: "Test",
|
|
105
|
+
champion_version_id: @champion[:id],
|
|
106
|
+
challenger_version_id: @challenger[:id],
|
|
107
|
+
start_date: Time.now.utc + 3600
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
before do
|
|
112
|
+
manager.start_test(test.id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "assigns a variant and returns assignment details" do
|
|
116
|
+
assignment = manager.assign_variant(test_id: test.id, user_id: "user_123")
|
|
117
|
+
|
|
118
|
+
expect(assignment[:test_id]).to eq(test.id)
|
|
119
|
+
expect(%i[champion challenger]).to include(assignment[:variant])
|
|
120
|
+
expect([@champion[:id], @challenger[:id]]).to include(assignment[:version_id])
|
|
121
|
+
expect(assignment[:assignment_id]).not_to be_nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "assigns same variant to same user" do
|
|
125
|
+
user_id = "consistent_user"
|
|
126
|
+
assignment1 = manager.assign_variant(test_id: test.id, user_id: user_id)
|
|
127
|
+
assignment2 = manager.assign_variant(test_id: test.id, user_id: user_id)
|
|
128
|
+
|
|
129
|
+
expect(assignment1[:variant]).to eq(assignment2[:variant])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "raises error for nonexistent test" do
|
|
133
|
+
expect do
|
|
134
|
+
manager.assign_variant(test_id: 99_999)
|
|
135
|
+
end.to raise_error(DecisionAgent::ABTesting::TestNotFoundError)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe "#record_decision" do
|
|
140
|
+
let(:test) do
|
|
141
|
+
test = manager.create_test(
|
|
142
|
+
name: "Test",
|
|
143
|
+
champion_version_id: @champion[:id],
|
|
144
|
+
challenger_version_id: @challenger[:id],
|
|
145
|
+
start_date: Time.now.utc + 3600
|
|
146
|
+
)
|
|
147
|
+
manager.start_test(test.id)
|
|
148
|
+
test
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "records decision result for an assignment" do
|
|
152
|
+
assignment = manager.assign_variant(test_id: test.id)
|
|
153
|
+
|
|
154
|
+
expect do
|
|
155
|
+
manager.record_decision(
|
|
156
|
+
assignment_id: assignment[:assignment_id],
|
|
157
|
+
decision: "approve",
|
|
158
|
+
confidence: 0.95
|
|
159
|
+
)
|
|
160
|
+
end.not_to raise_error
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe "#get_results" do
|
|
165
|
+
let(:test) do
|
|
166
|
+
test = manager.create_test(
|
|
167
|
+
name: "Test",
|
|
168
|
+
champion_version_id: @champion[:id],
|
|
169
|
+
challenger_version_id: @challenger[:id],
|
|
170
|
+
start_date: Time.now.utc + 3600
|
|
171
|
+
)
|
|
172
|
+
manager.start_test(test.id)
|
|
173
|
+
test
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "returns results with statistics" do
|
|
177
|
+
# Create some assignments and record decisions
|
|
178
|
+
10.times do |i|
|
|
179
|
+
assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
|
|
180
|
+
manager.record_decision(
|
|
181
|
+
assignment_id: assignment[:assignment_id],
|
|
182
|
+
decision: "approve",
|
|
183
|
+
confidence: 0.8 + (rand * 0.2)
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
results = manager.get_results(test.id)
|
|
188
|
+
|
|
189
|
+
expect(results[:test]).to be_a(Hash)
|
|
190
|
+
expect(results[:champion]).to be_a(Hash)
|
|
191
|
+
expect(results[:challenger]).to be_a(Hash)
|
|
192
|
+
expect(results[:comparison]).to be_a(Hash)
|
|
193
|
+
expect(results[:total_assignments]).to eq(10)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "handles tests with no assignments" do
|
|
197
|
+
results = manager.get_results(test.id)
|
|
198
|
+
|
|
199
|
+
expect(results[:total_assignments]).to eq(0)
|
|
200
|
+
expect(results[:champion][:decisions_recorded]).to eq(0)
|
|
201
|
+
expect(results[:challenger][:decisions_recorded]).to eq(0)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe "#active_tests" do
|
|
206
|
+
it "returns only running tests" do
|
|
207
|
+
test1 = manager.create_test(
|
|
208
|
+
name: "Running Test",
|
|
209
|
+
champion_version_id: @champion[:id],
|
|
210
|
+
challenger_version_id: @challenger[:id],
|
|
211
|
+
start_date: Time.now.utc + 3600
|
|
212
|
+
)
|
|
213
|
+
manager.start_test(test1.id)
|
|
214
|
+
|
|
215
|
+
manager.create_test(
|
|
216
|
+
name: "Scheduled Test",
|
|
217
|
+
champion_version_id: @champion[:id],
|
|
218
|
+
challenger_version_id: @challenger[:id],
|
|
219
|
+
start_date: Time.now.utc + 3600
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
active = manager.active_tests
|
|
223
|
+
|
|
224
|
+
expect(active.size).to eq(1)
|
|
225
|
+
expect(active.first.id).to eq(test1.id)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "caches active tests for performance" do
|
|
229
|
+
test = manager.create_test(
|
|
230
|
+
name: "Test",
|
|
231
|
+
champion_version_id: @champion[:id],
|
|
232
|
+
challenger_version_id: @challenger[:id],
|
|
233
|
+
start_date: Time.now.utc + 3600
|
|
234
|
+
)
|
|
235
|
+
manager.start_test(test.id)
|
|
236
|
+
|
|
237
|
+
# First call
|
|
238
|
+
manager.active_tests
|
|
239
|
+
|
|
240
|
+
# Expect storage adapter not to be called again (cached)
|
|
241
|
+
expect(storage_adapter).not_to receive(:list_tests)
|
|
242
|
+
manager.active_tests
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
describe "test lifecycle" do
|
|
247
|
+
let(:test) do
|
|
248
|
+
manager.create_test(
|
|
249
|
+
name: "Test",
|
|
250
|
+
champion_version_id: @champion[:id],
|
|
251
|
+
challenger_version_id: @challenger[:id],
|
|
252
|
+
start_date: Time.now.utc + 3600
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "starts a scheduled test" do
|
|
257
|
+
manager.start_test(test.id)
|
|
258
|
+
updated_test = manager.get_test(test.id)
|
|
259
|
+
|
|
260
|
+
expect(updated_test.status).to eq("running")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "completes a running test" do
|
|
264
|
+
manager.start_test(test.id)
|
|
265
|
+
manager.complete_test(test.id)
|
|
266
|
+
updated_test = manager.get_test(test.id)
|
|
267
|
+
|
|
268
|
+
expect(updated_test.status).to eq("completed")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "cancels a test" do
|
|
272
|
+
manager.cancel_test(test.id)
|
|
273
|
+
updated_test = manager.get_test(test.id)
|
|
274
|
+
|
|
275
|
+
expect(updated_test.status).to eq("cancelled")
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
describe "statistical analysis" do
|
|
280
|
+
let(:test) do
|
|
281
|
+
test = manager.create_test(
|
|
282
|
+
name: "Statistical Test",
|
|
283
|
+
champion_version_id: @champion[:id],
|
|
284
|
+
challenger_version_id: @challenger[:id],
|
|
285
|
+
traffic_split: { champion: 50, challenger: 50 },
|
|
286
|
+
start_date: Time.now.utc + 3600
|
|
287
|
+
)
|
|
288
|
+
manager.start_test(test.id)
|
|
289
|
+
test
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "calculates improvement percentage" do
|
|
293
|
+
# Create assignments with different confidence levels
|
|
294
|
+
50.times do |i|
|
|
295
|
+
assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
|
|
296
|
+
|
|
297
|
+
# Champion: avg 0.7, Challenger: avg 0.9
|
|
298
|
+
confidence = assignment[:variant] == :champion ? 0.7 : 0.9
|
|
299
|
+
|
|
300
|
+
manager.record_decision(
|
|
301
|
+
assignment_id: assignment[:assignment_id],
|
|
302
|
+
decision: "approve",
|
|
303
|
+
confidence: confidence
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
results = manager.get_results(test.id)
|
|
308
|
+
|
|
309
|
+
# Challenger should have higher avg confidence (0.9 vs 0.7)
|
|
310
|
+
expect(results[:comparison][:improvement_percentage]).to be > 0
|
|
311
|
+
expect(%w[champion challenger inconclusive]).to include(results[:comparison][:winner])
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "indicates insufficient data when sample is too small" do
|
|
315
|
+
# Create only a few assignments
|
|
316
|
+
5.times do |i|
|
|
317
|
+
assignment = manager.assign_variant(test_id: test.id, user_id: "user_#{i}")
|
|
318
|
+
manager.record_decision(
|
|
319
|
+
assignment_id: assignment[:assignment_id],
|
|
320
|
+
decision: "approve",
|
|
321
|
+
confidence: 0.8
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
results = manager.get_results(test.id)
|
|
326
|
+
|
|
327
|
+
expect(results[:comparison][:statistical_significance]).to eq("not_significant")
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "decision_agent/ab_testing/ab_test"
|
|
3
|
+
|
|
4
|
+
RSpec.describe DecisionAgent::ABTesting::ABTest do
|
|
5
|
+
describe "#initialize" do
|
|
6
|
+
it "creates a valid A/B test with default values" do
|
|
7
|
+
test = described_class.new(
|
|
8
|
+
name: "Test A vs B",
|
|
9
|
+
champion_version_id: "v1",
|
|
10
|
+
challenger_version_id: "v2"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
expect(test.name).to eq("Test A vs B")
|
|
14
|
+
expect(test.champion_version_id).to eq("v1")
|
|
15
|
+
expect(test.challenger_version_id).to eq("v2")
|
|
16
|
+
expect(test.traffic_split).to eq({ champion: 90, challenger: 10 })
|
|
17
|
+
expect(test.status).to eq("scheduled")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "accepts custom traffic split as hash" do
|
|
21
|
+
test = described_class.new(
|
|
22
|
+
name: "Custom Split",
|
|
23
|
+
champion_version_id: "v1",
|
|
24
|
+
challenger_version_id: "v2",
|
|
25
|
+
traffic_split: { champion: 70, challenger: 30 }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(test.traffic_split).to eq({ champion: 70, challenger: 30 })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "accepts custom traffic split as array" do
|
|
32
|
+
test = described_class.new(
|
|
33
|
+
name: "Array Split",
|
|
34
|
+
champion_version_id: "v1",
|
|
35
|
+
challenger_version_id: "v2",
|
|
36
|
+
traffic_split: [80, 20]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(test.traffic_split).to eq({ champion: 80, challenger: 20 })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "raises error if traffic split doesn't sum to 100" do
|
|
43
|
+
expect do
|
|
44
|
+
described_class.new(
|
|
45
|
+
name: "Bad Split",
|
|
46
|
+
champion_version_id: "v1",
|
|
47
|
+
challenger_version_id: "v2",
|
|
48
|
+
traffic_split: { champion: 60, challenger: 30 }
|
|
49
|
+
)
|
|
50
|
+
end.to raise_error(DecisionAgent::ValidationError, /must sum to 100/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "raises error if champion and challenger are the same" do
|
|
54
|
+
expect do
|
|
55
|
+
described_class.new(
|
|
56
|
+
name: "Same Versions",
|
|
57
|
+
champion_version_id: "v1",
|
|
58
|
+
challenger_version_id: "v1"
|
|
59
|
+
)
|
|
60
|
+
end.to raise_error(DecisionAgent::ValidationError, /must be different/)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "raises error if name is empty" do
|
|
64
|
+
expect do
|
|
65
|
+
described_class.new(
|
|
66
|
+
name: "",
|
|
67
|
+
champion_version_id: "v1",
|
|
68
|
+
challenger_version_id: "v2"
|
|
69
|
+
)
|
|
70
|
+
end.to raise_error(DecisionAgent::ValidationError, /name is required/)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "#assign_variant" do
|
|
75
|
+
let(:test) do
|
|
76
|
+
described_class.new(
|
|
77
|
+
name: "Test",
|
|
78
|
+
champion_version_id: "v1",
|
|
79
|
+
challenger_version_id: "v2",
|
|
80
|
+
traffic_split: { champion: 90, challenger: 10 },
|
|
81
|
+
status: "running",
|
|
82
|
+
id: 123
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "assigns champion or challenger based on traffic split" do
|
|
87
|
+
assignments = 1000.times.map { test.assign_variant }
|
|
88
|
+
champion_count = assignments.count { |v| v == :champion }
|
|
89
|
+
challenger_count = assignments.count { |v| v == :challenger }
|
|
90
|
+
|
|
91
|
+
# With 90/10 split, expect roughly 900/100
|
|
92
|
+
expect(champion_count).to be_between(850, 950)
|
|
93
|
+
expect(challenger_count).to be_between(50, 150)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "assigns same variant to same user consistently" do
|
|
97
|
+
user_id = "user_123"
|
|
98
|
+
variants = 10.times.map { test.assign_variant(user_id: user_id) }
|
|
99
|
+
|
|
100
|
+
expect(variants.uniq.size).to eq(1)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "assigns different users to different variants based on split" do
|
|
104
|
+
assignments = 1000.times.map { |i| test.assign_variant(user_id: "user_#{i}") }
|
|
105
|
+
champion_count = assignments.count { |v| v == :champion }
|
|
106
|
+
challenger_count = assignments.count { |v| v == :challenger }
|
|
107
|
+
|
|
108
|
+
expect(champion_count).to be_between(850, 950)
|
|
109
|
+
expect(challenger_count).to be_between(50, 150)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "raises error if test is not running" do
|
|
113
|
+
test = described_class.new(
|
|
114
|
+
name: "Not Running",
|
|
115
|
+
champion_version_id: "v1",
|
|
116
|
+
challenger_version_id: "v2",
|
|
117
|
+
status: "completed"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect do
|
|
121
|
+
test.assign_variant
|
|
122
|
+
end.to raise_error(DecisionAgent::ABTesting::TestNotRunningError)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "#version_for_variant" do
|
|
127
|
+
let(:test) do
|
|
128
|
+
described_class.new(
|
|
129
|
+
name: "Test",
|
|
130
|
+
champion_version_id: "champion_v1",
|
|
131
|
+
challenger_version_id: "challenger_v2"
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "returns champion version ID for :champion variant" do
|
|
136
|
+
expect(test.version_for_variant(:champion)).to eq("champion_v1")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "returns challenger version ID for :challenger variant" do
|
|
140
|
+
expect(test.version_for_variant(:challenger)).to eq("challenger_v2")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "raises error for invalid variant" do
|
|
144
|
+
expect do
|
|
145
|
+
test.version_for_variant(:invalid)
|
|
146
|
+
end.to raise_error(ArgumentError, /Invalid variant/)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe "#running?" do
|
|
151
|
+
it "returns true when status is running and within date range" do
|
|
152
|
+
test = described_class.new(
|
|
153
|
+
name: "Test",
|
|
154
|
+
champion_version_id: "v1",
|
|
155
|
+
challenger_version_id: "v2",
|
|
156
|
+
status: "running",
|
|
157
|
+
start_date: Time.now.utc - 3600,
|
|
158
|
+
end_date: Time.now.utc + 3600
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(test.running?).to be true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "returns false when status is not running" do
|
|
165
|
+
test = described_class.new(
|
|
166
|
+
name: "Test",
|
|
167
|
+
champion_version_id: "v1",
|
|
168
|
+
challenger_version_id: "v2",
|
|
169
|
+
status: "completed"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
expect(test.running?).to be false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "returns false when start date is in future" do
|
|
176
|
+
test = described_class.new(
|
|
177
|
+
name: "Test",
|
|
178
|
+
champion_version_id: "v1",
|
|
179
|
+
challenger_version_id: "v2",
|
|
180
|
+
status: "running",
|
|
181
|
+
start_date: Time.now.utc + 3600
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
expect(test.running?).to be false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "returns false when end date has passed" do
|
|
188
|
+
test = described_class.new(
|
|
189
|
+
name: "Test",
|
|
190
|
+
champion_version_id: "v1",
|
|
191
|
+
challenger_version_id: "v2",
|
|
192
|
+
status: "running",
|
|
193
|
+
start_date: Time.now.utc - 7200,
|
|
194
|
+
end_date: Time.now.utc - 3600
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(test.running?).to be false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
describe "status transitions" do
|
|
202
|
+
it "can start a scheduled test" do
|
|
203
|
+
test = described_class.new(
|
|
204
|
+
name: "Test",
|
|
205
|
+
champion_version_id: "v1",
|
|
206
|
+
challenger_version_id: "v2",
|
|
207
|
+
status: "scheduled"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect { test.start! }.not_to raise_error
|
|
211
|
+
expect(test.status).to eq("running")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "can complete a running test" do
|
|
215
|
+
test = described_class.new(
|
|
216
|
+
name: "Test",
|
|
217
|
+
champion_version_id: "v1",
|
|
218
|
+
challenger_version_id: "v2",
|
|
219
|
+
status: "running"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
expect { test.complete! }.not_to raise_error
|
|
223
|
+
expect(test.status).to eq("completed")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "can cancel a test" do
|
|
227
|
+
test = described_class.new(
|
|
228
|
+
name: "Test",
|
|
229
|
+
champion_version_id: "v1",
|
|
230
|
+
challenger_version_id: "v2",
|
|
231
|
+
status: "running"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect { test.cancel! }.not_to raise_error
|
|
235
|
+
expect(test.status).to eq("cancelled")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "raises error when trying invalid status transition" do
|
|
239
|
+
test = described_class.new(
|
|
240
|
+
name: "Test",
|
|
241
|
+
champion_version_id: "v1",
|
|
242
|
+
challenger_version_id: "v2",
|
|
243
|
+
status: "completed"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
expect do
|
|
247
|
+
test.start!
|
|
248
|
+
end.to raise_error(DecisionAgent::ABTesting::InvalidStatusTransitionError)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
describe "#to_h" do
|
|
253
|
+
it "returns hash representation" do
|
|
254
|
+
test = described_class.new(
|
|
255
|
+
name: "Test",
|
|
256
|
+
champion_version_id: "v1",
|
|
257
|
+
challenger_version_id: "v2",
|
|
258
|
+
id: 123
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
hash = test.to_h
|
|
262
|
+
|
|
263
|
+
expect(hash[:id]).to eq(123)
|
|
264
|
+
expect(hash[:name]).to eq("Test")
|
|
265
|
+
expect(hash[:champion_version_id]).to eq("v1")
|
|
266
|
+
expect(hash[:challenger_version_id]).to eq("v2")
|
|
267
|
+
expect(hash[:status]).to eq("scheduled")
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|