Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion tests/tools/test_transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import wave
from pathlib import Path
from unittest.mock import MagicMock, call, patch
from types import SimpleNamespace


import pytest

Expand Down Expand Up @@ -206,6 +208,31 @@ def test_null_groq_subsection_is_safe(self, monkeypatch, sample_wav):
# _transcribe_openai — additional tests
# ============================================================================

def test_openai_transcription_error_object_is_not_returned_as_text(
monkeypatch,
sample_wav,
):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
mock_client = MagicMock()
mock_client.audio.transcriptions.create.return_value = SimpleNamespace(
text=None,
error="Transcription failed",
)

with (
patch("tools.transcription_tools._HAS_OPENAI", True),
patch("openai.OpenAI", return_value=mock_client),
):
from tools.transcription_tools import _transcribe_openai

result = _transcribe_openai(sample_wav, "gpt-4o-transcribe")

assert result["success"] is False
assert result["transcript"] == ""
assert "Transcription failed" in result["error"]
assert "text=None" not in result["error"]


class TestTranscribeLocalCommand:
def test_command_provider_uses_sanitized_child_env(self, monkeypatch):
"""Salvage of #56332: command STT must not inherit Hermes secrets."""
Expand Down Expand Up @@ -928,6 +955,19 @@ def test_keeps_non_envelope_marker_literal(self):

assert result == "The user literally said <asr_text> while reading markup."

def test_rejects_structured_error_instead_of_stringifying_repr(self):
from tools.transcription_tools import _extract_transcript_text

transcription = SimpleNamespace(
text=None,
logprobs=None,
usage=None,
error="Transcription failed",
)

with pytest.raises(ValueError, match="Transcription failed"):
_extract_transcript_text(transcription)


# Shell safety — shlex.split on auto-detected templates
# ============================================================================
Expand Down Expand Up @@ -1159,7 +1199,7 @@ def test_transcribe_audio_blocks_credential_read(self, tmp_path):
from agent.file_safety import get_read_block_error

env_file = tmp_path / ".env"
env_file.write_text("OPENAI_API_KEY=sk-secret\n")
env_file.write_text("OPENAI_API_KEY=sk-secret\n", encoding="utf-8")

expected = get_read_block_error(str(env_file))
assert expected, "test setup: a .env file should be read-blocked"
Expand Down
13 changes: 13 additions & 0 deletions tools/transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2988,20 +2988,33 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
def _extract_transcript_text(transcription: Any) -> str:
"""Normalize text and JSON transcription responses to a plain string."""
text: Optional[str] = None
structured_response = False

if isinstance(transcription, str):
text = transcription.strip()

if text is None and hasattr(transcription, "text"):
structured_response = True
value = getattr(transcription, "text")
if isinstance(value, str):
text = value.strip()

if text is None and isinstance(transcription, dict):
structured_response = True
value = transcription.get("text")
if isinstance(value, str):
text = value.strip()

if text is None and structured_response:
error = (
transcription.get("error")
if isinstance(transcription, dict)
else getattr(transcription, "error", None)
)
if error:
raise ValueError(str(error))
return ""

if text is None:
text = str(transcription).strip()

Expand Down