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,3150 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe "Advanced DSL Operators" do
4
- # STRING OPERATORS
5
- describe "string operators" do
6
- describe "contains operator" do
7
- it "matches when string contains substring" do
8
- rules = {
9
- version: "1.0",
10
- ruleset: "test",
11
- rules: [
12
- {
13
- id: "rule_1",
14
- if: { field: "message", op: "contains", value: "error" },
15
- then: { decision: "alert", reason: "Error found in message" }
16
- }
17
- ]
18
- }
19
-
20
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
21
- context = DecisionAgent::Context.new({ message: "An error occurred" })
22
-
23
- evaluation = evaluator.evaluate(context)
24
-
25
- expect(evaluation).not_to be_nil
26
- expect(evaluation.decision).to eq("alert")
27
- end
28
-
29
- it "does not match when substring is not present" do
30
- rules = {
31
- version: "1.0",
32
- ruleset: "test",
33
- rules: [
34
- {
35
- id: "rule_1",
36
- if: { field: "message", op: "contains", value: "error" },
37
- then: { decision: "alert" }
38
- }
39
- ]
40
- }
41
-
42
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
43
- context = DecisionAgent::Context.new({ message: "Success" })
44
-
45
- evaluation = evaluator.evaluate(context)
46
-
47
- expect(evaluation).to be_nil
48
- end
49
-
50
- it "is case-sensitive" do
51
- rules = {
52
- version: "1.0",
53
- ruleset: "test",
54
- rules: [
55
- {
56
- id: "rule_1",
57
- if: { field: "message", op: "contains", value: "ERROR" },
58
- then: { decision: "alert" }
59
- }
60
- ]
61
- }
62
-
63
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
64
- context = DecisionAgent::Context.new({ message: "An error occurred" })
65
-
66
- evaluation = evaluator.evaluate(context)
67
-
68
- expect(evaluation).to be_nil
69
- end
70
- end
71
-
72
- describe "starts_with operator" do
73
- it "matches when string starts with prefix" do
74
- rules = {
75
- version: "1.0",
76
- ruleset: "test",
77
- rules: [
78
- {
79
- id: "rule_1",
80
- if: { field: "code", op: "starts_with", value: "ERR" },
81
- then: { decision: "error_handler" }
82
- }
83
- ]
84
- }
85
-
86
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
87
- context = DecisionAgent::Context.new({ code: "ERR_404" })
88
-
89
- evaluation = evaluator.evaluate(context)
90
-
91
- expect(evaluation).not_to be_nil
92
- expect(evaluation.decision).to eq("error_handler")
93
- end
94
-
95
- it "does not match when prefix is not present" do
96
- rules = {
97
- version: "1.0",
98
- ruleset: "test",
99
- rules: [
100
- {
101
- id: "rule_1",
102
- if: { field: "code", op: "starts_with", value: "ERR" },
103
- then: { decision: "error_handler" }
104
- }
105
- ]
106
- }
107
-
108
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
109
- context = DecisionAgent::Context.new({ code: "SUCCESS_200" })
110
-
111
- evaluation = evaluator.evaluate(context)
112
-
113
- expect(evaluation).to be_nil
114
- end
115
- end
116
-
117
- describe "ends_with operator" do
118
- it "matches when string ends with suffix" do
119
- rules = {
120
- version: "1.0",
121
- ruleset: "test",
122
- rules: [
123
- {
124
- id: "rule_1",
125
- if: { field: "filename", op: "ends_with", value: ".pdf" },
126
- then: { decision: "process_pdf" }
127
- }
128
- ]
129
- }
130
-
131
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
132
- context = DecisionAgent::Context.new({ filename: "document.pdf" })
133
-
134
- evaluation = evaluator.evaluate(context)
135
-
136
- expect(evaluation).not_to be_nil
137
- expect(evaluation.decision).to eq("process_pdf")
138
- end
139
-
140
- it "does not match when suffix is not present" do
141
- rules = {
142
- version: "1.0",
143
- ruleset: "test",
144
- rules: [
145
- {
146
- id: "rule_1",
147
- if: { field: "filename", op: "ends_with", value: ".pdf" },
148
- then: { decision: "process_pdf" }
149
- }
150
- ]
151
- }
152
-
153
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
154
- context = DecisionAgent::Context.new({ filename: "document.txt" })
155
-
156
- evaluation = evaluator.evaluate(context)
157
-
158
- expect(evaluation).to be_nil
159
- end
160
- end
161
-
162
- describe "matches operator" do
163
- it "matches string against regular expression" do
164
- rules = {
165
- version: "1.0",
166
- ruleset: "test",
167
- rules: [
168
- {
169
- id: "rule_1",
170
- if: { field: "email", op: "matches", value: "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$" },
171
- then: { decision: "valid_email" }
172
- }
173
- ]
174
- }
175
-
176
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
177
- context = DecisionAgent::Context.new({ email: "user@example.com" })
178
-
179
- evaluation = evaluator.evaluate(context)
180
-
181
- expect(evaluation).not_to be_nil
182
- expect(evaluation.decision).to eq("valid_email")
183
- end
184
-
185
- it "does not match when regex fails" do
186
- rules = {
187
- version: "1.0",
188
- ruleset: "test",
189
- rules: [
190
- {
191
- id: "rule_1",
192
- if: { field: "email", op: "matches", value: "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$" },
193
- then: { decision: "valid_email" }
194
- }
195
- ]
196
- }
197
-
198
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
199
- context = DecisionAgent::Context.new({ email: "invalid-email" })
200
-
201
- evaluation = evaluator.evaluate(context)
202
-
203
- expect(evaluation).to be_nil
204
- end
205
-
206
- it "handles invalid regex gracefully" do
207
- rules = {
208
- version: "1.0",
209
- ruleset: "test",
210
- rules: [
211
- {
212
- id: "rule_1",
213
- if: { field: "text", op: "matches", value: "[invalid(" },
214
- then: { decision: "match" }
215
- }
216
- ]
217
- }
218
-
219
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
220
- context = DecisionAgent::Context.new({ text: "test" })
221
-
222
- evaluation = evaluator.evaluate(context)
223
-
224
- expect(evaluation).to be_nil
225
- end
226
- end
227
- end
228
-
229
- # NUMERIC OPERATORS
230
- describe "numeric operators" do
231
- describe "between operator" do
232
- it "matches when value is between min and max (array format)" do
233
- rules = {
234
- version: "1.0",
235
- ruleset: "test",
236
- rules: [
237
- {
238
- id: "rule_1",
239
- if: { field: "age", op: "between", value: [18, 65] },
240
- then: { decision: "eligible" }
241
- }
242
- ]
243
- }
244
-
245
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
246
- context = DecisionAgent::Context.new({ age: 30 })
247
-
248
- evaluation = evaluator.evaluate(context)
249
-
250
- expect(evaluation).not_to be_nil
251
- expect(evaluation.decision).to eq("eligible")
252
- end
253
-
254
- it "matches when value is between min and max (hash format)" do
255
- rules = {
256
- version: "1.0",
257
- ruleset: "test",
258
- rules: [
259
- {
260
- id: "rule_1",
261
- if: { field: "score", op: "between", value: { min: 0, max: 100 } },
262
- then: { decision: "valid_score" }
263
- }
264
- ]
265
- }
266
-
267
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
268
- context = DecisionAgent::Context.new({ score: 75 })
269
-
270
- evaluation = evaluator.evaluate(context)
271
-
272
- expect(evaluation).not_to be_nil
273
- expect(evaluation.decision).to eq("valid_score")
274
- end
275
-
276
- it "includes boundary values" do
277
- rules = {
278
- version: "1.0",
279
- ruleset: "test",
280
- rules: [
281
- {
282
- id: "rule_1",
283
- if: { field: "value", op: "between", value: [10, 20] },
284
- then: { decision: "in_range" }
285
- }
286
- ]
287
- }
288
-
289
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
290
-
291
- context_min = DecisionAgent::Context.new({ value: 10 })
292
- evaluation_min = evaluator.evaluate(context_min)
293
- expect(evaluation_min).not_to be_nil
294
-
295
- context_max = DecisionAgent::Context.new({ value: 20 })
296
- evaluation_max = evaluator.evaluate(context_max)
297
- expect(evaluation_max).not_to be_nil
298
- end
299
-
300
- it "does not match when value is outside range" do
301
- rules = {
302
- version: "1.0",
303
- ruleset: "test",
304
- rules: [
305
- {
306
- id: "rule_1",
307
- if: { field: "value", op: "between", value: [10, 20] },
308
- then: { decision: "in_range" }
309
- }
310
- ]
311
- }
312
-
313
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
314
- context = DecisionAgent::Context.new({ value: 25 })
315
-
316
- evaluation = evaluator.evaluate(context)
317
-
318
- expect(evaluation).to be_nil
319
- end
320
- end
321
-
322
- describe "modulo operator" do
323
- it "matches when modulo condition is satisfied (array format)" do
324
- rules = {
325
- version: "1.0",
326
- ruleset: "test",
327
- rules: [
328
- {
329
- id: "rule_1",
330
- if: { field: "number", op: "modulo", value: [2, 0] },
331
- then: { decision: "even" }
332
- }
333
- ]
334
- }
335
-
336
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
337
- context = DecisionAgent::Context.new({ number: 10 })
338
-
339
- evaluation = evaluator.evaluate(context)
340
-
341
- expect(evaluation).not_to be_nil
342
- expect(evaluation.decision).to eq("even")
343
- end
344
-
345
- it "matches when modulo condition is satisfied (hash format)" do
346
- rules = {
347
- version: "1.0",
348
- ruleset: "test",
349
- rules: [
350
- {
351
- id: "rule_1",
352
- if: { field: "number", op: "modulo", value: { divisor: 3, remainder: 1 } },
353
- then: { decision: "match" }
354
- }
355
- ]
356
- }
357
-
358
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
359
- context = DecisionAgent::Context.new({ number: 7 })
360
-
361
- evaluation = evaluator.evaluate(context)
362
-
363
- expect(evaluation).not_to be_nil
364
- expect(evaluation.decision).to eq("match")
365
- end
366
-
367
- it "does not match when modulo condition fails" do
368
- rules = {
369
- version: "1.0",
370
- ruleset: "test",
371
- rules: [
372
- {
373
- id: "rule_1",
374
- if: { field: "number", op: "modulo", value: [2, 0] },
375
- then: { decision: "even" }
376
- }
377
- ]
378
- }
379
-
380
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
381
- context = DecisionAgent::Context.new({ number: 11 })
382
-
383
- evaluation = evaluator.evaluate(context)
384
-
385
- expect(evaluation).to be_nil
386
- end
387
- end
388
-
389
- # MATHEMATICAL FUNCTIONS
390
- describe "trigonometric functions" do
391
- describe "sin operator" do
392
- it "matches when sin(field_value) equals expected_value" do
393
- rules = {
394
- version: "1.0",
395
- ruleset: "test",
396
- rules: [
397
- {
398
- id: "rule_1",
399
- if: { field: "angle", op: "sin", value: 0.0 },
400
- then: { decision: "zero_angle" }
401
- }
402
- ]
403
- }
404
-
405
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
406
- context = DecisionAgent::Context.new({ angle: 0 })
407
-
408
- evaluation = evaluator.evaluate(context)
409
-
410
- expect(evaluation).not_to be_nil
411
- expect(evaluation.decision).to eq("zero_angle")
412
- end
413
-
414
- it "matches when sin(pi/2) equals 1" do
415
- rules = {
416
- version: "1.0",
417
- ruleset: "test",
418
- rules: [
419
- {
420
- id: "rule_1",
421
- if: { field: "angle", op: "sin", value: 1.0 },
422
- then: { decision: "right_angle" }
423
- }
424
- ]
425
- }
426
-
427
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
428
- context = DecisionAgent::Context.new({ angle: Math::PI / 2 })
429
-
430
- evaluation = evaluator.evaluate(context)
431
-
432
- expect(evaluation).not_to be_nil
433
- expect(evaluation.decision).to eq("right_angle")
434
- end
435
-
436
- it "does not match when sin value is different" do
437
- rules = {
438
- version: "1.0",
439
- ruleset: "test",
440
- rules: [
441
- {
442
- id: "rule_1",
443
- if: { field: "angle", op: "sin", value: 1.0 },
444
- then: { decision: "right_angle" }
445
- }
446
- ]
447
- }
448
-
449
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
450
- context = DecisionAgent::Context.new({ angle: Math::PI })
451
-
452
- evaluation = evaluator.evaluate(context)
453
-
454
- expect(evaluation).to be_nil
455
- end
456
- end
457
-
458
- describe "cos operator" do
459
- it "matches when cos(field_value) equals expected_value" do
460
- rules = {
461
- version: "1.0",
462
- ruleset: "test",
463
- rules: [
464
- {
465
- id: "rule_1",
466
- if: { field: "angle", op: "cos", value: 1.0 },
467
- then: { decision: "zero_angle" }
468
- }
469
- ]
470
- }
471
-
472
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
473
- context = DecisionAgent::Context.new({ angle: 0 })
474
-
475
- evaluation = evaluator.evaluate(context)
476
-
477
- expect(evaluation).not_to be_nil
478
- expect(evaluation.decision).to eq("zero_angle")
479
- end
480
-
481
- it "does not match when cos value is different" do
482
- rules = {
483
- version: "1.0",
484
- ruleset: "test",
485
- rules: [
486
- {
487
- id: "rule_1",
488
- if: { field: "angle", op: "cos", value: 1.0 },
489
- then: { decision: "zero_angle" }
490
- }
491
- ]
492
- }
493
-
494
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
495
- context = DecisionAgent::Context.new({ angle: Math::PI / 2 })
496
-
497
- evaluation = evaluator.evaluate(context)
498
-
499
- expect(evaluation).to be_nil
500
- end
501
- end
502
-
503
- describe "tan operator" do
504
- it "matches when tan(field_value) equals expected_value" do
505
- rules = {
506
- version: "1.0",
507
- ruleset: "test",
508
- rules: [
509
- {
510
- id: "rule_1",
511
- if: { field: "angle", op: "tan", value: 0.0 },
512
- then: { decision: "zero_tangent" }
513
- }
514
- ]
515
- }
516
-
517
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
518
- context = DecisionAgent::Context.new({ angle: 0 })
519
-
520
- evaluation = evaluator.evaluate(context)
521
-
522
- expect(evaluation).not_to be_nil
523
- expect(evaluation.decision).to eq("zero_tangent")
524
- end
525
-
526
- it "does not match when tan value is different" do
527
- rules = {
528
- version: "1.0",
529
- ruleset: "test",
530
- rules: [
531
- {
532
- id: "rule_1",
533
- if: { field: "angle", op: "tan", value: 0.0 },
534
- then: { decision: "zero_tangent" }
535
- }
536
- ]
537
- }
538
-
539
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
540
- context = DecisionAgent::Context.new({ angle: Math::PI / 4 })
541
-
542
- evaluation = evaluator.evaluate(context)
543
-
544
- expect(evaluation).to be_nil
545
- end
546
- end
547
- end
548
-
549
- describe "exponential and logarithmic functions" do
550
- describe "sqrt operator" do
551
- it "matches when sqrt(field_value) equals expected_value" do
552
- rules = {
553
- version: "1.0",
554
- ruleset: "test",
555
- rules: [
556
- {
557
- id: "rule_1",
558
- if: { field: "number", op: "sqrt", value: 3.0 },
559
- then: { decision: "square_root" }
560
- }
561
- ]
562
- }
563
-
564
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
565
- context = DecisionAgent::Context.new({ number: 9 })
566
-
567
- evaluation = evaluator.evaluate(context)
568
-
569
- expect(evaluation).not_to be_nil
570
- expect(evaluation.decision).to eq("square_root")
571
- end
572
-
573
- it "does not match when sqrt value is different" do
574
- rules = {
575
- version: "1.0",
576
- ruleset: "test",
577
- rules: [
578
- {
579
- id: "rule_1",
580
- if: { field: "number", op: "sqrt", value: 3.0 },
581
- then: { decision: "square_root" }
582
- }
583
- ]
584
- }
585
-
586
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
587
- context = DecisionAgent::Context.new({ number: 16 })
588
-
589
- evaluation = evaluator.evaluate(context)
590
-
591
- expect(evaluation).to be_nil
592
- end
593
-
594
- it "does not match for negative numbers" do
595
- rules = {
596
- version: "1.0",
597
- ruleset: "test",
598
- rules: [
599
- {
600
- id: "rule_1",
601
- if: { field: "number", op: "sqrt", value: 0.0 },
602
- then: { decision: "square_root" }
603
- }
604
- ]
605
- }
606
-
607
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
608
- context = DecisionAgent::Context.new({ number: -4 })
609
-
610
- evaluation = evaluator.evaluate(context)
611
-
612
- expect(evaluation).to be_nil
613
- end
614
- end
615
-
616
- describe "power operator" do
617
- it "matches when power(field_value, exponent) equals result (array format)" do
618
- rules = {
619
- version: "1.0",
620
- ruleset: "test",
621
- rules: [
622
- {
623
- id: "rule_1",
624
- if: { field: "base", op: "power", value: [2, 4] },
625
- then: { decision: "power_match" }
626
- }
627
- ]
628
- }
629
-
630
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
631
- context = DecisionAgent::Context.new({ base: 2 })
632
-
633
- evaluation = evaluator.evaluate(context)
634
-
635
- expect(evaluation).not_to be_nil
636
- expect(evaluation.decision).to eq("power_match")
637
- end
638
-
639
- it "matches when power(field_value, exponent) equals result (hash format)" do
640
- rules = {
641
- version: "1.0",
642
- ruleset: "test",
643
- rules: [
644
- {
645
- id: "rule_1",
646
- if: { field: "base", op: "power", value: { exponent: 3, result: 8 } },
647
- then: { decision: "power_match" }
648
- }
649
- ]
650
- }
651
-
652
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
653
- context = DecisionAgent::Context.new({ base: 2 })
654
-
655
- evaluation = evaluator.evaluate(context)
656
-
657
- expect(evaluation).not_to be_nil
658
- expect(evaluation.decision).to eq("power_match")
659
- end
660
-
661
- it "does not match when power result is different" do
662
- rules = {
663
- version: "1.0",
664
- ruleset: "test",
665
- rules: [
666
- {
667
- id: "rule_1",
668
- if: { field: "base", op: "power", value: [2, 4] },
669
- then: { decision: "power_match" }
670
- }
671
- ]
672
- }
673
-
674
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
675
- context = DecisionAgent::Context.new({ base: 3 })
676
-
677
- evaluation = evaluator.evaluate(context)
678
-
679
- expect(evaluation).to be_nil
680
- end
681
- end
682
-
683
- describe "exp operator" do
684
- it "matches when exp(field_value) equals expected_value" do
685
- rules = {
686
- version: "1.0",
687
- ruleset: "test",
688
- rules: [
689
- {
690
- id: "rule_1",
691
- if: { field: "exponent", op: "exp", value: Math::E },
692
- then: { decision: "e_power" }
693
- }
694
- ]
695
- }
696
-
697
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
698
- context = DecisionAgent::Context.new({ exponent: 1 })
699
-
700
- evaluation = evaluator.evaluate(context)
701
-
702
- expect(evaluation).not_to be_nil
703
- expect(evaluation.decision).to eq("e_power")
704
- end
705
-
706
- it "does not match when exp value is different" do
707
- rules = {
708
- version: "1.0",
709
- ruleset: "test",
710
- rules: [
711
- {
712
- id: "rule_1",
713
- if: { field: "exponent", op: "exp", value: Math::E },
714
- then: { decision: "e_power" }
715
- }
716
- ]
717
- }
718
-
719
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
720
- context = DecisionAgent::Context.new({ exponent: 2 })
721
-
722
- evaluation = evaluator.evaluate(context)
723
-
724
- expect(evaluation).to be_nil
725
- end
726
- end
727
-
728
- describe "log operator" do
729
- it "matches when log(field_value) equals expected_value" do
730
- rules = {
731
- version: "1.0",
732
- ruleset: "test",
733
- rules: [
734
- {
735
- id: "rule_1",
736
- if: { field: "number", op: "log", value: 0.0 },
737
- then: { decision: "log_one" }
738
- }
739
- ]
740
- }
741
-
742
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
743
- context = DecisionAgent::Context.new({ number: 1 })
744
-
745
- evaluation = evaluator.evaluate(context)
746
-
747
- expect(evaluation).not_to be_nil
748
- expect(evaluation.decision).to eq("log_one")
749
- end
750
-
751
- it "does not match when log value is different" do
752
- rules = {
753
- version: "1.0",
754
- ruleset: "test",
755
- rules: [
756
- {
757
- id: "rule_1",
758
- if: { field: "number", op: "log", value: 0.0 },
759
- then: { decision: "log_one" }
760
- }
761
- ]
762
- }
763
-
764
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
765
- context = DecisionAgent::Context.new({ number: 2 })
766
-
767
- evaluation = evaluator.evaluate(context)
768
-
769
- expect(evaluation).to be_nil
770
- end
771
-
772
- it "does not match for non-positive numbers" do
773
- rules = {
774
- version: "1.0",
775
- ruleset: "test",
776
- rules: [
777
- {
778
- id: "rule_1",
779
- if: { field: "number", op: "log", value: 0.0 },
780
- then: { decision: "log_one" }
781
- }
782
- ]
783
- }
784
-
785
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
786
- context = DecisionAgent::Context.new({ number: -1 })
787
-
788
- evaluation = evaluator.evaluate(context)
789
-
790
- expect(evaluation).to be_nil
791
- end
792
- end
793
- end
794
-
795
- describe "rounding and absolute value functions" do
796
- describe "round operator" do
797
- it "matches when round(field_value) equals expected_value" do
798
- rules = {
799
- version: "1.0",
800
- ruleset: "test",
801
- rules: [
802
- {
803
- id: "rule_1",
804
- if: { field: "value", op: "round", value: 3 },
805
- then: { decision: "rounded" }
806
- }
807
- ]
808
- }
809
-
810
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
811
- context = DecisionAgent::Context.new({ value: 3.4 })
812
-
813
- evaluation = evaluator.evaluate(context)
814
-
815
- expect(evaluation).not_to be_nil
816
- expect(evaluation.decision).to eq("rounded")
817
- end
818
-
819
- it "matches when rounding up" do
820
- rules = {
821
- version: "1.0",
822
- ruleset: "test",
823
- rules: [
824
- {
825
- id: "rule_1",
826
- if: { field: "value", op: "round", value: 4 },
827
- then: { decision: "rounded" }
828
- }
829
- ]
830
- }
831
-
832
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
833
- context = DecisionAgent::Context.new({ value: 3.6 })
834
-
835
- evaluation = evaluator.evaluate(context)
836
-
837
- expect(evaluation).not_to be_nil
838
- expect(evaluation.decision).to eq("rounded")
839
- end
840
-
841
- it "does not match when rounded value is different" do
842
- rules = {
843
- version: "1.0",
844
- ruleset: "test",
845
- rules: [
846
- {
847
- id: "rule_1",
848
- if: { field: "value", op: "round", value: 3 },
849
- then: { decision: "rounded" }
850
- }
851
- ]
852
- }
853
-
854
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
855
- context = DecisionAgent::Context.new({ value: 2.3 })
856
-
857
- evaluation = evaluator.evaluate(context)
858
-
859
- expect(evaluation).to be_nil
860
- end
861
- end
862
-
863
- describe "floor operator" do
864
- it "matches when floor(field_value) equals expected_value" do
865
- rules = {
866
- version: "1.0",
867
- ruleset: "test",
868
- rules: [
869
- {
870
- id: "rule_1",
871
- if: { field: "value", op: "floor", value: 3 },
872
- then: { decision: "floored" }
873
- }
874
- ]
875
- }
876
-
877
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
878
- context = DecisionAgent::Context.new({ value: 3.9 })
879
-
880
- evaluation = evaluator.evaluate(context)
881
-
882
- expect(evaluation).not_to be_nil
883
- expect(evaluation.decision).to eq("floored")
884
- end
885
-
886
- it "does not match when floor value is different" do
887
- rules = {
888
- version: "1.0",
889
- ruleset: "test",
890
- rules: [
891
- {
892
- id: "rule_1",
893
- if: { field: "value", op: "floor", value: 3 },
894
- then: { decision: "floored" }
895
- }
896
- ]
897
- }
898
-
899
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
900
- context = DecisionAgent::Context.new({ value: 2.5 })
901
-
902
- evaluation = evaluator.evaluate(context)
903
-
904
- expect(evaluation).to be_nil
905
- end
906
- end
907
-
908
- describe "ceil operator" do
909
- it "matches when ceil(field_value) equals expected_value" do
910
- rules = {
911
- version: "1.0",
912
- ruleset: "test",
913
- rules: [
914
- {
915
- id: "rule_1",
916
- if: { field: "value", op: "ceil", value: 4 },
917
- then: { decision: "ceiled" }
918
- }
919
- ]
920
- }
921
-
922
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
923
- context = DecisionAgent::Context.new({ value: 3.1 })
924
-
925
- evaluation = evaluator.evaluate(context)
926
-
927
- expect(evaluation).not_to be_nil
928
- expect(evaluation.decision).to eq("ceiled")
929
- end
930
-
931
- it "does not match when ceil value is different" do
932
- rules = {
933
- version: "1.0",
934
- ruleset: "test",
935
- rules: [
936
- {
937
- id: "rule_1",
938
- if: { field: "value", op: "ceil", value: 4 },
939
- then: { decision: "ceiled" }
940
- }
941
- ]
942
- }
943
-
944
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
945
- context = DecisionAgent::Context.new({ value: 2.1 })
946
-
947
- evaluation = evaluator.evaluate(context)
948
-
949
- expect(evaluation).to be_nil
950
- end
951
- end
952
-
953
- describe "abs operator" do
954
- it "matches when abs(field_value) equals expected_value for positive" do
955
- rules = {
956
- version: "1.0",
957
- ruleset: "test",
958
- rules: [
959
- {
960
- id: "rule_1",
961
- if: { field: "value", op: "abs", value: 5 },
962
- then: { decision: "absolute" }
963
- }
964
- ]
965
- }
966
-
967
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
968
- context = DecisionAgent::Context.new({ value: 5 })
969
-
970
- evaluation = evaluator.evaluate(context)
971
-
972
- expect(evaluation).not_to be_nil
973
- expect(evaluation.decision).to eq("absolute")
974
- end
975
-
976
- it "matches when abs(field_value) equals expected_value for negative" do
977
- rules = {
978
- version: "1.0",
979
- ruleset: "test",
980
- rules: [
981
- {
982
- id: "rule_1",
983
- if: { field: "value", op: "abs", value: 5 },
984
- then: { decision: "absolute" }
985
- }
986
- ]
987
- }
988
-
989
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
990
- context = DecisionAgent::Context.new({ value: -5 })
991
-
992
- evaluation = evaluator.evaluate(context)
993
-
994
- expect(evaluation).not_to be_nil
995
- expect(evaluation.decision).to eq("absolute")
996
- end
997
-
998
- it "does not match when abs value is different" do
999
- rules = {
1000
- version: "1.0",
1001
- ruleset: "test",
1002
- rules: [
1003
- {
1004
- id: "rule_1",
1005
- if: { field: "value", op: "abs", value: 5 },
1006
- then: { decision: "absolute" }
1007
- }
1008
- ]
1009
- }
1010
-
1011
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1012
- context = DecisionAgent::Context.new({ value: -3 })
1013
-
1014
- evaluation = evaluator.evaluate(context)
1015
-
1016
- expect(evaluation).to be_nil
1017
- end
1018
- end
1019
- end
1020
-
1021
- describe "aggregation functions" do
1022
- describe "min operator" do
1023
- it "matches when min(field_value) equals expected_value" do
1024
- rules = {
1025
- version: "1.0",
1026
- ruleset: "test",
1027
- rules: [
1028
- {
1029
- id: "rule_1",
1030
- if: { field: "numbers", op: "min", value: 1 },
1031
- then: { decision: "min_found" }
1032
- }
1033
- ]
1034
- }
1035
-
1036
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1037
- context = DecisionAgent::Context.new({ numbers: [3, 1, 5, 2] })
1038
-
1039
- evaluation = evaluator.evaluate(context)
1040
-
1041
- expect(evaluation).not_to be_nil
1042
- expect(evaluation.decision).to eq("min_found")
1043
- end
1044
-
1045
- it "does not match when min value is different" do
1046
- rules = {
1047
- version: "1.0",
1048
- ruleset: "test",
1049
- rules: [
1050
- {
1051
- id: "rule_1",
1052
- if: { field: "numbers", op: "min", value: 1 },
1053
- then: { decision: "min_found" }
1054
- }
1055
- ]
1056
- }
1057
-
1058
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1059
- context = DecisionAgent::Context.new({ numbers: [3, 5, 2] })
1060
-
1061
- evaluation = evaluator.evaluate(context)
1062
-
1063
- expect(evaluation).to be_nil
1064
- end
1065
-
1066
- it "does not match for empty arrays" do
1067
- rules = {
1068
- version: "1.0",
1069
- ruleset: "test",
1070
- rules: [
1071
- {
1072
- id: "rule_1",
1073
- if: { field: "numbers", op: "min", value: 1 },
1074
- then: { decision: "min_found" }
1075
- }
1076
- ]
1077
- }
1078
-
1079
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1080
- context = DecisionAgent::Context.new({ numbers: [] })
1081
-
1082
- evaluation = evaluator.evaluate(context)
1083
-
1084
- expect(evaluation).to be_nil
1085
- end
1086
- end
1087
-
1088
- describe "max operator" do
1089
- it "matches when max(field_value) equals expected_value" do
1090
- rules = {
1091
- version: "1.0",
1092
- ruleset: "test",
1093
- rules: [
1094
- {
1095
- id: "rule_1",
1096
- if: { field: "numbers", op: "max", value: 5 },
1097
- then: { decision: "max_found" }
1098
- }
1099
- ]
1100
- }
1101
-
1102
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1103
- context = DecisionAgent::Context.new({ numbers: [3, 1, 5, 2] })
1104
-
1105
- evaluation = evaluator.evaluate(context)
1106
-
1107
- expect(evaluation).not_to be_nil
1108
- expect(evaluation.decision).to eq("max_found")
1109
- end
1110
-
1111
- it "does not match when max value is different" do
1112
- rules = {
1113
- version: "1.0",
1114
- ruleset: "test",
1115
- rules: [
1116
- {
1117
- id: "rule_1",
1118
- if: { field: "numbers", op: "max", value: 5 },
1119
- then: { decision: "max_found" }
1120
- }
1121
- ]
1122
- }
1123
-
1124
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1125
- context = DecisionAgent::Context.new({ numbers: [3, 1, 2] })
1126
-
1127
- evaluation = evaluator.evaluate(context)
1128
-
1129
- expect(evaluation).to be_nil
1130
- end
1131
-
1132
- it "does not match for empty arrays" do
1133
- rules = {
1134
- version: "1.0",
1135
- ruleset: "test",
1136
- rules: [
1137
- {
1138
- id: "rule_1",
1139
- if: { field: "numbers", op: "max", value: 5 },
1140
- then: { decision: "max_found" }
1141
- }
1142
- ]
1143
- }
1144
-
1145
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1146
- context = DecisionAgent::Context.new({ numbers: [] })
1147
-
1148
- evaluation = evaluator.evaluate(context)
1149
-
1150
- expect(evaluation).to be_nil
1151
- end
1152
- end
1153
- end
1154
-
1155
- # EDGE CASES FOR MATHEMATICAL OPERATORS
1156
- describe "edge cases for mathematical operators" do
1157
- describe "non-numeric values" do
1158
- it "handles string values gracefully for sin" do
1159
- rules = {
1160
- version: "1.0",
1161
- ruleset: "test",
1162
- rules: [
1163
- {
1164
- id: "rule_1",
1165
- if: { field: "value", op: "sin", value: 0.0 },
1166
- then: { decision: "match" }
1167
- }
1168
- ]
1169
- }
1170
-
1171
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1172
- context = DecisionAgent::Context.new({ value: "not_a_number" })
1173
-
1174
- evaluation = evaluator.evaluate(context)
1175
-
1176
- expect(evaluation).to be_nil
1177
- end
1178
-
1179
- it "handles string values gracefully for sqrt" do
1180
- rules = {
1181
- version: "1.0",
1182
- ruleset: "test",
1183
- rules: [
1184
- {
1185
- id: "rule_1",
1186
- if: { field: "value", op: "sqrt", value: 3.0 },
1187
- then: { decision: "match" }
1188
- }
1189
- ]
1190
- }
1191
-
1192
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1193
- context = DecisionAgent::Context.new({ value: "invalid" })
1194
-
1195
- evaluation = evaluator.evaluate(context)
1196
-
1197
- expect(evaluation).to be_nil
1198
- end
1199
-
1200
- it "handles string values gracefully for round" do
1201
- rules = {
1202
- version: "1.0",
1203
- ruleset: "test",
1204
- rules: [
1205
- {
1206
- id: "rule_1",
1207
- if: { field: "value", op: "round", value: 3 },
1208
- then: { decision: "match" }
1209
- }
1210
- ]
1211
- }
1212
-
1213
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1214
- context = DecisionAgent::Context.new({ value: "not_numeric" })
1215
-
1216
- evaluation = evaluator.evaluate(context)
1217
-
1218
- expect(evaluation).to be_nil
1219
- end
1220
-
1221
- it "handles non-array values gracefully for min" do
1222
- rules = {
1223
- version: "1.0",
1224
- ruleset: "test",
1225
- rules: [
1226
- {
1227
- id: "rule_1",
1228
- if: { field: "value", op: "min", value: 1 },
1229
- then: { decision: "match" }
1230
- }
1231
- ]
1232
- }
1233
-
1234
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1235
- context = DecisionAgent::Context.new({ value: "not_an_array" })
1236
-
1237
- evaluation = evaluator.evaluate(context)
1238
-
1239
- expect(evaluation).to be_nil
1240
- end
1241
- end
1242
-
1243
- describe "missing or nil values" do
1244
- it "handles missing field gracefully for sin" do
1245
- rules = {
1246
- version: "1.0",
1247
- ruleset: "test",
1248
- rules: [
1249
- {
1250
- id: "rule_1",
1251
- if: { field: "missing", op: "sin", value: 0.0 },
1252
- then: { decision: "match" }
1253
- }
1254
- ]
1255
- }
1256
-
1257
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1258
- context = DecisionAgent::Context.new({})
1259
-
1260
- evaluation = evaluator.evaluate(context)
1261
-
1262
- expect(evaluation).to be_nil
1263
- end
1264
-
1265
- it "handles nil value gracefully for sqrt" do
1266
- rules = {
1267
- version: "1.0",
1268
- ruleset: "test",
1269
- rules: [
1270
- {
1271
- id: "rule_1",
1272
- if: { field: "value", op: "sqrt", value: 3.0 },
1273
- then: { decision: "match" }
1274
- }
1275
- ]
1276
- }
1277
-
1278
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1279
- context = DecisionAgent::Context.new({ value: nil })
1280
-
1281
- evaluation = evaluator.evaluate(context)
1282
-
1283
- expect(evaluation).to be_nil
1284
- end
1285
-
1286
- it "handles nil value gracefully for min" do
1287
- rules = {
1288
- version: "1.0",
1289
- ruleset: "test",
1290
- rules: [
1291
- {
1292
- id: "rule_1",
1293
- if: { field: "value", op: "min", value: 1 },
1294
- then: { decision: "match" }
1295
- }
1296
- ]
1297
- }
1298
-
1299
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1300
- context = DecisionAgent::Context.new({ value: nil })
1301
-
1302
- evaluation = evaluator.evaluate(context)
1303
-
1304
- expect(evaluation).to be_nil
1305
- end
1306
- end
1307
-
1308
- describe "floating point precision" do
1309
- it "handles floating point precision for sin" do
1310
- rules = {
1311
- version: "1.0",
1312
- ruleset: "test",
1313
- rules: [
1314
- {
1315
- id: "rule_1",
1316
- if: { field: "angle", op: "sin", value: 0.0 },
1317
- then: { decision: "match" }
1318
- }
1319
- ]
1320
- }
1321
-
1322
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1323
- # sin(0) should be exactly 0.0
1324
- context = DecisionAgent::Context.new({ angle: 0.0 })
1325
-
1326
- evaluation = evaluator.evaluate(context)
1327
-
1328
- expect(evaluation).not_to be_nil
1329
- expect(evaluation.decision).to eq("match")
1330
- end
1331
-
1332
- it "handles floating point precision for cos" do
1333
- rules = {
1334
- version: "1.0",
1335
- ruleset: "test",
1336
- rules: [
1337
- {
1338
- id: "rule_1",
1339
- if: { field: "angle", op: "cos", value: 1.0 },
1340
- then: { decision: "match" }
1341
- }
1342
- ]
1343
- }
1344
-
1345
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1346
- context = DecisionAgent::Context.new({ angle: 0.0 })
1347
-
1348
- evaluation = evaluator.evaluate(context)
1349
-
1350
- expect(evaluation).not_to be_nil
1351
- expect(evaluation.decision).to eq("match")
1352
- end
1353
- end
1354
-
1355
- describe "very large numbers" do
1356
- it "handles very large numbers for exp" do
1357
- rules = {
1358
- version: "1.0",
1359
- ruleset: "test",
1360
- rules: [
1361
- {
1362
- id: "rule_1",
1363
- if: { field: "exponent", op: "exp", value: Math.exp(10) },
1364
- then: { decision: "match" }
1365
- }
1366
- ]
1367
- }
1368
-
1369
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1370
- context = DecisionAgent::Context.new({ exponent: 10 })
1371
-
1372
- evaluation = evaluator.evaluate(context)
1373
-
1374
- expect(evaluation).not_to be_nil
1375
- expect(evaluation.decision).to eq("match")
1376
- end
1377
-
1378
- it "handles very large numbers for power" do
1379
- rules = {
1380
- version: "1.0",
1381
- ruleset: "test",
1382
- rules: [
1383
- {
1384
- id: "rule_1",
1385
- if: { field: "base", op: "power", value: [3, 27] },
1386
- then: { decision: "match" }
1387
- }
1388
- ]
1389
- }
1390
-
1391
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1392
- context = DecisionAgent::Context.new({ base: 3 })
1393
-
1394
- evaluation = evaluator.evaluate(context)
1395
-
1396
- expect(evaluation).not_to be_nil
1397
- expect(evaluation.decision).to eq("match")
1398
- end
1399
- end
1400
-
1401
- describe "integration with all/any conditions" do
1402
- it "works with all condition combining multiple mathematical operators" do
1403
- rules = {
1404
- version: "1.0",
1405
- ruleset: "test",
1406
- rules: [
1407
- {
1408
- id: "rule_1",
1409
- if: {
1410
- all: [
1411
- { field: "angle", op: "sin", value: 0.0 },
1412
- { field: "number", op: "sqrt", value: 3.0 },
1413
- { field: "value", op: "abs", value: 5 }
1414
- ]
1415
- },
1416
- then: { decision: "all_match" }
1417
- }
1418
- ]
1419
- }
1420
-
1421
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1422
- context = DecisionAgent::Context.new({
1423
- angle: 0,
1424
- number: 9,
1425
- value: -5
1426
- })
1427
-
1428
- evaluation = evaluator.evaluate(context)
1429
-
1430
- expect(evaluation).not_to be_nil
1431
- expect(evaluation.decision).to eq("all_match")
1432
- end
1433
-
1434
- it "works with any condition combining multiple mathematical operators" do
1435
- rules = {
1436
- version: "1.0",
1437
- ruleset: "test",
1438
- rules: [
1439
- {
1440
- id: "rule_1",
1441
- if: {
1442
- any: [
1443
- { field: "angle", op: "sin", value: 1.0 },
1444
- { field: "number", op: "sqrt", value: 4.0 },
1445
- { field: "value", op: "abs", value: 10 }
1446
- ]
1447
- },
1448
- then: { decision: "any_match" }
1449
- }
1450
- ]
1451
- }
1452
-
1453
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1454
- context = DecisionAgent::Context.new({
1455
- angle: 0, # sin(0) = 0, not 1
1456
- number: 9, # sqrt(9) = 3, not 4
1457
- value: -10 # abs(-10) = 10, matches!
1458
- })
1459
-
1460
- evaluation = evaluator.evaluate(context)
1461
-
1462
- expect(evaluation).not_to be_nil
1463
- expect(evaluation.decision).to eq("any_match")
1464
- end
1465
- end
1466
-
1467
- describe "nested field access" do
1468
- it "works with nested fields for sin" do
1469
- rules = {
1470
- version: "1.0",
1471
- ruleset: "test",
1472
- rules: [
1473
- {
1474
- id: "rule_1",
1475
- if: { field: "math.angle", op: "sin", value: 0.0 },
1476
- then: { decision: "match" }
1477
- }
1478
- ]
1479
- }
1480
-
1481
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1482
- context = DecisionAgent::Context.new({ math: { angle: 0 } })
1483
-
1484
- evaluation = evaluator.evaluate(context)
1485
-
1486
- expect(evaluation).not_to be_nil
1487
- expect(evaluation.decision).to eq("match")
1488
- end
1489
-
1490
- it "works with nested fields for min" do
1491
- rules = {
1492
- version: "1.0",
1493
- ruleset: "test",
1494
- rules: [
1495
- {
1496
- id: "rule_1",
1497
- if: { field: "data.numbers", op: "min", value: 1 },
1498
- then: { decision: "match" }
1499
- }
1500
- ]
1501
- }
1502
-
1503
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1504
- context = DecisionAgent::Context.new({ data: { numbers: [3, 1, 5, 2] } })
1505
-
1506
- evaluation = evaluator.evaluate(context)
1507
-
1508
- expect(evaluation).not_to be_nil
1509
- expect(evaluation.decision).to eq("match")
1510
- end
1511
- end
1512
-
1513
- describe "power operator edge cases" do
1514
- it "handles power with zero exponent" do
1515
- rules = {
1516
- version: "1.0",
1517
- ruleset: "test",
1518
- rules: [
1519
- {
1520
- id: "rule_1",
1521
- if: { field: "base", op: "power", value: [0, 1] },
1522
- then: { decision: "match" }
1523
- }
1524
- ]
1525
- }
1526
-
1527
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1528
- context = DecisionAgent::Context.new({ base: 5 })
1529
-
1530
- evaluation = evaluator.evaluate(context)
1531
-
1532
- expect(evaluation).not_to be_nil
1533
- expect(evaluation.decision).to eq("match")
1534
- end
1535
-
1536
- it "handles power with negative exponent" do
1537
- rules = {
1538
- version: "1.0",
1539
- ruleset: "test",
1540
- rules: [
1541
- {
1542
- id: "rule_1",
1543
- if: { field: "base", op: "power", value: [-1, 0.5] },
1544
- then: { decision: "match" }
1545
- }
1546
- ]
1547
- }
1548
-
1549
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1550
- context = DecisionAgent::Context.new({ base: 2 })
1551
-
1552
- evaluation = evaluator.evaluate(context)
1553
-
1554
- expect(evaluation).not_to be_nil
1555
- expect(evaluation.decision).to eq("match")
1556
- end
1557
-
1558
- it "handles invalid power parameters gracefully" do
1559
- rules = {
1560
- version: "1.0",
1561
- ruleset: "test",
1562
- rules: [
1563
- {
1564
- id: "rule_1",
1565
- if: { field: "base", op: "power", value: "invalid" },
1566
- then: { decision: "match" }
1567
- }
1568
- ]
1569
- }
1570
-
1571
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1572
- context = DecisionAgent::Context.new({ base: 2 })
1573
-
1574
- evaluation = evaluator.evaluate(context)
1575
-
1576
- expect(evaluation).to be_nil
1577
- end
1578
- end
1579
-
1580
- describe "min/max with mixed types" do
1581
- it "handles min with mixed numeric types" do
1582
- rules = {
1583
- version: "1.0",
1584
- ruleset: "test",
1585
- rules: [
1586
- {
1587
- id: "rule_1",
1588
- if: { field: "numbers", op: "min", value: 1 },
1589
- then: { decision: "match" }
1590
- }
1591
- ]
1592
- }
1593
-
1594
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1595
- context = DecisionAgent::Context.new({ numbers: [3.5, 1, 5.0, 2] })
1596
-
1597
- evaluation = evaluator.evaluate(context)
1598
-
1599
- expect(evaluation).not_to be_nil
1600
- expect(evaluation.decision).to eq("match")
1601
- end
1602
-
1603
- it "handles max with mixed numeric types" do
1604
- rules = {
1605
- version: "1.0",
1606
- ruleset: "test",
1607
- rules: [
1608
- {
1609
- id: "rule_1",
1610
- if: { field: "numbers", op: "max", value: 5 },
1611
- then: { decision: "match" }
1612
- }
1613
- ]
1614
- }
1615
-
1616
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1617
- context = DecisionAgent::Context.new({ numbers: [1, 3.5, 5, 2.0] })
1618
-
1619
- evaluation = evaluator.evaluate(context)
1620
-
1621
- expect(evaluation).not_to be_nil
1622
- expect(evaluation.decision).to eq("match")
1623
- end
1624
- end
1625
- end
1626
- end
1627
-
1628
- # DATE/TIME OPERATORS
1629
- describe "date/time operators" do
1630
- describe "before_date operator" do
1631
- it "matches when date is before specified date" do
1632
- rules = {
1633
- version: "1.0",
1634
- ruleset: "test",
1635
- rules: [
1636
- {
1637
- id: "rule_1",
1638
- if: { field: "expires_at", op: "before_date", value: "2025-12-31" },
1639
- then: { decision: "not_expired" }
1640
- }
1641
- ]
1642
- }
1643
-
1644
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1645
- context = DecisionAgent::Context.new({ expires_at: "2025-06-01" })
1646
-
1647
- evaluation = evaluator.evaluate(context)
1648
-
1649
- expect(evaluation).not_to be_nil
1650
- expect(evaluation.decision).to eq("not_expired")
1651
- end
1652
-
1653
- it "does not match when date is after specified date" do
1654
- rules = {
1655
- version: "1.0",
1656
- ruleset: "test",
1657
- rules: [
1658
- {
1659
- id: "rule_1",
1660
- if: { field: "expires_at", op: "before_date", value: "2025-01-01" },
1661
- then: { decision: "not_expired" }
1662
- }
1663
- ]
1664
- }
1665
-
1666
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1667
- context = DecisionAgent::Context.new({ expires_at: "2025-12-31" })
1668
-
1669
- evaluation = evaluator.evaluate(context)
1670
-
1671
- expect(evaluation).to be_nil
1672
- end
1673
- end
1674
-
1675
- describe "after_date operator" do
1676
- it "matches when date is after specified date" do
1677
- rules = {
1678
- version: "1.0",
1679
- ruleset: "test",
1680
- rules: [
1681
- {
1682
- id: "rule_1",
1683
- if: { field: "created_at", op: "after_date", value: "2024-01-01" },
1684
- then: { decision: "recent" }
1685
- }
1686
- ]
1687
- }
1688
-
1689
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1690
- context = DecisionAgent::Context.new({ created_at: "2025-06-01" })
1691
-
1692
- evaluation = evaluator.evaluate(context)
1693
-
1694
- expect(evaluation).not_to be_nil
1695
- expect(evaluation.decision).to eq("recent")
1696
- end
1697
-
1698
- it "does not match when date is before specified date" do
1699
- rules = {
1700
- version: "1.0",
1701
- ruleset: "test",
1702
- rules: [
1703
- {
1704
- id: "rule_1",
1705
- if: { field: "created_at", op: "after_date", value: "2025-12-31" },
1706
- then: { decision: "recent" }
1707
- }
1708
- ]
1709
- }
1710
-
1711
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1712
- context = DecisionAgent::Context.new({ created_at: "2025-01-01" })
1713
-
1714
- evaluation = evaluator.evaluate(context)
1715
-
1716
- expect(evaluation).to be_nil
1717
- end
1718
- end
1719
-
1720
- describe "within_days operator" do
1721
- it "matches when date is within N days from now" do
1722
- rules = {
1723
- version: "1.0",
1724
- ruleset: "test",
1725
- rules: [
1726
- {
1727
- id: "rule_1",
1728
- if: { field: "event_date", op: "within_days", value: 7 },
1729
- then: { decision: "upcoming" }
1730
- }
1731
- ]
1732
- }
1733
-
1734
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1735
- event_date = (Time.now + (3 * 24 * 60 * 60)).strftime("%Y-%m-%d")
1736
- context = DecisionAgent::Context.new({ event_date: event_date })
1737
-
1738
- evaluation = evaluator.evaluate(context)
1739
-
1740
- expect(evaluation).not_to be_nil
1741
- expect(evaluation.decision).to eq("upcoming")
1742
- end
1743
-
1744
- it "does not match when date is outside N days" do
1745
- rules = {
1746
- version: "1.0",
1747
- ruleset: "test",
1748
- rules: [
1749
- {
1750
- id: "rule_1",
1751
- if: { field: "event_date", op: "within_days", value: 7 },
1752
- then: { decision: "upcoming" }
1753
- }
1754
- ]
1755
- }
1756
-
1757
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1758
- event_date = (Time.now + (30 * 24 * 60 * 60)).strftime("%Y-%m-%d")
1759
- context = DecisionAgent::Context.new({ event_date: event_date })
1760
-
1761
- evaluation = evaluator.evaluate(context)
1762
-
1763
- expect(evaluation).to be_nil
1764
- end
1765
- end
1766
-
1767
- describe "day_of_week operator" do
1768
- it "matches when day of week is correct (string format)" do
1769
- rules = {
1770
- version: "1.0",
1771
- ruleset: "test",
1772
- rules: [
1773
- {
1774
- id: "rule_1",
1775
- if: { field: "appointment", op: "day_of_week", value: "monday" },
1776
- then: { decision: "monday_appointment" }
1777
- }
1778
- ]
1779
- }
1780
-
1781
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1782
- # Find next Monday
1783
- monday_date = Time.now
1784
- monday_date += 24 * 60 * 60 until monday_date.wday == 1
1785
- context = DecisionAgent::Context.new({ appointment: monday_date.strftime("%Y-%m-%d") })
1786
-
1787
- evaluation = evaluator.evaluate(context)
1788
-
1789
- expect(evaluation).not_to be_nil
1790
- expect(evaluation.decision).to eq("monday_appointment")
1791
- end
1792
-
1793
- it "matches when day of week is correct (numeric format)" do
1794
- rules = {
1795
- version: "1.0",
1796
- ruleset: "test",
1797
- rules: [
1798
- {
1799
- id: "rule_1",
1800
- if: { field: "appointment", op: "day_of_week", value: 1 },
1801
- then: { decision: "monday_appointment" }
1802
- }
1803
- ]
1804
- }
1805
-
1806
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1807
- monday_date = Time.now
1808
- monday_date += 24 * 60 * 60 until monday_date.wday == 1
1809
- context = DecisionAgent::Context.new({ appointment: monday_date.strftime("%Y-%m-%d") })
1810
-
1811
- evaluation = evaluator.evaluate(context)
1812
-
1813
- expect(evaluation).not_to be_nil
1814
- expect(evaluation.decision).to eq("monday_appointment")
1815
- end
1816
- end
1817
- end
1818
-
1819
- # COLLECTION OPERATORS
1820
- describe "collection operators" do
1821
- describe "contains_all operator" do
1822
- it "matches when array contains all specified elements" do
1823
- rules = {
1824
- version: "1.0",
1825
- ruleset: "test",
1826
- rules: [
1827
- {
1828
- id: "rule_1",
1829
- if: { field: "permissions", op: "contains_all", value: %w[read write] },
1830
- then: { decision: "full_access" }
1831
- }
1832
- ]
1833
- }
1834
-
1835
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1836
- context = DecisionAgent::Context.new({ permissions: %w[read write execute] })
1837
-
1838
- evaluation = evaluator.evaluate(context)
1839
-
1840
- expect(evaluation).not_to be_nil
1841
- expect(evaluation.decision).to eq("full_access")
1842
- end
1843
-
1844
- it "does not match when array is missing some elements" do
1845
- rules = {
1846
- version: "1.0",
1847
- ruleset: "test",
1848
- rules: [
1849
- {
1850
- id: "rule_1",
1851
- if: { field: "permissions", op: "contains_all", value: %w[read write] },
1852
- then: { decision: "full_access" }
1853
- }
1854
- ]
1855
- }
1856
-
1857
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1858
- context = DecisionAgent::Context.new({ permissions: ["read"] })
1859
-
1860
- evaluation = evaluator.evaluate(context)
1861
-
1862
- expect(evaluation).to be_nil
1863
- end
1864
- end
1865
-
1866
- describe "contains_any operator" do
1867
- it "matches when array contains any of the specified elements" do
1868
- rules = {
1869
- version: "1.0",
1870
- ruleset: "test",
1871
- rules: [
1872
- {
1873
- id: "rule_1",
1874
- if: { field: "tags", op: "contains_any", value: %w[urgent critical] },
1875
- then: { decision: "prioritize" }
1876
- }
1877
- ]
1878
- }
1879
-
1880
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1881
- context = DecisionAgent::Context.new({ tags: %w[normal urgent] })
1882
-
1883
- evaluation = evaluator.evaluate(context)
1884
-
1885
- expect(evaluation).not_to be_nil
1886
- expect(evaluation.decision).to eq("prioritize")
1887
- end
1888
-
1889
- it "does not match when array contains none of the specified elements" do
1890
- rules = {
1891
- version: "1.0",
1892
- ruleset: "test",
1893
- rules: [
1894
- {
1895
- id: "rule_1",
1896
- if: { field: "tags", op: "contains_any", value: %w[urgent critical] },
1897
- then: { decision: "prioritize" }
1898
- }
1899
- ]
1900
- }
1901
-
1902
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1903
- context = DecisionAgent::Context.new({ tags: %w[normal low] })
1904
-
1905
- evaluation = evaluator.evaluate(context)
1906
-
1907
- expect(evaluation).to be_nil
1908
- end
1909
- end
1910
-
1911
- describe "intersects operator" do
1912
- it "matches when arrays have common elements" do
1913
- rules = {
1914
- version: "1.0",
1915
- ruleset: "test",
1916
- rules: [
1917
- {
1918
- id: "rule_1",
1919
- if: { field: "user_roles", op: "intersects", value: %w[admin moderator] },
1920
- then: { decision: "has_elevated_role" }
1921
- }
1922
- ]
1923
- }
1924
-
1925
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1926
- context = DecisionAgent::Context.new({ user_roles: %w[user moderator] })
1927
-
1928
- evaluation = evaluator.evaluate(context)
1929
-
1930
- expect(evaluation).not_to be_nil
1931
- expect(evaluation.decision).to eq("has_elevated_role")
1932
- end
1933
-
1934
- it "does not match when arrays have no common elements" do
1935
- rules = {
1936
- version: "1.0",
1937
- ruleset: "test",
1938
- rules: [
1939
- {
1940
- id: "rule_1",
1941
- if: { field: "user_roles", op: "intersects", value: %w[admin moderator] },
1942
- then: { decision: "has_elevated_role" }
1943
- }
1944
- ]
1945
- }
1946
-
1947
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1948
- context = DecisionAgent::Context.new({ user_roles: %w[user guest] })
1949
-
1950
- evaluation = evaluator.evaluate(context)
1951
-
1952
- expect(evaluation).to be_nil
1953
- end
1954
- end
1955
-
1956
- describe "subset_of operator" do
1957
- it "matches when array is a subset of another" do
1958
- rules = {
1959
- version: "1.0",
1960
- ruleset: "test",
1961
- rules: [
1962
- {
1963
- id: "rule_1",
1964
- if: { field: "selected_items", op: "subset_of", value: %w[a b c d] },
1965
- then: { decision: "valid_selection" }
1966
- }
1967
- ]
1968
- }
1969
-
1970
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1971
- context = DecisionAgent::Context.new({ selected_items: %w[a c] })
1972
-
1973
- evaluation = evaluator.evaluate(context)
1974
-
1975
- expect(evaluation).not_to be_nil
1976
- expect(evaluation.decision).to eq("valid_selection")
1977
- end
1978
-
1979
- it "does not match when array is not a subset" do
1980
- rules = {
1981
- version: "1.0",
1982
- ruleset: "test",
1983
- rules: [
1984
- {
1985
- id: "rule_1",
1986
- if: { field: "selected_items", op: "subset_of", value: %w[a b c] },
1987
- then: { decision: "valid_selection" }
1988
- }
1989
- ]
1990
- }
1991
-
1992
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
1993
- context = DecisionAgent::Context.new({ selected_items: %w[a d] })
1994
-
1995
- evaluation = evaluator.evaluate(context)
1996
-
1997
- expect(evaluation).to be_nil
1998
- end
1999
- end
2000
- end
2001
-
2002
- # GEOSPATIAL OPERATORS
2003
- describe "geospatial operators" do
2004
- describe "within_radius operator" do
2005
- it "matches when point is within radius (hash format)" do
2006
- rules = {
2007
- version: "1.0",
2008
- ruleset: "test",
2009
- rules: [
2010
- {
2011
- id: "rule_1",
2012
- if: {
2013
- field: "location",
2014
- op: "within_radius",
2015
- value: { center: { lat: 40.7128, lon: -74.0060 }, radius: 10 }
2016
- },
2017
- then: { decision: "nearby" }
2018
- }
2019
- ]
2020
- }
2021
-
2022
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2023
- # Point very close to center (within 1km)
2024
- context = DecisionAgent::Context.new({ location: { lat: 40.7200, lon: -74.0000 } })
2025
-
2026
- evaluation = evaluator.evaluate(context)
2027
-
2028
- expect(evaluation).not_to be_nil
2029
- expect(evaluation.decision).to eq("nearby")
2030
- end
2031
-
2032
- it "matches when point is within radius (array format)" do
2033
- rules = {
2034
- version: "1.0",
2035
- ruleset: "test",
2036
- rules: [
2037
- {
2038
- id: "rule_1",
2039
- if: {
2040
- field: "location",
2041
- op: "within_radius",
2042
- value: { center: [40.7128, -74.0060], radius: 10 }
2043
- },
2044
- then: { decision: "nearby" }
2045
- }
2046
- ]
2047
- }
2048
-
2049
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2050
- context = DecisionAgent::Context.new({ location: [40.7200, -74.0000] })
2051
-
2052
- evaluation = evaluator.evaluate(context)
2053
-
2054
- expect(evaluation).not_to be_nil
2055
- expect(evaluation.decision).to eq("nearby")
2056
- end
2057
-
2058
- it "does not match when point is outside radius" do
2059
- rules = {
2060
- version: "1.0",
2061
- ruleset: "test",
2062
- rules: [
2063
- {
2064
- id: "rule_1",
2065
- if: {
2066
- field: "location",
2067
- op: "within_radius",
2068
- value: { center: { lat: 40.7128, lon: -74.0060 }, radius: 1 }
2069
- },
2070
- then: { decision: "nearby" }
2071
- }
2072
- ]
2073
- }
2074
-
2075
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2076
- # Point far from center (more than 100km away)
2077
- context = DecisionAgent::Context.new({ location: { lat: 41.0, lon: -75.0 } })
2078
-
2079
- evaluation = evaluator.evaluate(context)
2080
-
2081
- expect(evaluation).to be_nil
2082
- end
2083
- end
2084
-
2085
- describe "in_polygon operator" do
2086
- it "matches when point is inside polygon" do
2087
- # Square polygon around point (0,0)
2088
- polygon = [
2089
- { lat: -1, lon: -1 },
2090
- { lat: 1, lon: -1 },
2091
- { lat: 1, lon: 1 },
2092
- { lat: -1, lon: 1 }
2093
- ]
2094
-
2095
- rules = {
2096
- version: "1.0",
2097
- ruleset: "test",
2098
- rules: [
2099
- {
2100
- id: "rule_1",
2101
- if: { field: "location", op: "in_polygon", value: polygon },
2102
- then: { decision: "inside_zone" }
2103
- }
2104
- ]
2105
- }
2106
-
2107
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2108
- context = DecisionAgent::Context.new({ location: { lat: 0, lon: 0 } })
2109
-
2110
- evaluation = evaluator.evaluate(context)
2111
-
2112
- expect(evaluation).not_to be_nil
2113
- expect(evaluation.decision).to eq("inside_zone")
2114
- end
2115
-
2116
- it "does not match when point is outside polygon" do
2117
- polygon = [
2118
- { lat: -1, lon: -1 },
2119
- { lat: 1, lon: -1 },
2120
- { lat: 1, lon: 1 },
2121
- { lat: -1, lon: 1 }
2122
- ]
2123
-
2124
- rules = {
2125
- version: "1.0",
2126
- ruleset: "test",
2127
- rules: [
2128
- {
2129
- id: "rule_1",
2130
- if: { field: "location", op: "in_polygon", value: polygon },
2131
- then: { decision: "inside_zone" }
2132
- }
2133
- ]
2134
- }
2135
-
2136
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2137
- context = DecisionAgent::Context.new({ location: { lat: 5, lon: 5 } })
2138
-
2139
- evaluation = evaluator.evaluate(context)
2140
-
2141
- expect(evaluation).to be_nil
2142
- end
2143
-
2144
- it "works with array format coordinates" do
2145
- polygon = [
2146
- [-1, -1],
2147
- [1, -1],
2148
- [1, 1],
2149
- [-1, 1]
2150
- ]
2151
-
2152
- rules = {
2153
- version: "1.0",
2154
- ruleset: "test",
2155
- rules: [
2156
- {
2157
- id: "rule_1",
2158
- if: { field: "location", op: "in_polygon", value: polygon },
2159
- then: { decision: "inside_zone" }
2160
- }
2161
- ]
2162
- }
2163
-
2164
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2165
- context = DecisionAgent::Context.new({ location: [0, 0] })
2166
-
2167
- evaluation = evaluator.evaluate(context)
2168
-
2169
- expect(evaluation).not_to be_nil
2170
- expect(evaluation.decision).to eq("inside_zone")
2171
- end
2172
- end
2173
- end
2174
-
2175
- # COMBINATION TESTS
2176
- describe "combining new operators with all/any" do
2177
- it "works with 'all' condition" do
2178
- rules = {
2179
- version: "1.0",
2180
- ruleset: "test",
2181
- rules: [
2182
- {
2183
- id: "rule_1",
2184
- if: {
2185
- all: [
2186
- { field: "email", op: "ends_with", value: "@company.com" },
2187
- { field: "age", op: "between", value: [18, 65] },
2188
- { field: "roles", op: "contains_any", value: %w[admin manager] }
2189
- ]
2190
- },
2191
- then: { decision: "approve" }
2192
- }
2193
- ]
2194
- }
2195
-
2196
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2197
- context = DecisionAgent::Context.new({
2198
- email: "user@company.com",
2199
- age: 30,
2200
- roles: %w[user admin]
2201
- })
2202
-
2203
- evaluation = evaluator.evaluate(context)
2204
-
2205
- expect(evaluation).not_to be_nil
2206
- expect(evaluation.decision).to eq("approve")
2207
- end
2208
-
2209
- it "works with 'any' condition" do
2210
- rules = {
2211
- version: "1.0",
2212
- ruleset: "test",
2213
- rules: [
2214
- {
2215
- id: "rule_1",
2216
- if: {
2217
- any: [
2218
- { field: "status", op: "contains", value: "urgent" },
2219
- { field: "priority", op: "modulo", value: [2, 1] },
2220
- { field: "tags", op: "intersects", value: %w[critical emergency] }
2221
- ]
2222
- },
2223
- then: { decision: "escalate" }
2224
- }
2225
- ]
2226
- }
2227
-
2228
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2229
- context = DecisionAgent::Context.new({
2230
- status: "normal",
2231
- priority: 7,
2232
- tags: ["normal"]
2233
- })
2234
-
2235
- evaluation = evaluator.evaluate(context)
2236
-
2237
- expect(evaluation).not_to be_nil
2238
- expect(evaluation.decision).to eq("escalate")
2239
- end
2240
- end
2241
-
2242
- # STATISTICAL AGGREGATIONS
2243
- describe "statistical aggregation operators" do
2244
- describe "sum operator" do
2245
- it "matches when sum equals expected value" do
2246
- rules = {
2247
- version: "1.0",
2248
- ruleset: "test",
2249
- rules: [
2250
- {
2251
- id: "rule_1",
2252
- if: { field: "amounts", op: "sum", value: 100 },
2253
- then: { decision: "total_match" }
2254
- }
2255
- ]
2256
- }
2257
-
2258
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2259
- context = DecisionAgent::Context.new({ amounts: [30, 40, 30] })
2260
-
2261
- evaluation = evaluator.evaluate(context)
2262
-
2263
- expect(evaluation).not_to be_nil
2264
- expect(evaluation.decision).to eq("total_match")
2265
- end
2266
-
2267
- it "matches with comparison operators" do
2268
- rules = {
2269
- version: "1.0",
2270
- ruleset: "test",
2271
- rules: [
2272
- {
2273
- id: "rule_1",
2274
- if: { field: "prices", op: "sum", value: { gte: 100 } },
2275
- then: { decision: "free_shipping" }
2276
- }
2277
- ]
2278
- }
2279
-
2280
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2281
- context = DecisionAgent::Context.new({ prices: [25, 30, 50] })
2282
-
2283
- evaluation = evaluator.evaluate(context)
2284
-
2285
- expect(evaluation).not_to be_nil
2286
- expect(evaluation.decision).to eq("free_shipping")
2287
- end
2288
-
2289
- it "returns false for empty array" do
2290
- rules = {
2291
- version: "1.0",
2292
- ruleset: "test",
2293
- rules: [
2294
- {
2295
- id: "rule_1",
2296
- if: { field: "amounts", op: "sum", value: 0 },
2297
- then: { decision: "match" }
2298
- }
2299
- ]
2300
- }
2301
-
2302
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2303
- context = DecisionAgent::Context.new({ amounts: [] })
2304
-
2305
- evaluation = evaluator.evaluate(context)
2306
-
2307
- expect(evaluation).to be_nil
2308
- end
2309
-
2310
- it "filters out non-numeric values" do
2311
- rules = {
2312
- version: "1.0",
2313
- ruleset: "test",
2314
- rules: [
2315
- {
2316
- id: "rule_1",
2317
- if: { field: "values", op: "sum", value: 15 },
2318
- then: { decision: "match" }
2319
- }
2320
- ]
2321
- }
2322
-
2323
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2324
- context = DecisionAgent::Context.new({ values: [5, "invalid", 10, nil] })
2325
-
2326
- evaluation = evaluator.evaluate(context)
2327
-
2328
- expect(evaluation).not_to be_nil
2329
- expect(evaluation.decision).to eq("match")
2330
- end
2331
- end
2332
-
2333
- describe "average operator" do
2334
- it "matches when average equals expected value" do
2335
- rules = {
2336
- version: "1.0",
2337
- ruleset: "test",
2338
- rules: [
2339
- {
2340
- id: "rule_1",
2341
- if: { field: "scores", op: "average", value: 50 },
2342
- then: { decision: "average_score" }
2343
- }
2344
- ]
2345
- }
2346
-
2347
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2348
- context = DecisionAgent::Context.new({ scores: [40, 50, 60] })
2349
-
2350
- evaluation = evaluator.evaluate(context)
2351
-
2352
- expect(evaluation).not_to be_nil
2353
- expect(evaluation.decision).to eq("average_score")
2354
- end
2355
-
2356
- it "matches with comparison operators" do
2357
- rules = {
2358
- version: "1.0",
2359
- ruleset: "test",
2360
- rules: [
2361
- {
2362
- id: "rule_1",
2363
- if: { field: "latencies", op: "average", value: { lt: 200 } },
2364
- then: { decision: "acceptable" }
2365
- }
2366
- ]
2367
- }
2368
-
2369
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2370
- context = DecisionAgent::Context.new({ latencies: [150, 180, 190] })
2371
-
2372
- evaluation = evaluator.evaluate(context)
2373
-
2374
- expect(evaluation).not_to be_nil
2375
- expect(evaluation.decision).to eq("acceptable")
2376
- end
2377
- end
2378
-
2379
- describe "mean operator" do
2380
- it "works as alias for average" do
2381
- rules = {
2382
- version: "1.0",
2383
- ruleset: "test",
2384
- rules: [
2385
- {
2386
- id: "rule_1",
2387
- if: { field: "values", op: "mean", value: 25 },
2388
- then: { decision: "match" }
2389
- }
2390
- ]
2391
- }
2392
-
2393
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2394
- context = DecisionAgent::Context.new({ values: [20, 25, 30] })
2395
-
2396
- evaluation = evaluator.evaluate(context)
2397
-
2398
- expect(evaluation).not_to be_nil
2399
- expect(evaluation.decision).to eq("match")
2400
- end
2401
- end
2402
-
2403
- describe "median operator" do
2404
- it "matches when median equals expected value" do
2405
- rules = {
2406
- version: "1.0",
2407
- ruleset: "test",
2408
- rules: [
2409
- {
2410
- id: "rule_1",
2411
- if: { field: "scores", op: "median", value: 50 },
2412
- then: { decision: "median_match" }
2413
- }
2414
- ]
2415
- }
2416
-
2417
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2418
- context = DecisionAgent::Context.new({ scores: [40, 50, 60] })
2419
-
2420
- evaluation = evaluator.evaluate(context)
2421
-
2422
- expect(evaluation).not_to be_nil
2423
- expect(evaluation.decision).to eq("median_match")
2424
- end
2425
-
2426
- it "handles even number of elements" do
2427
- rules = {
2428
- version: "1.0",
2429
- ruleset: "test",
2430
- rules: [
2431
- {
2432
- id: "rule_1",
2433
- if: { field: "values", op: "median", value: 25 },
2434
- then: { decision: "match" }
2435
- }
2436
- ]
2437
- }
2438
-
2439
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2440
- context = DecisionAgent::Context.new({ values: [20, 30] })
2441
-
2442
- evaluation = evaluator.evaluate(context)
2443
-
2444
- expect(evaluation).not_to be_nil
2445
- expect(evaluation.decision).to eq("match")
2446
- end
2447
- end
2448
-
2449
- describe "stddev operator" do
2450
- it "matches when standard deviation meets threshold" do
2451
- rules = {
2452
- version: "1.0",
2453
- ruleset: "test",
2454
- rules: [
2455
- {
2456
- id: "rule_1",
2457
- if: { field: "values", op: "stddev", value: { lt: 5 } },
2458
- then: { decision: "low_variance" }
2459
- }
2460
- ]
2461
- }
2462
-
2463
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2464
- context = DecisionAgent::Context.new({ values: [10, 11, 12, 13, 14] })
2465
-
2466
- evaluation = evaluator.evaluate(context)
2467
-
2468
- expect(evaluation).not_to be_nil
2469
- expect(evaluation.decision).to eq("low_variance")
2470
- end
2471
-
2472
- it "returns false for arrays with less than 2 elements" do
2473
- rules = {
2474
- version: "1.0",
2475
- ruleset: "test",
2476
- rules: [
2477
- {
2478
- id: "rule_1",
2479
- if: { field: "values", op: "stddev", value: 0 },
2480
- then: { decision: "match" }
2481
- }
2482
- ]
2483
- }
2484
-
2485
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2486
- context = DecisionAgent::Context.new({ values: [10] })
2487
-
2488
- evaluation = evaluator.evaluate(context)
2489
-
2490
- expect(evaluation).to be_nil
2491
- end
2492
- end
2493
-
2494
- describe "variance operator" do
2495
- it "matches when variance meets threshold" do
2496
- rules = {
2497
- version: "1.0",
2498
- ruleset: "test",
2499
- rules: [
2500
- {
2501
- id: "rule_1",
2502
- if: { field: "scores", op: "variance", value: { lt: 100 } },
2503
- then: { decision: "low_variance" }
2504
- }
2505
- ]
2506
- }
2507
-
2508
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2509
- context = DecisionAgent::Context.new({ scores: [50, 52, 48, 51, 49] })
2510
-
2511
- evaluation = evaluator.evaluate(context)
2512
-
2513
- expect(evaluation).not_to be_nil
2514
- expect(evaluation.decision).to eq("low_variance")
2515
- end
2516
- end
2517
-
2518
- describe "percentile operator" do
2519
- it "matches when percentile meets threshold" do
2520
- rules = {
2521
- version: "1.0",
2522
- ruleset: "test",
2523
- rules: [
2524
- {
2525
- id: "rule_1",
2526
- if: { field: "latencies", op: "percentile", value: { percentile: 95, threshold: 200 } },
2527
- then: { decision: "p95_ok" }
2528
- }
2529
- ]
2530
- }
2531
-
2532
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2533
- context = DecisionAgent::Context.new({ latencies: [100, 120, 150, 180, 190, 200, 210] })
2534
-
2535
- evaluation = evaluator.evaluate(context)
2536
-
2537
- expect(evaluation).not_to be_nil
2538
- expect(evaluation.decision).to eq("p95_ok")
2539
- end
2540
-
2541
- it "works with comparison operators" do
2542
- rules = {
2543
- version: "1.0",
2544
- ruleset: "test",
2545
- rules: [
2546
- {
2547
- id: "rule_1",
2548
- if: { field: "times", op: "percentile", value: { percentile: 99, gt: 500 } },
2549
- then: { decision: "high_p99" }
2550
- }
2551
- ]
2552
- }
2553
-
2554
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2555
- context = DecisionAgent::Context.new({ times: [100, 200, 300, 400, 500, 600, 700] })
2556
-
2557
- evaluation = evaluator.evaluate(context)
2558
-
2559
- expect(evaluation).not_to be_nil
2560
- expect(evaluation.decision).to eq("high_p99")
2561
- end
2562
- end
2563
-
2564
- describe "count operator" do
2565
- it "matches when count equals expected value" do
2566
- rules = {
2567
- version: "1.0",
2568
- ruleset: "test",
2569
- rules: [
2570
- {
2571
- id: "rule_1",
2572
- if: { field: "items", op: "count", value: 3 },
2573
- then: { decision: "three_items" }
2574
- }
2575
- ]
2576
- }
2577
-
2578
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2579
- context = DecisionAgent::Context.new({ items: %w[a b c] })
2580
-
2581
- evaluation = evaluator.evaluate(context)
2582
-
2583
- expect(evaluation).not_to be_nil
2584
- expect(evaluation.decision).to eq("three_items")
2585
- end
2586
-
2587
- it "matches with comparison operators" do
2588
- rules = {
2589
- version: "1.0",
2590
- ruleset: "test",
2591
- rules: [
2592
- {
2593
- id: "rule_1",
2594
- if: { field: "errors", op: "count", value: { gte: 5 } },
2595
- then: { decision: "alert" }
2596
- }
2597
- ]
2598
- }
2599
-
2600
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2601
- context = DecisionAgent::Context.new({ errors: %w[err1 err2 err3 err4 err5] })
2602
-
2603
- evaluation = evaluator.evaluate(context)
2604
-
2605
- expect(evaluation).not_to be_nil
2606
- expect(evaluation.decision).to eq("alert")
2607
- end
2608
- end
2609
- end
2610
-
2611
- # DURATION CALCULATIONS
2612
- describe "duration calculation operators" do
2613
- describe "duration_seconds operator" do
2614
- it "matches when duration is within threshold" do
2615
- rules = {
2616
- version: "1.0",
2617
- ruleset: "test",
2618
- rules: [
2619
- {
2620
- id: "rule_1",
2621
- if: { field: "start_time", op: "duration_seconds", value: { end: "now", max: 3600 } },
2622
- then: { decision: "within_hour" }
2623
- }
2624
- ]
2625
- }
2626
-
2627
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2628
- context = DecisionAgent::Context.new({ start_time: (Time.now - 1800).iso8601 })
2629
-
2630
- evaluation = evaluator.evaluate(context)
2631
-
2632
- expect(evaluation).not_to be_nil
2633
- expect(evaluation.decision).to eq("within_hour")
2634
- end
2635
-
2636
- it "works with field path for end time" do
2637
- rules = {
2638
- version: "1.0",
2639
- ruleset: "test",
2640
- rules: [
2641
- {
2642
- id: "rule_1",
2643
- if: { field: "session.start", op: "duration_seconds", value: { end: "session.end", max: 7200 } },
2644
- then: { decision: "short_session" }
2645
- }
2646
- ]
2647
- }
2648
-
2649
- start_time = Time.now - 3600
2650
- end_time = Time.now - 300
2651
-
2652
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2653
- context = DecisionAgent::Context.new({
2654
- session: {
2655
- start: start_time.iso8601,
2656
- end: end_time.iso8601
2657
- }
2658
- })
2659
-
2660
- evaluation = evaluator.evaluate(context)
2661
-
2662
- expect(evaluation).not_to be_nil
2663
- expect(evaluation.decision).to eq("short_session")
2664
- end
2665
- end
2666
-
2667
- describe "duration_minutes operator" do
2668
- it "calculates duration in minutes" do
2669
- rules = {
2670
- version: "1.0",
2671
- ruleset: "test",
2672
- rules: [
2673
- {
2674
- id: "rule_1",
2675
- if: { field: "created_at", op: "duration_minutes", value: { end: "now", gte: 30 } },
2676
- then: { decision: "old_enough" }
2677
- }
2678
- ]
2679
- }
2680
-
2681
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2682
- context = DecisionAgent::Context.new({ created_at: (Time.now - 3600).iso8601 })
2683
-
2684
- evaluation = evaluator.evaluate(context)
2685
-
2686
- expect(evaluation).not_to be_nil
2687
- expect(evaluation.decision).to eq("old_enough")
2688
- end
2689
- end
2690
-
2691
- describe "duration_hours and duration_days operators" do
2692
- it "calculates duration in hours" do
2693
- rules = {
2694
- version: "1.0",
2695
- ruleset: "test",
2696
- rules: [
2697
- {
2698
- id: "rule_1",
2699
- if: { field: "start", op: "duration_hours", value: { end: "now", gte: 1 } },
2700
- then: { decision: "match" }
2701
- }
2702
- ]
2703
- }
2704
-
2705
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2706
- context = DecisionAgent::Context.new({ start: (Time.now - 7200).iso8601 })
2707
-
2708
- evaluation = evaluator.evaluate(context)
2709
-
2710
- expect(evaluation).not_to be_nil
2711
- expect(evaluation.decision).to eq("match")
2712
- end
2713
-
2714
- it "calculates duration in days" do
2715
- rules = {
2716
- version: "1.0",
2717
- ruleset: "test",
2718
- rules: [
2719
- {
2720
- id: "rule_1",
2721
- if: { field: "trial_start", op: "duration_days", value: { end: "now", gte: 7 } },
2722
- then: { decision: "trial_expired" }
2723
- }
2724
- ]
2725
- }
2726
-
2727
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2728
- context = DecisionAgent::Context.new({ trial_start: (Time.now - (8 * 86_400)).iso8601 })
2729
-
2730
- evaluation = evaluator.evaluate(context)
2731
-
2732
- expect(evaluation).not_to be_nil
2733
- expect(evaluation.decision).to eq("trial_expired")
2734
- end
2735
- end
2736
- end
2737
-
2738
- # DATE ARITHMETIC
2739
- describe "date arithmetic operators" do
2740
- describe "add_days operator" do
2741
- it "adds days and compares with target" do
2742
- rules = {
2743
- version: "1.0",
2744
- ruleset: "test",
2745
- rules: [
2746
- {
2747
- id: "rule_1",
2748
- if: { field: "created_at", op: "add_days", value: { days: 7, compare: "lte", target: "now" } },
2749
- then: { decision: "week_old" }
2750
- }
2751
- ]
2752
- }
2753
-
2754
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2755
- context = DecisionAgent::Context.new({ created_at: (Time.now - (8 * 86_400)).iso8601 })
2756
-
2757
- evaluation = evaluator.evaluate(context)
2758
-
2759
- expect(evaluation).not_to be_nil
2760
- expect(evaluation.decision).to eq("week_old")
2761
- end
2762
- end
2763
-
2764
- describe "subtract_days, add_hours, subtract_hours, add_minutes, subtract_minutes operators" do
2765
- it "subtracts days correctly" do
2766
- rules = {
2767
- version: "1.0",
2768
- ruleset: "test",
2769
- rules: [
2770
- {
2771
- id: "rule_1",
2772
- if: { field: "deadline", op: "subtract_days", value: { days: 1, compare: "gt", target: "now" } },
2773
- then: { decision: "not_urgent" }
2774
- }
2775
- ]
2776
- }
2777
-
2778
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2779
- context = DecisionAgent::Context.new({ deadline: (Time.now + (2 * 86_400)).iso8601 })
2780
-
2781
- evaluation = evaluator.evaluate(context)
2782
-
2783
- expect(evaluation).not_to be_nil
2784
- expect(evaluation.decision).to eq("not_urgent")
2785
- end
2786
-
2787
- it "adds hours correctly" do
2788
- rules = {
2789
- version: "1.0",
2790
- ruleset: "test",
2791
- rules: [
2792
- {
2793
- id: "rule_1",
2794
- if: { field: "start", op: "add_hours", value: { hours: 2, compare: "lt", target: "now" } },
2795
- then: { decision: "past_2h" }
2796
- }
2797
- ]
2798
- }
2799
-
2800
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2801
- context = DecisionAgent::Context.new({ start: (Time.now - 7200).iso8601 })
2802
-
2803
- evaluation = evaluator.evaluate(context)
2804
-
2805
- expect(evaluation).not_to be_nil
2806
- expect(evaluation.decision).to eq("past_2h")
2807
- end
2808
- end
2809
- end
2810
-
2811
- # TIME COMPONENT EXTRACTION
2812
- describe "time component extraction operators" do
2813
- describe "hour_of_day operator" do
2814
- it "extracts hour and compares" do
2815
- rules = {
2816
- version: "1.0",
2817
- ruleset: "test",
2818
- rules: [
2819
- {
2820
- id: "rule_1",
2821
- if: { field: "timestamp", op: "hour_of_day", value: { gte: 9, lte: 17 } },
2822
- then: { decision: "business_hours" }
2823
- }
2824
- ]
2825
- }
2826
-
2827
- time = Time.new(2025, 1, 1, 14, 0, 0)
2828
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2829
- context = DecisionAgent::Context.new({ timestamp: time.iso8601 })
2830
-
2831
- evaluation = evaluator.evaluate(context)
2832
-
2833
- expect(evaluation).not_to be_nil
2834
- expect(evaluation.decision).to eq("business_hours")
2835
- end
2836
- end
2837
-
2838
- describe "day_of_month, month, year, week_of_year operators" do
2839
- it "extracts day of month" do
2840
- rules = {
2841
- version: "1.0",
2842
- ruleset: "test",
2843
- rules: [
2844
- {
2845
- id: "rule_1",
2846
- if: { field: "date", op: "day_of_month", value: 15 },
2847
- then: { decision: "mid_month" }
2848
- }
2849
- ]
2850
- }
2851
-
2852
- time = Time.new(2025, 1, 15, 12, 0, 0)
2853
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2854
- context = DecisionAgent::Context.new({ date: time.iso8601 })
2855
-
2856
- evaluation = evaluator.evaluate(context)
2857
-
2858
- expect(evaluation).not_to be_nil
2859
- expect(evaluation.decision).to eq("mid_month")
2860
- end
2861
-
2862
- it "extracts month" do
2863
- rules = {
2864
- version: "1.0",
2865
- ruleset: "test",
2866
- rules: [
2867
- {
2868
- id: "rule_1",
2869
- if: { field: "event_date", op: "month", value: 12 },
2870
- then: { decision: "december" }
2871
- }
2872
- ]
2873
- }
2874
-
2875
- time = Time.new(2025, 12, 25, 12, 0, 0)
2876
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2877
- context = DecisionAgent::Context.new({ event_date: time.iso8601 })
2878
-
2879
- evaluation = evaluator.evaluate(context)
2880
-
2881
- expect(evaluation).not_to be_nil
2882
- expect(evaluation.decision).to eq("december")
2883
- end
2884
- end
2885
- end
2886
-
2887
- # RATE CALCULATIONS
2888
- describe "rate calculation operators" do
2889
- describe "rate_per_second operator" do
2890
- it "calculates rate per second from timestamps" do
2891
- rules = {
2892
- version: "1.0",
2893
- ruleset: "test",
2894
- rules: [
2895
- {
2896
- id: "rule_1",
2897
- if: { field: "request_timestamps", op: "rate_per_second", value: { max: 10 } },
2898
- then: { decision: "within_limit" }
2899
- }
2900
- ]
2901
- }
2902
-
2903
- now = Time.now
2904
- timestamps = [
2905
- (now - 5).iso8601,
2906
- (now - 4).iso8601,
2907
- (now - 3).iso8601,
2908
- (now - 2).iso8601,
2909
- (now - 1).iso8601
2910
- ]
2911
-
2912
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2913
- context = DecisionAgent::Context.new({ request_timestamps: timestamps })
2914
-
2915
- evaluation = evaluator.evaluate(context)
2916
-
2917
- expect(evaluation).not_to be_nil
2918
- expect(evaluation.decision).to eq("within_limit")
2919
- end
2920
- end
2921
-
2922
- describe "rate_per_minute and rate_per_hour operators" do
2923
- it "calculates rate per minute" do
2924
- rules = {
2925
- version: "1.0",
2926
- ruleset: "test",
2927
- rules: [
2928
- {
2929
- id: "rule_1",
2930
- if: { field: "events", op: "rate_per_minute", value: { max: 60 } },
2931
- then: { decision: "ok" }
2932
- }
2933
- ]
2934
- }
2935
-
2936
- now = Time.now
2937
- timestamps = [
2938
- (now - 60).iso8601,
2939
- (now - 30).iso8601,
2940
- now.iso8601
2941
- ]
2942
-
2943
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2944
- context = DecisionAgent::Context.new({ events: timestamps })
2945
-
2946
- evaluation = evaluator.evaluate(context)
2947
-
2948
- expect(evaluation).not_to be_nil
2949
- expect(evaluation.decision).to eq("ok")
2950
- end
2951
- end
2952
- end
2953
-
2954
- # MOVING WINDOW CALCULATIONS
2955
- describe "moving window operators" do
2956
- describe "moving_average operator" do
2957
- it "calculates moving average over window" do
2958
- rules = {
2959
- version: "1.0",
2960
- ruleset: "test",
2961
- rules: [
2962
- {
2963
- id: "rule_1",
2964
- if: { field: "metrics", op: "moving_average", value: { window: 5, lte: 100 } },
2965
- then: { decision: "low_avg" }
2966
- }
2967
- ]
2968
- }
2969
-
2970
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2971
- context = DecisionAgent::Context.new({ metrics: [80, 85, 90, 95, 100] })
2972
-
2973
- evaluation = evaluator.evaluate(context)
2974
-
2975
- expect(evaluation).not_to be_nil
2976
- expect(evaluation.decision).to eq("low_avg")
2977
- end
2978
- end
2979
-
2980
- describe "moving_sum, moving_max, moving_min operators" do
2981
- it "calculates moving sum" do
2982
- rules = {
2983
- version: "1.0",
2984
- ruleset: "test",
2985
- rules: [
2986
- {
2987
- id: "rule_1",
2988
- if: { field: "values", op: "moving_sum", value: { window: 3, gte: 25 } },
2989
- then: { decision: "match" }
2990
- }
2991
- ]
2992
- }
2993
-
2994
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
2995
- context = DecisionAgent::Context.new({ values: [10, 10, 10, 5] })
2996
-
2997
- evaluation = evaluator.evaluate(context)
2998
-
2999
- expect(evaluation).not_to be_nil
3000
- expect(evaluation.decision).to eq("match")
3001
- end
3002
- end
3003
- end
3004
-
3005
- # FINANCIAL CALCULATIONS
3006
- describe "financial calculation operators" do
3007
- describe "compound_interest operator" do
3008
- it "calculates compound interest correctly" do
3009
- rules = {
3010
- version: "1.0",
3011
- ruleset: "test",
3012
- rules: [
3013
- {
3014
- id: "rule_1",
3015
- if: { field: "principal", op: "compound_interest", value: { rate: 0.05, periods: 12, result: 1051.16 } },
3016
- then: { decision: "match" }
3017
- }
3018
- ]
3019
- }
3020
-
3021
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3022
- context = DecisionAgent::Context.new({ principal: 1000 })
3023
-
3024
- evaluation = evaluator.evaluate(context)
3025
-
3026
- expect(evaluation).not_to be_nil
3027
- expect(evaluation.decision).to eq("match")
3028
- end
3029
- end
3030
-
3031
- describe "present_value and future_value operators" do
3032
- it "calculates present value" do
3033
- rules = {
3034
- version: "1.0",
3035
- ruleset: "test",
3036
- rules: [
3037
- {
3038
- id: "rule_1",
3039
- if: { field: "future_value", op: "present_value", value: { rate: 0.05, periods: 10, result: 613.91 } },
3040
- then: { decision: "match" }
3041
- }
3042
- ]
3043
- }
3044
-
3045
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3046
- context = DecisionAgent::Context.new({ future_value: 1000 })
3047
-
3048
- evaluation = evaluator.evaluate(context)
3049
-
3050
- expect(evaluation).not_to be_nil
3051
- expect(evaluation.decision).to eq("match")
3052
- end
3053
- end
3054
- end
3055
-
3056
- # STRING AGGREGATIONS
3057
- describe "string aggregation operators" do
3058
- describe "join operator" do
3059
- it "joins array with separator and matches result" do
3060
- rules = {
3061
- version: "1.0",
3062
- ruleset: "test",
3063
- rules: [
3064
- {
3065
- id: "rule_1",
3066
- if: { field: "tags", op: "join", value: { separator: ",", result: "a,b,c" } },
3067
- then: { decision: "match" }
3068
- }
3069
- ]
3070
- }
3071
-
3072
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3073
- context = DecisionAgent::Context.new({ tags: %w[a b c] })
3074
-
3075
- evaluation = evaluator.evaluate(context)
3076
-
3077
- expect(evaluation).not_to be_nil
3078
- expect(evaluation.decision).to eq("match")
3079
- end
3080
-
3081
- it "matches with contains check" do
3082
- rules = {
3083
- version: "1.0",
3084
- ruleset: "test",
3085
- rules: [
3086
- {
3087
- id: "rule_1",
3088
- if: { field: "tags", op: "join", value: { separator: ",", contains: "important" } },
3089
- then: { decision: "has_important" }
3090
- }
3091
- ]
3092
- }
3093
-
3094
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3095
- context = DecisionAgent::Context.new({ tags: %w[urgent important critical] })
3096
-
3097
- evaluation = evaluator.evaluate(context)
3098
-
3099
- expect(evaluation).not_to be_nil
3100
- expect(evaluation.decision).to eq("has_important")
3101
- end
3102
- end
3103
-
3104
- describe "length operator" do
3105
- it "matches when string length meets threshold" do
3106
- rules = {
3107
- version: "1.0",
3108
- ruleset: "test",
3109
- rules: [
3110
- {
3111
- id: "rule_1",
3112
- if: { field: "description", op: "length", value: { min: 10, max: 500 } },
3113
- then: { decision: "valid_length" }
3114
- }
3115
- ]
3116
- }
3117
-
3118
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3119
- context = DecisionAgent::Context.new({ description: "This is a valid description" })
3120
-
3121
- evaluation = evaluator.evaluate(context)
3122
-
3123
- expect(evaluation).not_to be_nil
3124
- expect(evaluation.decision).to eq("valid_length")
3125
- end
3126
-
3127
- it "works with arrays" do
3128
- rules = {
3129
- version: "1.0",
3130
- ruleset: "test",
3131
- rules: [
3132
- {
3133
- id: "rule_1",
3134
- if: { field: "items", op: "length", value: { gte: 3 } },
3135
- then: { decision: "enough_items" }
3136
- }
3137
- ]
3138
- }
3139
-
3140
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
3141
- context = DecisionAgent::Context.new({ items: %w[a b c d] })
3142
-
3143
- evaluation = evaluator.evaluate(context)
3144
-
3145
- expect(evaluation).not_to be_nil
3146
- expect(evaluation.decision).to eq("enough_items")
3147
- end
3148
- end
3149
- end
3150
- end