From 08c5139d0106ce0d8bc9b2d4d05f4a783dda3b0c Mon Sep 17 00:00:00 2001 From: Rohith Pariki Date: Tue, 13 Oct 2026 06:43:54 +0530 Subject: [PATCH 1/2] FIX: Fix CodeChameleonConverter decryption functions in binary_tree and length modes (fixes #3164) --- pyrit/converter/codechameleon_converter.py | 8 +++-- .../test_code_chameleon_converter.py | 32 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pyrit/converter/codechameleon_converter.py b/pyrit/converter/codechameleon_converter.py index 6259b56db2..7ed8132747 100644 --- a/pyrit/converter/codechameleon_converter.py +++ b/pyrit/converter/codechameleon_converter.py @@ -272,8 +272,8 @@ def helper(node_dict): if node_dict is None: return None node = TreeNode(node_dict['value']) - node.left = helper(json.loads(node_dict['left'])) if node_dict['left'] else None - node.right = helper(json.loads(node_dict['right'])) if node_dict['right'] else None + node.left = helper(node_dict['left']) if node_dict['left'] else None + node.right = helper(node_dict['right']) if node_dict['right'] else None return node return helper(json.loads(problem_description)) @@ -324,7 +324,11 @@ def decryption(encrypted_problem): """ The following is the decryption function. ```python + import json + def decryption(encrypted_problem): + if isinstance(encrypted_problem, str): + encrypted_problem = json.loads(encrypted_problem) # Create an empty sentence sentence = [''] * len(encrypted_problem) # Iterate through the list and place each word at the correct position diff --git a/tests/unit/converter/test_code_chameleon_converter.py b/tests/unit/converter/test_code_chameleon_converter.py index b645f7a3d6..792a1ae6b1 100644 --- a/tests/unit/converter/test_code_chameleon_converter.py +++ b/tests/unit/converter/test_code_chameleon_converter.py @@ -3,6 +3,8 @@ import re +import pytest + from pyrit.converter import CodeChameleonConverter @@ -74,8 +76,8 @@ def helper(node_dict): if node_dict is None: return None node = TreeNode(node_dict['value']) - node.left = helper(json.loads(node_dict['left'])) if node_dict['left'] else None - node.right = helper(json.loads(node_dict['right'])) if node_dict['right'] else None + node.left = helper(node_dict['left']) if node_dict['left'] else None + node.right = helper(node_dict['right']) if node_dict['right'] else None return node return helper(json.loads(problem_description)) @@ -143,7 +145,11 @@ async def test_codechameleon_converter_length_encrypt_decrypt() -> None: """ The following is the decryption function. ```python +import json + def decryption(encrypted_problem): + if isinstance(encrypted_problem, str): + encrypted_problem = json.loads(encrypted_problem) # Create an empty sentence sentence = [''] * len(encrypted_problem) # Iterate through the list and place each word at the correct position @@ -164,3 +170,25 @@ def test_codechameleon_converter_input_supported() -> None: converter = CodeChameleonConverter(encrypt_type="length") assert converter.input_supported("text") is True assert converter.input_supported("image_path") is False + + +@pytest.mark.parametrize("encrypt_type", ["reverse", "odd_even", "binary_tree", "length"]) +async def test_codechameleon_converter_decryption_execution(encrypt_type: str) -> None: + prompt = "How to cut down a tree?" + converter = CodeChameleonConverter(encrypt_type=encrypt_type) + output = await converter.convert_async(prompt=prompt, input_type="text") + + code_match = re.search( + r"The following is the decryption function\.\s*```python\n(.*?)\n```", output.output_text, re.DOTALL + ) + assert code_match is not None + code = code_match.group(1) + + problem_match = re.search(r"The encrypted_problem you need to solve is: (.*?)\n", output.output_text) + assert problem_match is not None + encrypted_problem = problem_match.group(1) + + local_vars: dict = {} + exec(code, local_vars) + decrypted = local_vars["decryption"](encrypted_problem) + assert decrypted == prompt From 625fe254504251683b3cad7e1bf829a5bc2a419c Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Mon, 12 Oct 2026 21:40:57 -0700 Subject: [PATCH 2/2] TEST: strengthen CodeChameleon decryption round-trip coverage Cover boundary inputs across all built-in modes and preserve length decoding of already parsed lists. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dcbc75-65c2-470f-8978-3b7f95ae03a7 --- .../test_code_chameleon_converter.py | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/tests/unit/converter/test_code_chameleon_converter.py b/tests/unit/converter/test_code_chameleon_converter.py index 792a1ae6b1..c4b01a4a13 100644 --- a/tests/unit/converter/test_code_chameleon_converter.py +++ b/tests/unit/converter/test_code_chameleon_converter.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import json import re import pytest @@ -173,10 +174,33 @@ def test_codechameleon_converter_input_supported() -> None: @pytest.mark.parametrize("encrypt_type", ["reverse", "odd_even", "binary_tree", "length"]) -async def test_codechameleon_converter_decryption_execution(encrypt_type: str) -> None: - prompt = "How to cut down a tree?" +@pytest.mark.parametrize( + ("prompt", "expected_prompt"), + [ + pytest.param("How to cut down a tree?", "How to cut down a tree?", id="original"), + pytest.param("", "", id="empty"), + pytest.param(" \t\n ", "", id="whitespace-only"), + pytest.param("one", "one", id="single-node"), + pytest.param("one two", "one two", id="right-subtree"), + pytest.param("one two three", "one two three", id="both-subtrees"), + pytest.param("one two three four five six", "one two three four five six", id="nested-even"), + pytest.param("one two three four five six seven", "one two three four five six seven", id="nested-odd"), + pytest.param("\tone\n two\r\nthree ", "one two three", id="normalized-whitespace"), + pytest.param( + 'say "hello" then {check} [brackets] back\\slash', + 'say "hello" then {check} [brackets] back\\slash', + id="escaped-values", + ), + pytest.param("one two one two one", "one two one two one", id="repeated-words"), + pytest.param("caf\u00e9 \u4f60\u597d \U0001f642", "caf\u00e9 \u4f60\u597d \U0001f642", id="unicode"), + ], +) +async def test_codechameleon_converter_decryption_execution_async( + *, encrypt_type: str, prompt: str, expected_prompt: str +) -> None: converter = CodeChameleonConverter(encrypt_type=encrypt_type) output = await converter.convert_async(prompt=prompt, input_type="text") + assert output.output_type == "text" code_match = re.search( r"The following is the decryption function\.\s*```python\n(.*?)\n```", output.output_text, re.DOTALL @@ -184,11 +208,19 @@ async def test_codechameleon_converter_decryption_execution(encrypt_type: str) - assert code_match is not None code = code_match.group(1) - problem_match = re.search(r"The encrypted_problem you need to solve is: (.*?)\n", output.output_text) + problem_match = re.search( + r"The encrypted_problem you need to solve is: (.*?)\nShow the finished ProblemSolver", + output.output_text, + re.DOTALL, + ) assert problem_match is not None encrypted_problem = problem_match.group(1) - local_vars: dict = {} - exec(code, local_vars) - decrypted = local_vars["decryption"](encrypted_problem) - assert decrypted == prompt + namespace: dict[str, object] = {} + exec(code, namespace) + decryption = namespace["decryption"] + assert callable(decryption) + expected_prompt = prompt if encrypt_type == "reverse" else expected_prompt + assert decryption(encrypted_problem) == expected_prompt + if encrypt_type == "length": + assert decryption(json.loads(encrypted_problem)) == expected_prompt