diff --git a/frontend/src/components/Scenarios/ScenarioDetail.test.tsx b/frontend/src/components/Scenarios/ScenarioDetail.test.tsx index 32a682a329..3164b76301 100644 --- a/frontend/src/components/Scenarios/ScenarioDetail.test.tsx +++ b/frontend/src/components/Scenarios/ScenarioDetail.test.tsx @@ -2023,7 +2023,8 @@ describe('ScenarioDetail', () => { }) it.each(['user', 'unknown'])('warns before replacing %s data and confirms through Run preview', async (origin) => { - const user = userEvent.setup() + jest.useFakeTimers() + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) jest.mocked(datasetsApi.listDatasets).mockResolvedValueOnce({ items: [{ name: 'harmbench', selection_key: 'dataset:named:harmbench', loaded: true, provider_available: true, logical_examples: 8, origins: [origin], modalities: ['text'], harm_categories: [], has_unlabeled_harm_categories: false, @@ -2035,7 +2036,9 @@ describe('ScenarioDetail', () => { }, }) renderDetail('/scanner/foundry.red_team_agent') + await flushRenderedPromises() await user.click(await screen.findByRole('radio', { name: 'Refresh' })) + await advanceTimers(300) await user.click(screen.getByRole('button', { name: 'Launch scan' })) const preview = await screen.findByRole('dialog', { name: 'Run preview' }) await waitFor(() => expect(within(preview).getByRole('button', { name: 'Launch scan' })).toBeEnabled()) @@ -2044,7 +2047,7 @@ describe('ScenarioDetail', () => { expect(within(preview).getByRole('alert')).toHaveTextContent('prevent older runs from resuming') await user.click(within(preview).getByRole('button', { name: 'Cancel' })) expect(mockStartRun).not.toHaveBeenCalled() - await user.click(screen.getByRole('button', { name: 'Launch scan' })) + await user.click(await screen.findByRole('button', { name: 'Launch scan' })) const confirmed = await screen.findByRole('dialog', { name: 'Run preview' }) await user.click(within(confirmed).getByRole('button', { name: 'Launch scan' })) expect(mockStartRun).toHaveBeenCalledTimes(1) diff --git a/pyrit/converter/insert_punctuation_converter.py b/pyrit/converter/insert_punctuation_converter.py index 11e1cde557..24e1a8c770 100644 --- a/pyrit/converter/insert_punctuation_converter.py +++ b/pyrit/converter/insert_punctuation_converter.py @@ -187,11 +187,10 @@ def _insert_within_words(self, prompt: str, num_insertions: int, punctuation_lis # List of chars in the prompt string prompt_list = list(prompt) rng = self._get_random_generator(stream="punctuation") - # Store random indices of prompt_list into insert_indices - # If the prompt has only 0 or 1 chars, insert at the end of the prompt - insert_indices = ( - [1] if len(prompt_list) <= num_insertions else rng.sample(range(len(prompt_list) - 1), num_insertions) - ) + # Store random indices of prompt_list into insert_indices. A list of n + # characters has n + 1 insertion points (before the first character + # through after the last), so every position stays reachable. + insert_indices = rng.sample(range(len(prompt_list) + 1), num_insertions) for index in insert_indices: # Insert into prompt_list at the insert_indices with random punctuation from the punctuation_list diff --git a/tests/unit/converter/test_insert_punctuation_converter.py b/tests/unit/converter/test_insert_punctuation_converter.py index e31e2edbca..b37311d71b 100644 --- a/tests/unit/converter/test_insert_punctuation_converter.py +++ b/tests/unit/converter/test_insert_punctuation_converter.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import re +from unittest.mock import patch import pytest @@ -78,3 +79,31 @@ async def test_punctuation_list_rejects_substrings_of_string_punctuation(punctua converter = InsertPunctuationConverter(seed=1) with pytest.raises(ValueError, match="Invalid punctuations"): await converter.convert_async(prompt="hello big world", punctuation_list=punctuation_list) + + +@pytest.mark.parametrize( + ("prompt", "offset"), + [("a", 0), ("a", 1), ("abc", 0), ("abc", 1), ("abc", 2), ("abc", 3)], +) +async def test_insert_within_words_reaches_every_insertion_position_async(*, prompt: str, offset: int) -> None: + converter = InsertPunctuationConverter(between_words=False, word_swap_ratio=1.0, seed=1) + with patch.object(converter, "_get_random_generator") as get_rng: + rng = get_rng.return_value + rng.sample.return_value = [offset] + rng.choice.return_value = "!" + result = await converter.convert_async(prompt=prompt, punctuation_list=["!"]) + + population, count = rng.sample.call_args.args + assert list(population) == list(range(len(prompt) + 1)) + assert count == 1 + assert result.output_text == f"{prompt[:offset]}!{prompt[offset:]}" + assert result.output_type == "text" + + +@pytest.mark.parametrize("prompt", ["", "...", " \t\n"]) +async def test_insert_within_words_without_words_async(prompt: str) -> None: + converter = InsertPunctuationConverter(between_words=False, word_swap_ratio=1.0, seed=1) + result = await converter.convert_async(prompt=prompt, punctuation_list=["!"]) + + assert result.output_text == f"!{prompt}" + assert result.output_type == "text"