@@ -931,3 +931,146 @@ def test_state_alerts_flapping(db_session):
931931 assert alert .status == AlertStatus .PENDING
932932 else :
933933 assert alert .status == AlertStatus .FIRING
934+
935+
936+ def test_cel_equality_int_str_type_coercion (db_session ):
937+ """
938+ Reproduce the bug: CEL 'field == "2"' should match payload {"field": 2} and vice versa.
939+ """
940+ from keep .api .models .alert import AlertDto
941+ from keep .rulesengine .rulesengine import RulesEngine
942+
943+ # Case 1: field is int, CEL checks for string
944+ alert1 = AlertDto (id = "a1" , name = "test" , field = 2 , fingerprint = "fp1" )
945+ cel1 = 'field == "2"'
946+ engine = RulesEngine ()
947+ result1 = engine .filter_alerts ([alert1 ], cel1 )
948+ print (f"Case 1 result: { result1 } " )
949+ assert len (result1 ) == 1 , "CEL 'field == \" 2\" ' should match payload {field: 2}"
950+
951+ # Case 2: field is str, CEL checks for int
952+ alert2 = AlertDto (id = "a2" , name = "test" , field = "2" , fingerprint = "fp2" )
953+ cel2 = "field == 2"
954+ result2 = engine .filter_alerts ([alert2 ], cel2 )
955+ print (f"Case 2 result: { result2 } " )
956+ assert len (result2 ) == 1 , "CEL 'field == 2' should match payload {field: '2'}"
957+
958+ # Case 3: field is int, CEL checks for int (should match)
959+ alert3 = AlertDto (id = "a3" , name = "test" , field = 2 , fingerprint = "fp3" )
960+ cel3 = "field == 2"
961+ result3 = engine .filter_alerts ([alert3 ], cel3 )
962+ assert len (result3 ) == 1
963+
964+ # Case 4: field is str, CEL checks for str (should match)
965+ alert4 = AlertDto (id = "a4" , name = "test" , field = "2" , fingerprint = "fp4" )
966+ cel4 = 'field == "2"'
967+ result4 = engine .filter_alerts ([alert4 ], cel4 )
968+ assert len (result4 ) == 1
969+
970+
971+ def test_check_if_rule_apply_int_str_type_coercion (db_session ):
972+ """
973+ Test that _check_if_rule_apply handles type coercion between int and str in CEL expressions.
974+ This reproduces the same bug as test_cel_equality_int_str_type_coercion but for rule evaluation.
975+ """
976+ from datetime import datetime
977+
978+ from keep .api .core .dependencies import SINGLE_TENANT_UUID
979+ from keep .api .models .alert import AlertDto
980+ from keep .api .models .db .rule import Rule
981+ from keep .rulesengine .rulesengine import RulesEngine
982+
983+ # Create a test rule with CEL expression that checks for string equality with int payload
984+ rule = Rule (
985+ id = "test-rule-1" ,
986+ tenant_id = SINGLE_TENANT_UUID ,
987+ name = "Test Rule - Int Str Coercion" ,
988+ definition_cel = 'field == "2"' , # CEL checks for string "2"
989+ definition = {},
990+ timeframe = 60 ,
991+ timeunit = "seconds" ,
992+ 993+ creation_time = datetime .utcnow (),
994+ grouping_criteria = [],
995+ threshold = 1 ,
996+ )
997+
998+ engine = RulesEngine (tenant_id = SINGLE_TENANT_UUID )
999+
1000+ # Case 1: field is int (2), CEL checks for string ("2") - should match
1001+ alert1 = AlertDto (id = "a1" , name = "test" , field = 2 , fingerprint = "fp1" , source = ["test" ])
1002+ matched_rules1 = engine ._check_if_rule_apply (rule , alert1 )
1003+ print (f"Case 1 - field=2, CEL='field == \" 2\" ': matched_rules={ matched_rules1 } " )
1004+ assert (
1005+ len (matched_rules1 ) == 1
1006+ ), "Rule with 'field == \" 2\" ' should match alert with field=2"
1007+
1008+ # Case 2: field is string ("2"), CEL checks for int (2) - should match
1009+ rule2 = Rule (
1010+ id = "test-rule-2" ,
1011+ tenant_id = SINGLE_TENANT_UUID ,
1012+ name = "Test Rule - Str Int Coercion" ,
1013+ definition_cel = "field == 2" , # CEL checks for int 2
1014+ definition = {},
1015+ timeframe = 60 ,
1016+ timeunit = "seconds" ,
1017+ 1018+ creation_time = datetime .utcnow (),
1019+ grouping_criteria = [],
1020+ threshold = 1 ,
1021+ )
1022+
1023+ alert2 = AlertDto (
1024+ id = "a2" , name = "test" , field = "2" , fingerprint = "fp2" , source = ["test" ]
1025+ )
1026+ matched_rules2 = engine ._check_if_rule_apply (rule2 , alert2 )
1027+ print (f"Case 2 - field='2', CEL='field == 2': matched_rules={ matched_rules2 } " )
1028+ assert (
1029+ len (matched_rules2 ) == 1
1030+ ), "Rule with 'field == 2' should match alert with field='2'"
1031+
1032+ # Case 3: field is int (2), CEL checks for int (2) - should match
1033+ rule3 = Rule (
1034+ id = "test-rule-3" ,
1035+ tenant_id = SINGLE_TENANT_UUID ,
1036+ name = "Test Rule - Int Int" ,
1037+ definition_cel = "field == 2" , # CEL checks for int 2
1038+ definition = {},
1039+ timeframe = 60 ,
1040+ timeunit = "seconds" ,
1041+ 1042+ creation_time = datetime .utcnow (),
1043+ grouping_criteria = [],
1044+ threshold = 1 ,
1045+ )
1046+
1047+ alert3 = AlertDto (id = "a3" , name = "test" , field = 2 , fingerprint = "fp3" , source = ["test" ])
1048+ matched_rules3 = engine ._check_if_rule_apply (rule3 , alert3 )
1049+ print (f"Case 3 - field=2, CEL='field == 2': matched_rules={ matched_rules3 } " )
1050+ assert (
1051+ len (matched_rules3 ) == 1
1052+ ), "Rule with 'field == 2' should match alert with field=2"
1053+
1054+ # Case 4: field is string ("2"), CEL checks for string ("2") - should match
1055+ rule4 = Rule (
1056+ id = "test-rule-4" ,
1057+ tenant_id = SINGLE_TENANT_UUID ,
1058+ name = "Test Rule - Str Str" ,
1059+ definition_cel = 'field == "2"' , # CEL checks for string "2"
1060+ definition = {},
1061+ timeframe = 60 ,
1062+ timeunit = "seconds" ,
1063+ 1064+ creation_time = datetime .utcnow (),
1065+ grouping_criteria = [],
1066+ threshold = 1 ,
1067+ )
1068+
1069+ alert4 = AlertDto (
1070+ id = "a4" , name = "test" , field = "2" , fingerprint = "fp4" , source = ["test" ]
1071+ )
1072+ matched_rules4 = engine ._check_if_rule_apply (rule4 , alert4 )
1073+ print (f"Case 4 - field='2', CEL='field == \" 2\" ': matched_rules={ matched_rules4 } " )
1074+ assert (
1075+ len (matched_rules4 ) == 1
1076+ ), "Rule with 'field == \" 2\" ' should match alert with field='2'"
0 commit comments