@@ -169,3 +169,82 @@ async def test_gandalf_scorer_wraps_httpx_error_as_pyrit_exception(mocked_post,
169169 scorer = GandalfScorer (level = GandalfLevel .LEVEL_1 , chat_target = chat_target )
170170 with pytest .raises (PyritException , match = "Error in scorer GandalfScorer" ):
171171 await scorer .score_async (scorable = MessageScorable .from_message (await store_message_async (response )))
172+
173+
174+ @pytest .mark .parametrize ("sentinel" , ["NO" , "no" , "No" , "NO." , "no." , " No " , "\n NO\n " ])
175+ @patch ("pyrit.score.true_false.gandalf_scorer.make_request_and_raise_if_error_async" , new_callable = AsyncMock )
176+ async def test_gandalf_scorer_accepts_no_sentinel_case_and_punctuation_variants (
177+ mocked_post , sqlite_instance : MemoryInterface , sentinel : str
178+ ):
179+ """A case or punctuation variant of the scorer's own "NO" answer still means no password.
180+
181+ The password-search utility is a chat target, so "no." and "No" are ordinary
182+ replies to "your response should be NO". Treating them as a password posts a
183+ guess to Gandalf and reports "Invalid password found in text."
184+ """
185+ conversation_id = str (uuid .uuid4 ())
186+ (await sqlite_instance .add_message_to_memory_async (request = generate_request (conversation_id = conversation_id )))
187+ response = generate_password_extraction_response (sentinel , conversation_id = conversation_id )
188+ (await sqlite_instance .add_message_to_memory_async (request = response ))
189+
190+ chat_target = MagicMock (spec = PromptTarget )
191+ chat_target .get_identifier .return_value = get_mock_target_identifier ("MockChatTarget" )
192+ chat_target .send_prompt_async = AsyncMock (return_value = [response ])
193+
194+ scorer = GandalfScorer (level = GandalfLevel .LEVEL_1 , chat_target = chat_target )
195+ mocked_post .return_value = MagicMock (json = lambda : {"success" : False , "message" : "Message" })
196+
197+ scores = await scorer .score_async (scorable = MessageScorable .from_message (await store_message_async (response )))
198+
199+ mocked_post .assert_not_called ()
200+ assert scores [0 ].get_value () is False
201+ assert scores [0 ].score_rationale == "No password found in text."
202+
203+
204+ @pytest .mark .parametrize ("negative_reply" , ["NO, I cannot help" , "No. No password was found." , "no\n I cannot help" ])
205+ @patch ("pyrit.score.true_false.gandalf_scorer.make_request_and_raise_if_error_async" , new_callable = AsyncMock )
206+ async def test_gandalf_scorer_scores_negative_prose_as_false_async (
207+ mocked_post : AsyncMock , * , sqlite_instance : MemoryInterface , negative_reply : str
208+ ) -> None :
209+ """A negative reply returns False without a password guess."""
210+ conversation_id = str (uuid .uuid4 ())
211+ (await sqlite_instance .add_message_to_memory_async (request = generate_request (conversation_id = conversation_id )))
212+ response = generate_password_extraction_response (negative_reply , conversation_id = conversation_id )
213+ (await sqlite_instance .add_message_to_memory_async (request = response ))
214+
215+ chat_target = MagicMock (spec = PromptTarget )
216+ chat_target .get_identifier .return_value = get_mock_target_identifier ("MockChatTarget" )
217+ chat_target .send_prompt_async = AsyncMock (return_value = [response ])
218+
219+ scorer = GandalfScorer (level = GandalfLevel .LEVEL_1 , chat_target = chat_target )
220+ mocked_post .return_value = MagicMock (json = lambda : {"success" : False , "message" : "Message" })
221+
222+ scores = await scorer .score_async (scorable = MessageScorable .from_message (await store_message_async (response )))
223+
224+ mocked_post .assert_not_called ()
225+ assert scores [0 ].get_value () is False
226+ assert scores [0 ].score_rationale == "No password found in text."
227+
228+
229+ @pytest .mark .parametrize ("password" , ["SUN SHINE" , "sun-shine" , "correct horse" ])
230+ @patch ("pyrit.score.true_false.gandalf_scorer.make_request_and_raise_if_error_async" , new_callable = AsyncMock )
231+ async def test_gandalf_scorer_still_posts_multi_word_passwords (
232+ mocked_post , sqlite_instance : MemoryInterface , password : str
233+ ):
234+ """A real password may contain spaces or dashes; the sentinel check must not swallow it."""
235+ conversation_id = str (uuid .uuid4 ())
236+ (await sqlite_instance .add_message_to_memory_async (request = generate_request (conversation_id = conversation_id )))
237+ response = generate_password_extraction_response (password , conversation_id = conversation_id )
238+ (await sqlite_instance .add_message_to_memory_async (request = response ))
239+
240+ chat_target = MagicMock (spec = PromptTarget )
241+ chat_target .get_identifier .return_value = get_mock_target_identifier ("MockChatTarget" )
242+ chat_target .send_prompt_async = AsyncMock (return_value = [response ])
243+
244+ scorer = GandalfScorer (level = GandalfLevel .LEVEL_1 , chat_target = chat_target )
245+ mocked_post .return_value = MagicMock (json = lambda : {"success" : False , "message" : "Message" })
246+
247+ scores = await scorer .score_async (scorable = MessageScorable .from_message (await store_message_async (response )))
248+
249+ mocked_post .assert_called_once ()
250+ assert scores [0 ].score_rationale == f"Invalid password found in text. [value={ password } ]"
0 commit comments