decision_agent 0.3.0 → 1.1.0

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. 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