diff --git a/pyrit/backend/services/converter_service.py b/pyrit/backend/services/converter_service.py index 7ec6aec799..13e75c7152 100644 --- a/pyrit/backend/services/converter_service.py +++ b/pyrit/backend/services/converter_service.py @@ -196,20 +196,23 @@ async def preview_conversion_async(self, *, request: ConverterPreviewRequest) -> ) await serializer.save_b64_image_async(data=value) original_value = str(serializer.value) - # Already an existing file on disk — keep as-is - elif Path(original_value).is_file(): - pass else: - # Treat as raw base64 - ext = DEFAULT_MEDIA_EXTENSIONS.get(str(data_type), ".bin") - - serializer = data_serializer_factory( - category="prompt-memory-entries", - data_type=data_type, - extension=ext, - ) - await serializer.save_b64_image_async(data=original_value) - original_value = str(serializer.value) + try: + is_existing_file = Path(original_value).is_file() + except (OSError, ValueError): + is_existing_file = False + + if not is_existing_file: + # Treat as raw base64 + ext = DEFAULT_MEDIA_EXTENSIONS.get(str(data_type), ".bin") + + serializer = data_serializer_factory( + category="prompt-memory-entries", + data_type=data_type, + extension=ext, + ) + await serializer.save_b64_image_async(data=original_value) + original_value = str(serializer.value) converters = self._gather_converters(converter_ids=request.converter_ids) steps, final_value, final_type = await self._apply_converters_async( diff --git a/tests/unit/backend/test_converter_service.py b/tests/unit/backend/test_converter_service.py index 8a0d4ed03e..ceab484216 100644 --- a/tests/unit/backend/test_converter_service.py +++ b/tests/unit/backend/test_converter_service.py @@ -441,7 +441,7 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No mock_serializer.value = "/tmp/persisted.wav" mock_serializer.save_b64_image_async = AsyncMock() - raw_b64 = "UklGRiQAAABXQVZF" + raw_b64 = "UklGRiQ" + "A" * 5000 request = ConverterPreviewRequest( original_value=raw_b64, original_value_data_type="audio_path", @@ -452,13 +452,77 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No "pyrit.backend.services.converter_service.data_serializer_factory", return_value=mock_serializer, ) as mock_factory: - await service.preview_conversion_async(request=request) + result = await service.preview_conversion_async(request=request) mock_factory.assert_called_once() # ext is the audio_path mapping from DEFAULT_MEDIA_EXTENSIONS assert mock_factory.call_args.kwargs["extension"] == ".wav" assert mock_factory.call_args.kwargs["data_type"] == "audio_path" mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) + assert result.model_dump(mode="json")["original_value"] == raw_b64 + + async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails(self) -> None: + """Filesystem inspection failures classify the value as raw base64.""" + service = ConverterService() + mock_serializer = MagicMock() + mock_serializer.value = "/tmp/persisted.wav" + mock_serializer.save_b64_image_async = AsyncMock() + raw_b64 = "UklGRiQAAABXQVZF" + request = ConverterPreviewRequest( + original_value=raw_b64, + original_value_data_type="audio_path", + converter_ids=[], + ) + + with ( + patch.object(Path, "is_file", side_effect=OSError("path inspection failed")), + patch( + "pyrit.backend.services.converter_service.data_serializer_factory", + return_value=mock_serializer, + ), + ): + result = await service.preview_conversion_async(request=request) + + mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) + assert result.converted_value == "/tmp/persisted.wav" + + async def test_preview_conversion_propagates_invalid_base64_error_after_path_failure(self) -> None: + """Errors from raw base64 persistence are not mistaken for path inspection failures.""" + service = ConverterService() + mock_serializer = MagicMock() + mock_serializer.save_b64_image_async = AsyncMock(side_effect=ValueError("invalid base64")) + request = ConverterPreviewRequest( + original_value="A", + original_value_data_type="audio_path", + converter_ids=[], + ) + + with ( + patch.object(Path, "is_file", side_effect=ValueError("invalid path")), + patch( + "pyrit.backend.services.converter_service.data_serializer_factory", + return_value=mock_serializer, + ), + pytest.raises(ValueError, match="invalid base64"), + ): + await service.preview_conversion_async(request=request) + + async def test_preview_conversion_preserves_existing_file(self, tmp_path: Path) -> None: + """Existing local media paths pass through without being persisted again.""" + service = ConverterService() + media_path = tmp_path / "input.wav" + media_path.write_bytes(b"RIFF") + request = ConverterPreviewRequest( + original_value=str(media_path), + original_value_data_type="audio_path", + converter_ids=[], + ) + + with patch("pyrit.backend.services.converter_service.data_serializer_factory") as mock_factory: + result = await service.preview_conversion_async(request=request) + + mock_factory.assert_not_called() + assert result.converted_value == str(media_path) class TestGetConverterObjectsForIds: