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