From 6d1d0c52274f7ea18265fc76031e2a3f52330243 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:20:43 +0800 Subject: [PATCH 1/2] fix(converter): preserve unmapped characters in NatoConverter Non-alphabetic characters (digits, punctuation) are now passed through unchanged instead of being silently erased, so an encoded prompt keeps its full content and non-empty input never converts to an empty string. Matches the passthrough behavior of Arabizi/Atbash and the fidelity fix for BrailleConverter (#2309). Regression tests updated for digits, punctuation, and no-letters input. Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> --- pyrit/converter/nato_converter.py | 5 +++-- tests/unit/converter/test_nato_converter.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyrit/converter/nato_converter.py b/pyrit/converter/nato_converter.py index 7065feea8a..3bac866163 100644 --- a/pyrit/converter/nato_converter.py +++ b/pyrit/converter/nato_converter.py @@ -13,7 +13,8 @@ class NatoConverter(Converter): This converter transforms standard text into NATO phonetic alphabet format, where each letter is replaced with its corresponding NATO phonetic code word (e.g., "A" becomes "Alfa", "B" becomes "Bravo"). Only alphabetic characters - are converted; non-alphabetic characters are ignored. + are converted; non-alphabetic characters (digits, punctuation) are preserved + as-is, and spaces act as separators. The NATO phonetic alphabet is the most widely used spelling alphabet, designed to improve clarity of voice communication. This converter can be used to test @@ -90,6 +91,6 @@ def _convert_to_nato(self, text: str) -> str: Returns: str: The NATO phonetic alphabet representation, with code words separated by spaces. """ - output = [self._NATO_MAP[char] for char in text.upper() if char in self._NATO_MAP] + output = [self._NATO_MAP.get(char, char) for char in text.upper() if char != " "] return " ".join(output) diff --git a/tests/unit/converter/test_nato_converter.py b/tests/unit/converter/test_nato_converter.py index 2dbe2c0eda..a2ba2f8e2a 100644 --- a/tests/unit/converter/test_nato_converter.py +++ b/tests/unit/converter/test_nato_converter.py @@ -43,7 +43,7 @@ async def test_nato_converter_mixed_case(): async def test_nato_converter_with_numbers(): - """Test that numbers are ignored in NATO conversion.""" + """Test that numbers are preserved in NATO conversion.""" converter = NatoConverter() prompt = "a1b2c3" @@ -51,8 +51,8 @@ async def test_nato_converter_with_numbers(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Only letters should be converted - assert result.output_text == "Alfa Bravo Charlie" + # Digits are preserved as-is so the encoded prompt keeps its full content + assert result.output_text == "Alfa 1 Bravo 2 Charlie 3" async def test_nato_converter_with_spaces(): @@ -69,7 +69,7 @@ async def test_nato_converter_with_spaces(): async def test_nato_converter_with_punctuation(): - """Test that punctuation is ignored in NATO conversion.""" + """Test that punctuation is preserved in NATO conversion.""" converter = NatoConverter() prompt = "Hello, world!" @@ -77,8 +77,8 @@ async def test_nato_converter_with_punctuation(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Only letters should be converted - assert result.output_text == "Hotel Echo Lima Lima Oscar Whiskey Oscar Romeo Lima Delta" + # Punctuation is preserved as-is so the encoded prompt keeps its full content + assert result.output_text == "Hotel Echo Lima Lima Oscar , Whiskey Oscar Romeo Lima Delta !" async def test_nato_converter_empty_string(): @@ -94,7 +94,11 @@ async def test_nato_converter_empty_string(): async def test_nato_converter_no_letters(): - """Test NATO conversion with no alphabetic characters.""" + """Test NATO conversion with no alphabetic characters. + + Regression: non-empty input must never convert to an empty prompt + (digits/punctuation are preserved rather than erased). + """ converter = NatoConverter() prompt = "123!@#" @@ -102,7 +106,7 @@ async def test_nato_converter_no_letters(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - assert result.output_text == "" + assert result.output_text == "1 2 3 ! @ #" async def test_nato_converter_all_letters(): From 4e14e39311425f6a32410a30587466bae4ea29f8 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:34:42 -0700 Subject: [PATCH 2/2] fix(converter): preserve NATO word boundaries Refactor NatoConverter onto WordLevelConverter, represent source spaces explicitly, and preserve unmapped Unicode without case expansion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c9450170-29e5-4918-b1bc-71f6d9f46402 --- pyrit/converter/nato_converter.py | 54 ++++++++++----------- tests/unit/converter/test_nato_converter.py | 38 +++++++++++++-- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/pyrit/converter/nato_converter.py b/pyrit/converter/nato_converter.py index 3bac866163..551cc64ff1 100644 --- a/pyrit/converter/nato_converter.py +++ b/pyrit/converter/nato_converter.py @@ -2,19 +2,18 @@ # Licensed under the MIT license. -from pyrit.converter.converter import Converter, ConverterResult -from pyrit.models import PromptDataType +from pyrit.converter.word_level_converter import WordLevelConverter -class NatoConverter(Converter): +class NatoConverter(WordLevelConverter): """ Converts text into NATO phonetic alphabet representation. This converter transforms standard text into NATO phonetic alphabet format, - where each letter is replaced with its corresponding NATO phonetic code word - (e.g., "A" becomes "Alfa", "B" becomes "Bravo"). Only alphabetic characters - are converted; non-alphabetic characters (digits, punctuation) are preserved - as-is, and spaces act as separators. + where each ASCII letter is replaced with its corresponding NATO phonetic code + word (e.g., "A" becomes "Alfa", "B" becomes "Bravo"). Characters outside the + ASCII alphabet are preserved with their original casing. Spaces are represented + by ```` so word boundaries remain distinct from code-word separators. The NATO phonetic alphabet is the most widely used spelling alphabet, designed to improve clarity of voice communication. This converter can be used to test @@ -24,13 +23,15 @@ class NatoConverter(Converter): Reference: https://en.wikipedia.org/wiki/NATO_phonetic_alphabet Example: - Input: "Hello" - Output: "Hotel Echo Lima Lima Oscar" + Input: "Hello world" + Output: "Hotel Echo Lima Lima Oscar Whiskey Oscar Romeo Lima Delta" """ SUPPORTED_INPUT_TYPES = ("text",) SUPPORTED_OUTPUT_TYPES = ("text",) + _WORD_SEPARATOR = "" + _NATO_MAP = { "A": "Alfa", "B": "Bravo", @@ -60,37 +61,34 @@ class NatoConverter(Converter): "Z": "Zulu", } - async def convert_async(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult: + async def convert_word_async(self, word: str) -> str: """ - Convert the given text into NATO phonetic alphabet representation. + Convert one word into NATO phonetic alphabet representation. Args: - prompt (str): The text to be converted to NATO phonetic alphabet. - input_type (PromptDataType, optional): Type of input data. Defaults to "text". + word (str): The word to convert. Returns: - ConverterResult: The text converted to NATO phonetic alphabet format. - - Raises: - ValueError: If the input type is not supported (only "text" is supported). + str: The converted word, with code words separated by spaces. """ - if not self.input_supported(input_type): - raise ValueError("Input type not supported") - - nato_text = self._convert_to_nato(prompt) - - return ConverterResult(output_text=nato_text, output_type="text") + output = [self._NATO_MAP.get(char.upper(), char) if char.isascii() else char for char in word] + return " ".join(output) - def _convert_to_nato(self, text: str) -> str: + def join_words(self, words: list[str]) -> str: """ - Convert text to NATO phonetic alphabet representation. + Join converted words while preserving every source-space boundary. Args: - text (str): The text to convert. + words (list[str]): The converted words. Returns: - str: The NATO phonetic alphabet representation, with code words separated by spaces. + str: The converted words separated by explicit space tokens. """ - output = [self._NATO_MAP.get(char, char) for char in text.upper() if char != " "] + output: list[str] = [] + for index, word in enumerate(words): + if index: + output.append(self._WORD_SEPARATOR) + if word: + output.append(word) return " ".join(output) diff --git a/tests/unit/converter/test_nato_converter.py b/tests/unit/converter/test_nato_converter.py index a2ba2f8e2a..72f43dd50c 100644 --- a/tests/unit/converter/test_nato_converter.py +++ b/tests/unit/converter/test_nato_converter.py @@ -4,6 +4,7 @@ import pytest from pyrit.converter import ConverterResult, NatoConverter +from pyrit.converter.text_selection_strategy import WordIndexSelectionStrategy async def test_nato_converter_simple_text(): @@ -56,7 +57,7 @@ async def test_nato_converter_with_numbers(): async def test_nato_converter_with_spaces(): - """Test that spaces are ignored in NATO conversion.""" + """Test that word boundaries remain distinct from code-word separators.""" converter = NatoConverter() prompt = "a b c" @@ -64,8 +65,7 @@ async def test_nato_converter_with_spaces(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Spaces should be ignored - assert result.output_text == "Alfa Bravo Charlie" + assert result.output_text == "Alfa Bravo Charlie" async def test_nato_converter_with_punctuation(): @@ -78,7 +78,7 @@ async def test_nato_converter_with_punctuation(): assert isinstance(result, ConverterResult) assert result.output_type == "text" # Punctuation is preserved as-is so the encoded prompt keeps its full content - assert result.output_text == "Hotel Echo Lima Lima Oscar , Whiskey Oscar Romeo Lima Delta !" + assert result.output_text == "Hotel Echo Lima Lima Oscar , Whiskey Oscar Romeo Lima Delta !" async def test_nato_converter_empty_string(): @@ -109,6 +109,34 @@ async def test_nato_converter_no_letters(): assert result.output_text == "1 2 3 ! @ #" +async def test_nato_converter_space_only_prompt(): + """Test that a non-empty whitespace prompt remains non-empty.""" + converter = NatoConverter() + + result = await converter.convert_async(prompt=" ", input_type="text") + + assert result.output_text == " " + + +@pytest.mark.parametrize("prompt", ["é", "ß", "ı", "ñ"]) +async def test_nato_converter_preserves_unmapped_unicode(prompt: str): + """Test that Unicode characters are preserved without case conversion.""" + converter = NatoConverter() + + result = await converter.convert_async(prompt=prompt, input_type="text") + + assert result.output_text == prompt + + +async def test_nato_converter_word_selection_strategy(): + """Test that NATO conversion supports the shared word selection strategies.""" + converter = NatoConverter(word_selection_strategy=WordIndexSelectionStrategy(indices=[1])) + + result = await converter.convert_async(prompt="abc def", input_type="text") + + assert result.output_text == "abc Delta Echo Foxtrot" + + async def test_nato_converter_all_letters(): """Test NATO conversion with all letters of the alphabet.""" converter = NatoConverter() @@ -130,7 +158,7 @@ async def test_nato_converter_input_type_not_supported(): """Test that non-text input types raise ValueError.""" converter = NatoConverter() - with pytest.raises(ValueError, match="Input type not supported"): + with pytest.raises(ValueError, match="Input type image_path not supported"): await converter.convert_async(prompt="test", input_type="image_path")