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,655 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/versioning/file_storage_adapter"
3
- require "decision_agent/ab_testing/storage/memory_adapter"
4
-
5
- RSpec.describe DecisionAgent::ABTesting::ABTestingAgent do
6
- let(:version_manager) { double("VersionManager") }
7
- let(:ab_test_manager) { double("ABTestManager", version_manager: version_manager) }
8
- let(:base_evaluator) do
9
- DecisionAgent::Evaluators::StaticEvaluator.new(
10
- decision: "approve",
11
- weight: 0.8,
12
- reason: "Base evaluator"
13
- )
14
- end
15
-
16
- describe "#initialize" do
17
- it "initializes with ab_test_manager" do
18
- agent = described_class.new(ab_test_manager: ab_test_manager)
19
- expect(agent.ab_test_manager).to eq(ab_test_manager)
20
- end
21
-
22
- it "uses version_manager from ab_test_manager if not provided" do
23
- allow(ab_test_manager).to receive(:version_manager).and_return(version_manager)
24
- agent = described_class.new(ab_test_manager: ab_test_manager)
25
- expect(agent.version_manager).to eq(version_manager)
26
- end
27
-
28
- it "uses provided version_manager" do
29
- custom_version_manager = double("CustomVersionManager")
30
- agent = described_class.new(
31
- ab_test_manager: ab_test_manager,
32
- version_manager: custom_version_manager
33
- )
34
- expect(agent.version_manager).to eq(custom_version_manager)
35
- end
36
- end
37
-
38
- describe "#decide" do
39
- context "without A/B test" do
40
- it "makes standard decision" do
41
- agent = described_class.new(
42
- ab_test_manager: ab_test_manager,
43
- evaluators: [base_evaluator]
44
- )
45
-
46
- result = agent.decide(context: { user: "test" })
47
-
48
- expect(result[:decision]).to eq("approve")
49
- expect(result[:ab_test]).to be_nil
50
- end
51
- end
52
-
53
- context "with A/B test" do
54
- let(:assignment) do
55
- {
56
- assignment_id: "assign_1",
57
- variant: "A",
58
- version_id: "version_1"
59
- }
60
- end
61
-
62
- let(:version) do
63
- {
64
- content: {
65
- version: "1.0",
66
- ruleset: "test",
67
- rules: [
68
- {
69
- id: "rule_1",
70
- if: { field: "status", op: "eq", value: "active" },
71
- then: { decision: "approve", weight: 0.9 }
72
- }
73
- ]
74
- }
75
- }
76
- end
77
-
78
- before do
79
- allow(ab_test_manager).to receive(:assign_variant).and_return(assignment)
80
- allow(version_manager).to receive(:get_version).and_return(version)
81
- allow(ab_test_manager).to receive(:record_decision)
82
- end
83
-
84
- it "assigns variant and makes decision" do
85
- agent = described_class.new(
86
- ab_test_manager: ab_test_manager,
87
- version_manager: version_manager
88
- )
89
-
90
- result = agent.decide(
91
- context: { status: "active" },
92
- ab_test_id: "test_1",
93
- user_id: "user_1"
94
- )
95
-
96
- expect(result[:decision]).to eq("approve")
97
- expect(result[:ab_test]).not_to be_nil
98
- expect(result[:ab_test][:test_id]).to eq("test_1")
99
- expect(result[:ab_test][:variant]).to eq("A")
100
- end
101
-
102
- it "raises error if version not found" do
103
- allow(version_manager).to receive(:get_version).and_return(nil)
104
- agent = described_class.new(
105
- ab_test_manager: ab_test_manager,
106
- version_manager: version_manager
107
- )
108
-
109
- expect do
110
- agent.decide(
111
- context: { status: "active" },
112
- ab_test_id: "test_1"
113
- )
114
- end.to raise_error(DecisionAgent::ABTesting::VersionNotFoundError)
115
- end
116
-
117
- it "records decision result" do
118
- agent = described_class.new(
119
- ab_test_manager: ab_test_manager,
120
- version_manager: version_manager
121
- )
122
-
123
- agent.decide(
124
- context: { status: "active" },
125
- ab_test_id: "test_1"
126
- )
127
-
128
- expect(ab_test_manager).to have_received(:record_decision).with(
129
- assignment_id: "assign_1",
130
- decision: "approve",
131
- confidence: be_a(Numeric)
132
- )
133
- end
134
- end
135
- end
136
-
137
- describe "#get_test_results" do
138
- it "delegates to ab_test_manager" do
139
- agent = described_class.new(ab_test_manager: ab_test_manager)
140
- allow(ab_test_manager).to receive(:get_results).and_return({ results: [] })
141
-
142
- result = agent.get_test_results("test_1")
143
-
144
- expect(ab_test_manager).to have_received(:get_results).with("test_1")
145
- expect(result).to eq({ results: [] })
146
- end
147
- end
148
-
149
- describe "#active_tests" do
150
- it "delegates to ab_test_manager" do
151
- agent = described_class.new(ab_test_manager: ab_test_manager)
152
- allow(ab_test_manager).to receive(:active_tests).and_return([])
153
-
154
- result = agent.active_tests
155
-
156
- expect(ab_test_manager).to have_received(:active_tests)
157
- expect(result).to eq([])
158
- end
159
- end
160
-
161
- describe "#decide with feedback" do
162
- it "passes feedback to agent" do
163
- agent = described_class.new(
164
- ab_test_manager: ab_test_manager,
165
- evaluators: [base_evaluator]
166
- )
167
-
168
- result = agent.decide(context: { user: "test" }, feedback: { rating: 5 })
169
-
170
- expect(result[:decision]).to eq("approve")
171
- end
172
- end
173
-
174
- describe "#decide with A/B test - evaluator building" do
175
- let(:assignment) do
176
- {
177
- assignment_id: "assign_1",
178
- variant: "A",
179
- version_id: "version_1"
180
- }
181
- end
182
-
183
- let(:version_with_rules) do
184
- {
185
- content: {
186
- version: "1.0",
187
- ruleset: "test",
188
- rules: [
189
- {
190
- id: "rule_1",
191
- if: { field: "status", op: "eq", value: "active" },
192
- then: { decision: "approve", weight: 0.9 }
193
- }
194
- ]
195
- }
196
- }
197
- end
198
-
199
- let(:version_with_evaluators) do
200
- {
201
- content: {
202
- evaluators: [
203
- {
204
- type: "json_rule",
205
- rules: {
206
- version: "1.0",
207
- ruleset: "test",
208
- rules: [
209
- {
210
- id: "rule_1",
211
- if: { field: "status", op: "eq", value: "active" },
212
- then: { decision: "approve", weight: 0.9 }
213
- }
214
- ]
215
- }
216
- }
217
- ]
218
- }
219
- }
220
- end
221
-
222
- let(:version_with_static_evaluator) do
223
- {
224
- content: {
225
- evaluators: [
226
- {
227
- type: "static",
228
- decision: "reject",
229
- weight: 0.5,
230
- reason: "Static test"
231
- }
232
- ]
233
- }
234
- }
235
- end
236
-
237
- before do
238
- allow(ab_test_manager).to receive(:assign_variant).and_return(assignment)
239
- allow(ab_test_manager).to receive(:record_decision)
240
- end
241
-
242
- it "builds JsonRuleEvaluator from version with rules" do
243
- allow(version_manager).to receive(:get_version).and_return(version_with_rules)
244
- agent = described_class.new(
245
- ab_test_manager: ab_test_manager,
246
- version_manager: version_manager
247
- )
248
-
249
- result = agent.decide(
250
- context: { status: "active" },
251
- ab_test_id: "test_1"
252
- )
253
-
254
- expect(result[:decision]).to eq("approve")
255
- end
256
-
257
- it "builds evaluators from version with evaluator config" do
258
- allow(version_manager).to receive(:get_version).and_return(version_with_evaluators)
259
- agent = described_class.new(
260
- ab_test_manager: ab_test_manager,
261
- version_manager: version_manager
262
- )
263
-
264
- result = agent.decide(
265
- context: { status: "active" },
266
- ab_test_id: "test_1"
267
- )
268
-
269
- expect(result[:decision]).to eq("approve")
270
- end
271
-
272
- it "builds StaticEvaluator from version config" do
273
- allow(version_manager).to receive(:get_version).and_return(version_with_static_evaluator)
274
- agent = described_class.new(
275
- ab_test_manager: ab_test_manager,
276
- version_manager: version_manager
277
- )
278
-
279
- result = agent.decide(
280
- context: { status: "inactive" },
281
- ab_test_id: "test_1"
282
- )
283
-
284
- expect(result[:decision]).to eq("reject")
285
- end
286
-
287
- it "falls back to base evaluators when version content is invalid" do
288
- invalid_version = { content: "invalid" }
289
- allow(version_manager).to receive(:get_version).and_return(invalid_version)
290
- agent = described_class.new(
291
- ab_test_manager: ab_test_manager,
292
- version_manager: version_manager,
293
- evaluators: [base_evaluator]
294
- )
295
-
296
- result = agent.decide(
297
- context: { status: "active" },
298
- ab_test_id: "test_1"
299
- )
300
-
301
- expect(result[:decision]).to eq("approve")
302
- end
303
-
304
- it "raises error for unknown evaluator type" do
305
- invalid_evaluator_version = {
306
- content: {
307
- evaluators: [
308
- {
309
- type: "unknown_type",
310
- config: {}
311
- }
312
- ]
313
- }
314
- }
315
- allow(version_manager).to receive(:get_version).and_return(invalid_evaluator_version)
316
- agent = described_class.new(
317
- ab_test_manager: ab_test_manager,
318
- version_manager: version_manager
319
- )
320
-
321
- expect do
322
- agent.decide(
323
- context: { status: "active" },
324
- ab_test_id: "test_1"
325
- )
326
- end.to raise_error(/Unknown evaluator type/)
327
- end
328
- end
329
-
330
- describe "#decide with Context object" do
331
- it "handles Context object instead of hash" do
332
- agent = described_class.new(
333
- ab_test_manager: ab_test_manager,
334
- evaluators: [base_evaluator]
335
- )
336
-
337
- context = DecisionAgent::Context.new({ user: "test" })
338
- result = agent.decide(context: context)
339
-
340
- expect(result[:decision]).to eq("approve")
341
- end
342
- end
343
-
344
- describe "initialization with optional parameters" do
345
- it "initializes with scoring_strategy" do
346
- scoring_strategy = double("ScoringStrategy")
347
- agent = described_class.new(
348
- ab_test_manager: ab_test_manager,
349
- scoring_strategy: scoring_strategy
350
- )
351
-
352
- expect(agent.instance_variable_get(:@scoring_strategy)).to eq(scoring_strategy)
353
- end
354
-
355
- it "initializes with audit_adapter" do
356
- audit_adapter = double("AuditAdapter")
357
- agent = described_class.new(
358
- ab_test_manager: ab_test_manager,
359
- audit_adapter: audit_adapter
360
- )
361
-
362
- expect(agent.instance_variable_get(:@audit_adapter)).to eq(audit_adapter)
363
- end
364
- end
365
-
366
- describe "#build_agent" do
367
- it "uses base evaluators when provided evaluators are empty" do
368
- agent = described_class.new(
369
- ab_test_manager: ab_test_manager,
370
- evaluators: [base_evaluator]
371
- )
372
-
373
- # Access private method via send
374
- built_agent = agent.send(:build_agent, [])
375
- expect(built_agent).to be_a(DecisionAgent::Agent)
376
- end
377
-
378
- it "uses provided evaluators when not empty" do
379
- custom_evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(
380
- decision: "reject",
381
- weight: 0.5
382
- )
383
- agent = described_class.new(
384
- ab_test_manager: ab_test_manager,
385
- evaluators: [base_evaluator]
386
- )
387
-
388
- built_agent = agent.send(:build_agent, [custom_evaluator])
389
- expect(built_agent).to be_a(DecisionAgent::Agent)
390
- end
391
- end
392
-
393
- describe "#build_evaluators_from_version" do
394
- it "falls back to base evaluators when content is not a hash" do
395
- invalid_version = { content: "not a hash" }
396
- allow(version_manager).to receive(:get_version).and_return(invalid_version)
397
- agent = described_class.new(
398
- ab_test_manager: ab_test_manager,
399
- version_manager: version_manager,
400
- evaluators: [base_evaluator]
401
- )
402
-
403
- evaluators = agent.send(:build_evaluators_from_version, invalid_version)
404
- expect(evaluators).to eq([base_evaluator])
405
- end
406
-
407
- it "falls back to base evaluators when content hash has no evaluators or rules" do
408
- invalid_version = { content: { other_key: "value" } }
409
- allow(version_manager).to receive(:get_version).and_return(invalid_version)
410
- agent = described_class.new(
411
- ab_test_manager: ab_test_manager,
412
- version_manager: version_manager,
413
- evaluators: [base_evaluator]
414
- )
415
-
416
- evaluators = agent.send(:build_evaluators_from_version, invalid_version)
417
- expect(evaluators).to eq([base_evaluator])
418
- end
419
- end
420
-
421
- describe "#build_evaluator_from_config" do
422
- it "builds JsonRuleEvaluator from config" do
423
- agent = described_class.new(
424
- ab_test_manager: ab_test_manager,
425
- version_manager: version_manager
426
- )
427
-
428
- config = {
429
- type: "json_rule",
430
- rules: {
431
- version: "1.0",
432
- ruleset: "test",
433
- rules: [
434
- {
435
- id: "rule_1",
436
- if: { field: "status", op: "eq", value: "active" },
437
- then: { decision: "approve", weight: 0.9 }
438
- }
439
- ]
440
- }
441
- }
442
-
443
- evaluator = agent.send(:build_evaluator_from_config, config)
444
- expect(evaluator).to be_a(DecisionAgent::Evaluators::JsonRuleEvaluator)
445
- end
446
-
447
- it "builds StaticEvaluator from config with default weight" do
448
- agent = described_class.new(
449
- ab_test_manager: ab_test_manager,
450
- version_manager: version_manager
451
- )
452
-
453
- config = {
454
- type: "static",
455
- decision: "approve",
456
- reason: "Test reason"
457
- # weight not provided, should default to 1.0
458
- }
459
-
460
- evaluator = agent.send(:build_evaluator_from_config, config)
461
- expect(evaluator).to be_a(DecisionAgent::Evaluators::StaticEvaluator)
462
- expect(evaluator.decision).to eq("approve")
463
- end
464
-
465
- it "builds StaticEvaluator from config with custom weight" do
466
- agent = described_class.new(
467
- ab_test_manager: ab_test_manager,
468
- version_manager: version_manager
469
- )
470
-
471
- config = {
472
- type: "static",
473
- decision: "reject",
474
- weight: 0.7,
475
- reason: "Custom weight"
476
- }
477
-
478
- evaluator = agent.send(:build_evaluator_from_config, config)
479
- expect(evaluator).to be_a(DecisionAgent::Evaluators::StaticEvaluator)
480
- expect(evaluator.decision).to eq("reject")
481
- end
482
- end
483
-
484
- describe "integration tests with real version manager" do
485
- let(:temp_dir) { Dir.mktmpdir }
486
- let(:file_storage_adapter) do
487
- DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: temp_dir)
488
- end
489
- let(:real_version_manager) do
490
- DecisionAgent::Versioning::VersionManager.new(adapter: file_storage_adapter)
491
- end
492
- let(:storage_adapter) { DecisionAgent::ABTesting::Storage::MemoryAdapter.new }
493
- let(:real_ab_test_manager) do
494
- DecisionAgent::ABTesting::ABTestManager.new(
495
- storage_adapter: storage_adapter,
496
- version_manager: real_version_manager
497
- )
498
- end
499
-
500
- before do
501
- # Create test versions with real rules
502
- @champion_version = real_version_manager.save_version(
503
- rule_id: "approval_rule",
504
- rule_content: {
505
- version: "1.0",
506
- ruleset: "approval",
507
- rules: [{
508
- id: "rule_1",
509
- if: { field: "amount", op: "gt", value: 100 },
510
- then: { decision: "approve", weight: 0.9, reason: "Champion rule" }
511
- }]
512
- },
513
- created_by: "spec",
514
- changelog: "Champion version"
515
- )
516
-
517
- @challenger_version = real_version_manager.save_version(
518
- rule_id: "approval_rule",
519
- rule_content: {
520
- version: "1.0",
521
- ruleset: "approval",
522
- rules: [{
523
- id: "rule_1",
524
- if: { field: "amount", op: "gt", value: 200 },
525
- then: { decision: "approve", weight: 0.95, reason: "Challenger rule" }
526
- }]
527
- },
528
- created_by: "spec",
529
- changelog: "Challenger version"
530
- )
531
-
532
- @ab_test = real_ab_test_manager.create_test(
533
- name: "Approval Threshold Test",
534
- champion_version_id: @champion_version[:id],
535
- challenger_version_id: @challenger_version[:id],
536
- traffic_split: { champion: 50, challenger: 50 }
537
- )
538
- end
539
-
540
- after do
541
- FileUtils.rm_rf(temp_dir)
542
- end
543
-
544
- it "uses real version manager to get version content" do
545
- agent = described_class.new(
546
- ab_test_manager: real_ab_test_manager,
547
- version_manager: real_version_manager
548
- )
549
-
550
- result = agent.decide(
551
- context: { amount: 150 },
552
- ab_test_id: @ab_test.id,
553
- user_id: "user_1"
554
- )
555
-
556
- expect(result[:decision]).to eq("approve")
557
- expect(result[:ab_test]).not_to be_nil
558
- expect(result[:ab_test][:test_id]).to eq(@ab_test.id)
559
- end
560
-
561
- it "builds real JsonRuleEvaluator from version content" do
562
- agent = described_class.new(
563
- ab_test_manager: real_ab_test_manager,
564
- version_manager: real_version_manager
565
- )
566
-
567
- # Test with amount that matches champion (100 < amount < 200)
568
- result = agent.decide(
569
- context: { amount: 150 },
570
- ab_test_id: @ab_test.id,
571
- user_id: "user_1"
572
- )
573
-
574
- expect(result[:decision]).to eq("approve")
575
- expect(result[:confidence]).to be > 0
576
- end
577
-
578
- it "handles variant assignment with real version manager" do
579
- agent = described_class.new(
580
- ab_test_manager: real_ab_test_manager,
581
- version_manager: real_version_manager
582
- )
583
-
584
- # Make multiple decisions to test variant assignment
585
- # Use amount 250 which will match both champion (> 100) and challenger (> 200) rules
586
- results = []
587
- 10.times do |i|
588
- result = agent.decide(
589
- context: { amount: 250 },
590
- ab_test_id: @ab_test.id,
591
- user_id: "user_#{i}"
592
- )
593
- results << result
594
- end
595
-
596
- # At least one decision should have been made
597
- expect(results.size).to eq(10)
598
- # All should have ab_test information
599
- expect(results.all? { |r| r[:ab_test] }).to be true
600
- end
601
-
602
- it "records decisions with real ab_test_manager" do
603
- agent = described_class.new(
604
- ab_test_manager: real_ab_test_manager,
605
- version_manager: real_version_manager
606
- )
607
-
608
- result = agent.decide(
609
- context: { amount: 150 },
610
- ab_test_id: @ab_test.id,
611
- user_id: "user_1"
612
- )
613
-
614
- # Verify decision was recorded (check that results are available)
615
- expect(result[:decision]).to eq("approve")
616
- expect(result[:ab_test]).not_to be_nil
617
- end
618
-
619
- it "handles version with evaluators configuration" do
620
- version_with_evaluators = real_version_manager.save_version(
621
- rule_id: "test_evaluator_rule",
622
- rule_content: {
623
- evaluators: [{
624
- type: "static",
625
- decision: "approve",
626
- weight: 0.8,
627
- reason: "Static evaluator from version"
628
- }]
629
- },
630
- created_by: "spec",
631
- changelog: "Version with evaluators"
632
- )
633
-
634
- static_test = real_ab_test_manager.create_test(
635
- name: "Static Evaluator Test",
636
- champion_version_id: @champion_version[:id],
637
- challenger_version_id: version_with_evaluators[:id],
638
- traffic_split: { champion: 50, challenger: 50 }
639
- )
640
-
641
- agent = described_class.new(
642
- ab_test_manager: real_ab_test_manager,
643
- version_manager: real_version_manager
644
- )
645
-
646
- result = agent.decide(
647
- context: { amount: 50 }, # Low amount that won't match champion rule
648
- ab_test_id: static_test.id,
649
- user_id: "user_1"
650
- )
651
-
652
- expect(result[:decision]).to eq("approve")
653
- end
654
- end
655
- end
@@ -1,64 +0,0 @@
1
- require "spec_helper"
2
- require_relative "../../../lib/decision_agent/ab_testing/storage/adapter"
3
-
4
- RSpec.describe DecisionAgent::ABTesting::Storage::Adapter do
5
- let(:adapter) { described_class.new }
6
-
7
- describe "#save_test" do
8
- it "raises NotImplementedError" do
9
- test = double("ABTest")
10
- expect { adapter.save_test(test) }.to raise_error(NotImplementedError, /must implement #save_test/)
11
- end
12
- end
13
-
14
- describe "#get_test" do
15
- it "raises NotImplementedError" do
16
- expect { adapter.get_test("test_id") }.to raise_error(NotImplementedError, /must implement #get_test/)
17
- end
18
- end
19
-
20
- describe "#update_test" do
21
- it "raises NotImplementedError" do
22
- expect { adapter.update_test("test_id", {}) }.to raise_error(NotImplementedError, /must implement #update_test/)
23
- end
24
- end
25
-
26
- describe "#list_tests" do
27
- it "raises NotImplementedError" do
28
- expect { adapter.list_tests }.to raise_error(NotImplementedError, /must implement #list_tests/)
29
- end
30
-
31
- it "raises NotImplementedError with status filter" do
32
- expect { adapter.list_tests(status: "active") }.to raise_error(NotImplementedError, /must implement #list_tests/)
33
- end
34
-
35
- it "raises NotImplementedError with limit" do
36
- expect { adapter.list_tests(limit: 10) }.to raise_error(NotImplementedError, /must implement #list_tests/)
37
- end
38
- end
39
-
40
- describe "#save_assignment" do
41
- it "raises NotImplementedError" do
42
- assignment = double("ABTestAssignment")
43
- expect { adapter.save_assignment(assignment) }.to raise_error(NotImplementedError, /must implement #save_assignment/)
44
- end
45
- end
46
-
47
- describe "#update_assignment" do
48
- it "raises NotImplementedError" do
49
- expect { adapter.update_assignment("assignment_id", {}) }.to raise_error(NotImplementedError, /must implement #update_assignment/)
50
- end
51
- end
52
-
53
- describe "#get_assignments" do
54
- it "raises NotImplementedError" do
55
- expect { adapter.get_assignments("test_id") }.to raise_error(NotImplementedError, /must implement #get_assignments/)
56
- end
57
- end
58
-
59
- describe "#delete_test" do
60
- it "raises NotImplementedError" do
61
- expect { adapter.delete_test("test_id") }.to raise_error(NotImplementedError, /must implement #delete_test/)
62
- end
63
- end
64
- end