decision_agent 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -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