decision_agent 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -385,6 +385,1244 @@ RSpec.describe "Advanced DSL Operators" do
385
385
  expect(evaluation).to be_nil
386
386
  end
387
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
388
1626
  end
389
1627
 
390
1628
  # DATE/TIME OPERATORS
@@ -1000,4 +2238,913 @@ RSpec.describe "Advanced DSL Operators" do
1000
2238
  expect(evaluation.decision).to eq("escalate")
1001
2239
  end
1002
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
1003
3150
  end