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,648 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe "DSL Validation" do
4
- describe DecisionAgent::Dsl::SchemaValidator do
5
- describe "root structure validation" do
6
- it "rejects non-hash input" do
7
- expect do
8
- DecisionAgent::Dsl::SchemaValidator.validate!([1, 2, 3])
9
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Root element must be a hash/)
10
- end
11
-
12
- it "rejects string input" do
13
- expect do
14
- DecisionAgent::Dsl::SchemaValidator.validate!("not a hash")
15
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Root element must be a hash/)
16
- end
17
-
18
- it "accepts valid hash input" do
19
- valid_rules = {
20
- "version" => "1.0",
21
- "rules" => []
22
- }
23
-
24
- expect do
25
- DecisionAgent::Dsl::SchemaValidator.validate!(valid_rules)
26
- end.not_to raise_error
27
- end
28
- end
29
-
30
- describe "version validation" do
31
- it "requires version field" do
32
- rules = {
33
- "rules" => []
34
- }
35
-
36
- expect do
37
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
38
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'version'/)
39
- end
40
-
41
- it "accepts version as symbol key" do
42
- rules = {
43
- version: "1.0",
44
- rules: []
45
- }
46
-
47
- expect do
48
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
49
- end.not_to raise_error
50
- end
51
- end
52
-
53
- describe "rules array validation" do
54
- it "requires rules field" do
55
- rules = {
56
- "version" => "1.0"
57
- }
58
-
59
- expect do
60
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
61
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'rules'/)
62
- end
63
-
64
- it "rejects non-array rules" do
65
- rules = {
66
- "version" => "1.0",
67
- "rules" => "not an array"
68
- }
69
-
70
- expect do
71
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
72
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /must be an array/)
73
- end
74
-
75
- it "accepts empty rules array" do
76
- rules = {
77
- "version" => "1.0",
78
- "rules" => []
79
- }
80
-
81
- expect do
82
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
83
- end.not_to raise_error
84
- end
85
- end
86
-
87
- describe "rule structure validation" do
88
- it "rejects non-hash rule" do
89
- rules = {
90
- "version" => "1.0",
91
- "rules" => ["not a hash"]
92
- }
93
-
94
- expect do
95
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
96
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /rules\[0\].*must be a hash/)
97
- end
98
-
99
- it "requires rule id" do
100
- rules = {
101
- "version" => "1.0",
102
- "rules" => [
103
- {
104
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
105
- "then" => { "decision" => "approve" }
106
- }
107
- ]
108
- }
109
-
110
- expect do
111
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
112
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'id'/)
113
- end
114
-
115
- it "requires rule if clause" do
116
- rules = {
117
- "version" => "1.0",
118
- "rules" => [
119
- {
120
- "id" => "rule_1",
121
- "then" => { "decision" => "approve" }
122
- }
123
- ]
124
- }
125
-
126
- expect do
127
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
128
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'if'/)
129
- end
130
-
131
- it "requires rule then clause" do
132
- rules = {
133
- "version" => "1.0",
134
- "rules" => [
135
- {
136
- "id" => "rule_1",
137
- "if" => { "field" => "status", "op" => "eq", "value" => "active" }
138
- }
139
- ]
140
- }
141
-
142
- expect do
143
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
144
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'then'/)
145
- end
146
- end
147
-
148
- describe "condition validation" do
149
- it "rejects condition without field, all, or any" do
150
- rules = {
151
- "version" => "1.0",
152
- "rules" => [
153
- {
154
- "id" => "rule_1",
155
- "if" => { "invalid" => "condition" },
156
- "then" => { "decision" => "approve" }
157
- }
158
- ]
159
- }
160
-
161
- expect do
162
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
163
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Condition must have one of: 'field', 'all', or 'any'/)
164
- end
165
-
166
- it "rejects non-hash condition" do
167
- rules = {
168
- "version" => "1.0",
169
- "rules" => [
170
- {
171
- "id" => "rule_1",
172
- "if" => "not a hash",
173
- "then" => { "decision" => "approve" }
174
- }
175
- ]
176
- }
177
-
178
- expect do
179
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
180
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Condition must be a hash/)
181
- end
182
- end
183
-
184
- describe "field condition validation" do
185
- it "requires field key" do
186
- rules = {
187
- "version" => "1.0",
188
- "rules" => [
189
- {
190
- "id" => "rule_1",
191
- "if" => { "op" => "eq", "value" => "active" },
192
- "then" => { "decision" => "approve" }
193
- }
194
- ]
195
- }
196
-
197
- expect do
198
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
199
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
200
- expect(error.message).to match(/Condition must have one of: 'field', 'all', or 'any'/)
201
- end
202
- end
203
-
204
- it "requires op (operator) key" do
205
- rules = {
206
- "version" => "1.0",
207
- "rules" => [
208
- {
209
- "id" => "rule_1",
210
- "if" => { "field" => "status", "value" => "active" },
211
- "then" => { "decision" => "approve" }
212
- }
213
- ]
214
- }
215
-
216
- expect do
217
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
218
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /missing 'op'/)
219
- end
220
-
221
- it "validates operator is supported" do
222
- rules = {
223
- "version" => "1.0",
224
- "rules" => [
225
- {
226
- "id" => "rule_1",
227
- "if" => { "field" => "status", "op" => "invalid_op", "value" => "active" },
228
- "then" => { "decision" => "approve" }
229
- }
230
- ]
231
- }
232
-
233
- expect do
234
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
235
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
236
- expect(error.message).to include("Unsupported operator 'invalid_op'")
237
- expect(error.message).to include("eq, neq, gt, gte, lt, lte, in, present, blank")
238
- end
239
- end
240
-
241
- it "requires value for non-present/blank operators" do
242
- rules = {
243
- "version" => "1.0",
244
- "rules" => [
245
- {
246
- "id" => "rule_1",
247
- "if" => { "field" => "status", "op" => "eq" },
248
- "then" => { "decision" => "approve" }
249
- }
250
- ]
251
- }
252
-
253
- expect do
254
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
255
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /missing 'value' key/)
256
- end
257
-
258
- it "allows missing value for present operator" do
259
- rules = {
260
- "version" => "1.0",
261
- "rules" => [
262
- {
263
- "id" => "rule_1",
264
- "if" => { "field" => "assignee", "op" => "present" },
265
- "then" => { "decision" => "assigned" }
266
- }
267
- ]
268
- }
269
-
270
- expect do
271
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
272
- end.not_to raise_error
273
- end
274
-
275
- it "allows missing value for blank operator" do
276
- rules = {
277
- "version" => "1.0",
278
- "rules" => [
279
- {
280
- "id" => "rule_1",
281
- "if" => { "field" => "description", "op" => "blank" },
282
- "then" => { "decision" => "needs_info" }
283
- }
284
- ]
285
- }
286
-
287
- expect do
288
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
289
- end.not_to raise_error
290
- end
291
-
292
- it "rejects empty field path" do
293
- rules = {
294
- "version" => "1.0",
295
- "rules" => [
296
- {
297
- "id" => "rule_1",
298
- "if" => { "field" => "", "op" => "eq", "value" => "test" },
299
- "then" => { "decision" => "approve" }
300
- }
301
- ]
302
- }
303
-
304
- expect do
305
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
306
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Field path cannot be empty/)
307
- end
308
-
309
- it "rejects invalid dot-notation" do
310
- rules = {
311
- "version" => "1.0",
312
- "rules" => [
313
- {
314
- "id" => "rule_1",
315
- "if" => { "field" => "user..role", "op" => "eq", "value" => "admin" },
316
- "then" => { "decision" => "approve" }
317
- }
318
- ]
319
- }
320
-
321
- expect do
322
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
323
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /cannot have empty segments/)
324
- end
325
-
326
- it "accepts valid dot-notation" do
327
- rules = {
328
- "version" => "1.0",
329
- "rules" => [
330
- {
331
- "id" => "rule_1",
332
- "if" => { "field" => "user.profile.role", "op" => "eq", "value" => "admin" },
333
- "then" => { "decision" => "allow" }
334
- }
335
- ]
336
- }
337
-
338
- expect do
339
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
340
- end.not_to raise_error
341
- end
342
- end
343
-
344
- describe "all/any condition validation" do
345
- it "requires array for all condition" do
346
- rules = {
347
- "version" => "1.0",
348
- "rules" => [
349
- {
350
- "id" => "rule_1",
351
- "if" => { "all" => "not an array" },
352
- "then" => { "decision" => "approve" }
353
- }
354
- ]
355
- }
356
-
357
- expect do
358
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
359
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /'all' condition must contain an array/)
360
- end
361
-
362
- it "requires array for any condition" do
363
- rules = {
364
- "version" => "1.0",
365
- "rules" => [
366
- {
367
- "id" => "rule_1",
368
- "if" => { "any" => "not an array" },
369
- "then" => { "decision" => "approve" }
370
- }
371
- ]
372
- }
373
-
374
- expect do
375
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
376
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /'any' condition must contain an array/)
377
- end
378
-
379
- it "validates nested conditions in all" do
380
- rules = {
381
- "version" => "1.0",
382
- "rules" => [
383
- {
384
- "id" => "rule_1",
385
- "if" => {
386
- "all" => [
387
- { "field" => "status", "op" => "invalid_op", "value" => "active" }
388
- ]
389
- },
390
- "then" => { "decision" => "approve" }
391
- }
392
- ]
393
- }
394
-
395
- expect do
396
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
397
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Unsupported operator/)
398
- end
399
-
400
- it "validates nested conditions in any" do
401
- rules = {
402
- "version" => "1.0",
403
- "rules" => [
404
- {
405
- "id" => "rule_1",
406
- "if" => {
407
- "any" => [
408
- { "field" => "priority" } # Missing op
409
- ]
410
- },
411
- "then" => { "decision" => "escalate" }
412
- }
413
- ]
414
- }
415
-
416
- expect do
417
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
418
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /missing 'op'/)
419
- end
420
- end
421
-
422
- describe "then clause validation" do
423
- it "requires then clause to be a hash" do
424
- rules = {
425
- "version" => "1.0",
426
- "rules" => [
427
- {
428
- "id" => "rule_1",
429
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
430
- "then" => "not a hash"
431
- }
432
- ]
433
- }
434
-
435
- expect do
436
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
437
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /then.*Must be a hash/)
438
- end
439
-
440
- it "requires decision field in then clause" do
441
- rules = {
442
- "version" => "1.0",
443
- "rules" => [
444
- {
445
- "id" => "rule_1",
446
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
447
- "then" => { "weight" => 0.8 }
448
- }
449
- ]
450
- }
451
-
452
- expect do
453
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
454
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'decision'/)
455
- end
456
-
457
- it "validates weight is numeric" do
458
- rules = {
459
- "version" => "1.0",
460
- "rules" => [
461
- {
462
- "id" => "rule_1",
463
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
464
- "then" => { "decision" => "approve", "weight" => "not a number" }
465
- }
466
- ]
467
- }
468
-
469
- expect do
470
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
471
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /weight.*Must be a number/)
472
- end
473
-
474
- it "validates weight is between 0 and 1" do
475
- rules = {
476
- "version" => "1.0",
477
- "rules" => [
478
- {
479
- "id" => "rule_1",
480
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
481
- "then" => { "decision" => "approve", "weight" => 1.5 }
482
- }
483
- ]
484
- }
485
-
486
- expect do
487
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
488
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /weight.*between 0.0 and 1.0/)
489
- end
490
-
491
- it "validates reason is a string" do
492
- rules = {
493
- "version" => "1.0",
494
- "rules" => [
495
- {
496
- "id" => "rule_1",
497
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
498
- "then" => { "decision" => "approve", "reason" => 123 }
499
- }
500
- ]
501
- }
502
-
503
- expect do
504
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
505
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /reason.*Must be a string/)
506
- end
507
-
508
- it "accepts valid then clause with all fields" do
509
- rules = {
510
- "version" => "1.0",
511
- "rules" => [
512
- {
513
- "id" => "rule_1",
514
- "if" => { "field" => "status", "op" => "eq", "value" => "active" },
515
- "then" => {
516
- "decision" => "approve",
517
- "weight" => 0.8,
518
- "reason" => "Status is active"
519
- }
520
- }
521
- ]
522
- }
523
-
524
- expect do
525
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
526
- end.not_to raise_error
527
- end
528
- end
529
-
530
- describe "error message formatting" do
531
- it "provides numbered error list for multiple errors" do
532
- rules = {
533
- "version" => "1.0",
534
- "rules" => [
535
- {
536
- "id" => "rule_1"
537
- # Missing if clause
538
- # Missing then clause
539
- }
540
- ]
541
- }
542
-
543
- expect do
544
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
545
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
546
- expect(error.message).to include("1.")
547
- expect(error.message).to include("2.")
548
- expect(error.message).to match(/validation failed with 2 errors/)
549
- end
550
- end
551
-
552
- it "includes helpful context in error messages" do
553
- rules = {
554
- "version" => "1.0",
555
- "rules" => [
556
- {
557
- "id" => "rule_1",
558
- "if" => { "field" => "status", "op" => "invalid_op", "value" => "test" },
559
- "then" => { "decision" => "approve" }
560
- }
561
- ]
562
- }
563
-
564
- expect do
565
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
566
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
567
- expect(error.message).to include("rules[0].if")
568
- expect(error.message).to include("Supported operators:")
569
- end
570
- end
571
- end
572
-
573
- describe "complex nested validation" do
574
- it "validates deeply nested all/any structures" do
575
- rules = {
576
- "version" => "1.0",
577
- "rules" => [
578
- {
579
- "id" => "rule_1",
580
- "if" => {
581
- "all" => [
582
- {
583
- "any" => [
584
- { "field" => "a", "op" => "eq", "value" => 1 },
585
- { "field" => "b", "op" => "invalid_op", "value" => 2 }
586
- ]
587
- }
588
- ]
589
- },
590
- "then" => { "decision" => "approve" }
591
- }
592
- ]
593
- }
594
-
595
- expect do
596
- DecisionAgent::Dsl::SchemaValidator.validate!(rules)
597
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
598
- expect(error.message).to include("rules[0].if.all[0].any[1]")
599
- expect(error.message).to include("Unsupported operator")
600
- end
601
- end
602
- end
603
- end
604
-
605
- describe "RuleParser integration" do
606
- it "uses SchemaValidator for validation" do
607
- invalid_json = '{"version": "1.0", "rules": "not an array"}'
608
-
609
- expect do
610
- DecisionAgent::Dsl::RuleParser.parse(invalid_json)
611
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /must be an array/)
612
- end
613
-
614
- it "provides helpful error for malformed JSON" do
615
- malformed_json = '{"version": "1.0", "rules": [,,,]}'
616
-
617
- expect do
618
- DecisionAgent::Dsl::RuleParser.parse(malformed_json)
619
- end.to raise_error(DecisionAgent::InvalidRuleDslError) do |error|
620
- expect(error.message).to include("Invalid JSON syntax")
621
- expect(error.message).to include("Common issues")
622
- end
623
- end
624
-
625
- it "accepts hash input" do
626
- rules_hash = {
627
- version: "1.0",
628
- rules: [
629
- {
630
- id: "rule_1",
631
- if: { field: "status", op: "eq", value: "active" },
632
- then: { decision: "approve" }
633
- }
634
- ]
635
- }
636
-
637
- expect do
638
- DecisionAgent::Dsl::RuleParser.parse(rules_hash)
639
- end.not_to raise_error
640
- end
641
-
642
- it "rejects invalid input types" do
643
- expect do
644
- DecisionAgent::Dsl::RuleParser.parse(12_345)
645
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Expected JSON string or Hash/)
646
- end
647
- end
648
- end