1+ # ---------------------------------------------------------
2+ # Copyright (c) Microsoft Corporation. All rights reserved.
3+ # ---------------------------------------------------------
4+
5+ import pytest
6+ from unittest .mock import AsyncMock , MagicMock , patch
7+ from azure .ai .evaluation .simulator import DirectAttackSimulator , AdversarialScenario
8+ from azure .ai .evaluation .simulator ._utils import JsonLineList
9+ from azure .core .credentials import TokenCredential
10+
11+
12+ @pytest .fixture
13+ def mock_credential ():
14+ return MagicMock (spec = TokenCredential )
15+
16+
17+ @pytest .fixture
18+ def mock_azure_ai_project ():
19+ return {
20+ "subscription_id" : "mock-sub" ,
21+ "resource_group_name" : "mock-rg" ,
22+ "project_name" : "mock-proj"
23+ }
24+
25+
26+ @pytest .fixture
27+ def mock_target ():
28+ async def mock_target_fn (query : str ) -> str :
29+ return "mock response"
30+ return mock_target_fn
31+
32+
33+ @pytest .mark .unittest
34+ class TestDirectAttackSimulator :
35+
36+ @pytest .mark .asyncio
37+ @patch ("azure.ai.evaluation.simulator._direct_attack_simulator.DirectAttackSimulator._ensure_service_dependencies" )
38+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__init__" , return_value = None )
39+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__call__" , new_callable = AsyncMock )
40+ async def test_different_randomization_seeds_fix (
41+ self ,
42+ mock_adv_call ,
43+ mock_adv_init ,
44+ mock_ensure_deps ,
45+ mock_azure_ai_project ,
46+ mock_credential ,
47+ mock_target
48+ ):
49+ """Test that DirectAttackSimulator uses different seeds for regular and jailbreak simulations."""
50+
51+ # Setup mock returns
52+ mock_result = JsonLineList ([
53+ {"messages" : [{"content" : "test_query" , "role" : "user" }]}
54+ ])
55+ mock_adv_call .return_value = mock_result
56+
57+ # Create DirectAttackSimulator
58+ simulator = DirectAttackSimulator (
59+ azure_ai_project = mock_azure_ai_project ,
60+ credential = mock_credential
61+ )
62+
63+ # Call with fixed randomization seed
64+ result = await simulator (
65+ scenario = AdversarialScenario .ADVERSARIAL_QA ,
66+ target = mock_target ,
67+ max_simulation_results = 3 ,
68+ randomization_seed = 42
69+ )
70+
71+ # Verify that AdversarialSimulator was called twice (regular and jailbreak)
72+ assert mock_adv_call .call_count == 2
73+
74+ # Extract the randomization_seed from each call
75+ call_kwargs_list = [call [1 ] for call in mock_adv_call .call_args_list ]
76+ regular_seed = call_kwargs_list [0 ].get ("randomization_seed" )
77+ jailbreak_seed = call_kwargs_list [1 ].get ("randomization_seed" )
78+
79+ # The fix should ensure different seeds are used
80+ assert regular_seed != jailbreak_seed , "Regular and jailbreak simulations should use different seeds"
81+ assert regular_seed == 42 , "Regular simulation should use the original seed"
82+ assert jailbreak_seed == 43 , "Jailbreak simulation should use derived seed (original + 1)"
83+
84+ # Verify the structure of the result
85+ assert "regular" in result
86+ assert "jailbreak" in result
87+ assert result ["regular" ] == mock_result
88+ assert result ["jailbreak" ] == mock_result
89+
90+ @pytest .mark .asyncio
91+ @patch ("azure.ai.evaluation.simulator._direct_attack_simulator.DirectAttackSimulator._ensure_service_dependencies" )
92+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__init__" , return_value = None )
93+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__call__" , new_callable = AsyncMock )
94+ async def test_edge_case_max_seed_value (
95+ self ,
96+ mock_adv_call ,
97+ mock_adv_init ,
98+ mock_ensure_deps ,
99+ mock_azure_ai_project ,
100+ mock_credential ,
101+ mock_target
102+ ):
103+ """Test edge case when randomization_seed is at maximum value."""
104+
105+ # Setup mock returns
106+ mock_result = JsonLineList ([
107+ {"messages" : [{"content" : "test_query" , "role" : "user" }]}
108+ ])
109+ mock_adv_call .return_value = mock_result
110+
111+ # Create DirectAttackSimulator
112+ simulator = DirectAttackSimulator (
113+ azure_ai_project = mock_azure_ai_project ,
114+ credential = mock_credential
115+ )
116+
117+ # Call with max seed value
118+ max_seed = 999999
119+ result = await simulator (
120+ scenario = AdversarialScenario .ADVERSARIAL_QA ,
121+ target = mock_target ,
122+ max_simulation_results = 3 ,
123+ randomization_seed = max_seed
124+ )
125+
126+ # Verify that AdversarialSimulator was called twice
127+ assert mock_adv_call .call_count == 2
128+
129+ # Extract the randomization_seed from each call
130+ call_kwargs_list = [call [1 ] for call in mock_adv_call .call_args_list ]
131+ regular_seed = call_kwargs_list [0 ].get ("randomization_seed" )
132+ jailbreak_seed = call_kwargs_list [1 ].get ("randomization_seed" )
133+
134+ # When at max value, jailbreak seed should be original - 1
135+ assert regular_seed != jailbreak_seed , "Seeds should still be different at max value"
136+ assert regular_seed == max_seed , "Regular simulation should use the original max seed"
137+ assert jailbreak_seed == max_seed - 1 , "Jailbreak simulation should use max seed - 1"
138+
139+ @pytest .mark .asyncio
140+ @patch ("azure.ai.evaluation.simulator._direct_attack_simulator.DirectAttackSimulator._ensure_service_dependencies" )
141+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__init__" , return_value = None )
142+ @patch ("azure.ai.evaluation.simulator.AdversarialSimulator.__call__" , new_callable = AsyncMock )
143+ async def test_no_seed_provided_generates_different_seeds (
144+ self ,
145+ mock_adv_call ,
146+ mock_adv_init ,
147+ mock_ensure_deps ,
148+ mock_azure_ai_project ,
149+ mock_credential ,
150+ mock_target
151+ ):
152+ """Test that when no seed is provided, different seeds are still generated."""
153+
154+ # Setup mock returns
155+ mock_result = JsonLineList ([
156+ {"messages" : [{"content" : "test_query" , "role" : "user" }]}
157+ ])
158+ mock_adv_call .return_value = mock_result
159+
160+ # Create DirectAttackSimulator
161+ simulator = DirectAttackSimulator (
162+ azure_ai_project = mock_azure_ai_project ,
163+ credential = mock_credential
164+ )
165+
166+ # Call without providing randomization_seed (it will be generated randomly)
167+ result = await simulator (
168+ scenario = AdversarialScenario .ADVERSARIAL_QA ,
169+ target = mock_target ,
170+ max_simulation_results = 3
171+ )
172+
173+ # Verify that AdversarialSimulator was called twice
174+ assert mock_adv_call .call_count == 2
175+
176+ # Extract the randomization_seed from each call
177+ call_kwargs_list = [call [1 ] for call in mock_adv_call .call_args_list ]
178+ regular_seed = call_kwargs_list [0 ].get ("randomization_seed" )
179+ jailbreak_seed = call_kwargs_list [1 ].get ("randomization_seed" )
180+
181+ # Even with random generation, seeds should be different
182+ assert regular_seed != jailbreak_seed , "Generated seeds should be different"
183+ assert jailbreak_seed == regular_seed + 1 or jailbreak_seed == regular_seed - 1 , "Jailbreak seed should be derived from regular seed"
0 commit comments