decision_agent 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,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