@@ -931,6 +931,73 @@ async def test_parse_adversarial_response_with_various_inputs(
931931 result = attack ._parse_adversarial_response (response_json )
932932 assert isinstance (result , str )
933933
934+ @pytest .mark .parametrize (
935+ "raw,expected" ,
936+ [
937+ ("generated_question" , "generated_question" ),
938+ ("generatedQuestion" , "generated_question" ),
939+ ("GeneratedQuestion" , "generated_question" ),
940+ ("rationaleBehindJailbreak" , "rationale_behind_jailbreak" ),
941+ ("lastResponseSummary" , "last_response_summary" ),
942+ ("" , "" ),
943+ ],
944+ )
945+ def test_camel_to_snake_handles_common_cases (self , raw : str , expected : str ) -> None :
946+ """``_camel_to_snake`` normalizes camelCase / PascalCase and leaves snake_case alone."""
947+ assert CrescendoAttack ._camel_to_snake (raw ) == expected
948+
949+ def test_parse_adversarial_response_accepts_camel_case_keys (
950+ self ,
951+ mock_objective_target : MagicMock ,
952+ mock_adversarial_chat : MagicMock ,
953+ ) -> None :
954+ """camelCase keys are normalized to snake_case so well-formed JSON with the wrong casing still parses.
955+
956+ Regression test for the Azure DevOps Integration Tests failure on
957+ ``4_sequential_attack.ipynb``, where the adversarial model returned
958+ ``generatedQuestion`` / ``rationaleBehindJailbreak`` /
959+ ``lastResponseSummary`` for three retries straight and the strict
960+ snake_case-only parser tore down the run.
961+ """
962+ attack = CrescendoTestHelper .create_attack (
963+ objective_target = mock_objective_target ,
964+ adversarial_chat = mock_adversarial_chat ,
965+ )
966+ camel_case_response = (
967+ '{"generatedQuestion": "Attack question", '
968+ '"lastResponseSummary": "Summary text", '
969+ '"rationaleBehindJailbreak": "Why this works"}'
970+ )
971+
972+ result = attack ._parse_adversarial_response (camel_case_response )
973+
974+ assert result == "Attack question"
975+
976+ def test_parse_adversarial_response_mixed_casing_still_validates_extras (
977+ self ,
978+ mock_objective_target : MagicMock ,
979+ mock_adversarial_chat : MagicMock ,
980+ ) -> None :
981+ """Extra keys remain rejected even after camelCase normalization.
982+
983+ ``unexpectedKey`` normalizes to ``unexpected_key`` (still not in the
984+ expected set), so the strict extra-key check continues to fire — we
985+ only loosen casing, not the schema.
986+ """
987+ attack = CrescendoTestHelper .create_attack (
988+ objective_target = mock_objective_target ,
989+ adversarial_chat = mock_adversarial_chat ,
990+ )
991+ response_with_extra = (
992+ '{"generatedQuestion": "Attack", '
993+ '"lastResponseSummary": "Summary", '
994+ '"rationaleBehindJailbreak": "Rationale", '
995+ '"unexpectedKey": "value"}'
996+ )
997+
998+ with pytest .raises (InvalidJsonException , match = "Unexpected keys" ):
999+ attack ._parse_adversarial_response (response_with_extra )
1000+
9341001 async def test_custom_message_is_sent_to_target (
9351002 self ,
9361003 mock_objective_target : MagicMock ,
0 commit comments