hermes-agent/tests/tools/test_transcription_tools.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

1232 lines
50 KiB
Python

"""Tests for tools.transcription_tools — three-provider STT pipeline.
Covers the full provider matrix (local, groq, openai), fallback chains,
model auto-correction, config loading, validation edge cases, and
end-to-end dispatch. All external dependencies are mocked.
"""
import os
import sys
import struct
import subprocess
import types
import wave
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
if "faster_whisper" not in sys.modules:
faster_whisper_stub = types.ModuleType("faster_whisper")
faster_whisper_stub.WhisperModel = MagicMock(name="WhisperModel")
# Set ``__spec__`` so ``importlib.util.find_spec("faster_whisper")``
# doesn't raise ``ValueError: faster_whisper.__spec__ is None`` during
# collection (used by skipif markers further down in this file).
from importlib.machinery import ModuleSpec
faster_whisper_stub.__spec__ = ModuleSpec("faster_whisper", loader=None)
sys.modules["faster_whisper"] = faster_whisper_stub
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def sample_wav(tmp_path):
"""Create a minimal valid WAV file (1 second of silence at 16kHz)."""
wav_path = tmp_path / "test.wav"
n_frames = 16000
silence = struct.pack(f"<{n_frames}h", *([0] * n_frames))
with wave.open(str(wav_path), "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(16000)
wf.writeframes(silence)
return str(wav_path)
@pytest.fixture
def sample_ogg(tmp_path):
"""Create a fake OGG file for validation tests."""
ogg_path = tmp_path / "test.ogg"
ogg_path.write_bytes(b"fake audio data")
return str(ogg_path)
@pytest.fixture
def sample_silk(tmp_path):
"""Create a fake WeChat .silk file for preprocessing tests."""
silk_path = tmp_path / "voice.silk"
silk_path.write_bytes(b"\x02#!SILK_V3fake")
return str(silk_path)
@pytest.fixture
def oversized_wav(tmp_path):
"""Create a sparse WAV-shaped file just above the remote upload cap."""
from tools.transcription_tools import MAX_FILE_SIZE
wav_path = tmp_path / "oversized.wav"
with wav_path.open("wb") as audio_file:
audio_file.seek(MAX_FILE_SIZE)
audio_file.write(b"\0")
return str(wav_path)
pytestmark = pytest.mark.usefixtures("disable_lazy_stt_install")
@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
"""Ensure no real API keys leak into tests."""
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.delenv("ELEVENLABS_API_KEY", raising=False)
monkeypatch.delenv("HERMES_LOCAL_STT_COMMAND", raising=False)
monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False)
# ============================================================================
# _get_provider — full permutation matrix
# ============================================================================
class TestGetProviderGroq:
"""Groq-specific provider selection tests."""
def test_groq_when_key_set(self, monkeypatch):
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
with patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("tools.transcription_tools._HAS_FASTER_WHISPER", False):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "groq"}) == "groq"
class TestGetProviderFallbackPriority:
"""Auto-detect fallback priority and explicit provider behaviour."""
def test_auto_detect_prefers_local(self):
"""Auto-detect prefers local over any cloud provider."""
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "local"
def test_unknown_provider_passed_through(self):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "custom-endpoint"}) == "custom-endpoint"
# ============================================================================
# Explicit provider config respected (GH-1774)
# ============================================================================
class TestExplicitProviderRespected:
"""When stt.provider is explicitly set, that choice is authoritative.
No silent fallback to a different cloud provider."""
def test_explicit_local_no_fallback_to_openai(self, monkeypatch):
"""GH-1774: provider=local must not silently fall back to openai
even when an OpenAI API key is set."""
monkeypatch.setenv("OPENAI_API_KEY", "***")
monkeypatch.delenv("GROQ_API_KEY", raising=False)
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", True):
from tools.transcription_tools import _get_provider
result = _get_provider({"provider": "local"})
assert result == "none", f"Expected 'none' but got {result!r}"
def test_explicit_local_uses_local_command_fallback(self, monkeypatch):
"""Local-to-local_command fallback is fine — both are local."""
monkeypatch.setenv(
"HERMES_LOCAL_STT_COMMAND",
"whisper {input_path} --output_dir {output_dir} --language {language}",
)
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False):
from tools.transcription_tools import _get_provider
result = _get_provider({"provider": "local"})
assert result == "local_command"
def test_auto_detect_prefers_groq_over_openai(self, monkeypatch):
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
monkeypatch.setenv("OPENAI_API_KEY", "sk-real-key")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", True):
from tools.transcription_tools import _get_provider
result = _get_provider({})
assert result == "groq"
# ============================================================================
# _transcribe_groq
# ============================================================================
class TestTranscribeGroq:
def test_no_key(self, monkeypatch):
monkeypatch.delenv("GROQ_API_KEY", raising=False)
from tools.transcription_tools import _transcribe_groq
result = _transcribe_groq("/tmp/test.ogg", "whisper-large-v3-turbo")
assert result["success"] is False
assert "GROQ_API_KEY" in result["error"]
def test_openai_package_not_installed(self, monkeypatch):
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
with patch("tools.transcription_tools._HAS_OPENAI", False):
from tools.transcription_tools import _transcribe_groq
result = _transcribe_groq("/tmp/test.ogg", "whisper-large-v3-turbo")
assert result["success"] is False
assert "openai package" in result["error"]
def test_null_groq_subsection_is_safe(self, monkeypatch, sample_wav):
"""`stt.groq: null` in YAML yields None; must not raise, auto-detect stays intact."""
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False)
mock_client = MagicMock()
mock_client.audio.transcriptions.create.return_value = "hi"
with patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("openai.OpenAI", return_value=mock_client), \
patch(
"tools.transcription_tools._load_stt_config",
return_value={"groq": None},
):
from tools.transcription_tools import _transcribe_groq
result = _transcribe_groq(sample_wav, "whisper-large-v3-turbo")
assert result["success"] is True
kwargs = mock_client.audio.transcriptions.create.call_args.kwargs
assert "language" not in kwargs
# ============================================================================
# _transcribe_openai — additional tests
# ============================================================================
class TestTranscribeLocalCommand:
def test_command_provider_uses_sanitized_child_env(self, monkeypatch):
"""Salvage of #56332: command STT must not inherit Hermes secrets."""
monkeypatch.setenv("AUXILIARY_VISION_API_KEY", "sk-vision")
monkeypatch.setenv("GATEWAY_RELAY_SECRET", "relay-secret")
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai")
monkeypatch.setenv("MY_SAFE_STT_VAR", "keep")
captured = {}
class _Stream:
def read(self, size):
return ""
class Proc:
returncode = 0
stdout = _Stream()
stderr = _Stream()
def wait(self, timeout=None):
return 0
def fake_popen(command, **kwargs):
captured["env"] = kwargs["env"]
return Proc()
monkeypatch.setattr("tools.transcription_tools.subprocess.Popen", fake_popen)
from tools.transcription_tools import _run_command_stt
result = _run_command_stt("echo hi", timeout=1)
assert result.returncode == 0
env = captured["env"]
assert "AUXILIARY_VISION_API_KEY" not in env
assert "GATEWAY_RELAY_SECRET" not in env
assert "OPENAI_API_KEY" not in env
assert env["MY_SAFE_STT_VAR"] == "keep"
def test_local_whisper_subprocess_uses_sanitized_env(
self, monkeypatch, sample_wav, tmp_path
):
"""Sibling path: local whisper subprocess.run also scrubbed (#56332 gap)."""
monkeypatch.setenv("AUXILIARY_VISION_API_KEY", "sk-vision")
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai")
monkeypatch.setenv("MY_SAFE_LOCAL_STT", "keep")
monkeypatch.setenv(
"HERMES_LOCAL_STT_COMMAND",
"whisper {input_path} --model {model} --output_dir {output_dir} --language {language}",
)
captured = {}
out_dir = tmp_path / "local-out"
out_dir.mkdir()
(out_dir / "transcript.txt").write_text("hello", encoding="utf-8")
def fake_tempdir(prefix=None):
class _TempDir:
def __enter__(self_inner):
return str(out_dir)
def __exit__(self_inner, *exc):
return False
return _TempDir()
def fake_run(*args, **kwargs):
captured["env"] = kwargs.get("env")
class R:
returncode = 0
return R()
monkeypatch.setattr("tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir)
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
monkeypatch.setattr(
"tools.transcription_tools._prepare_local_audio",
lambda *a, **k: (str(sample_wav), None),
)
from tools.transcription_tools import _transcribe_local_command
result = _transcribe_local_command(str(sample_wav), "base")
assert result["success"] is True
env = captured["env"]
assert env is not None
assert "AUXILIARY_VISION_API_KEY" not in env
assert "OPENAI_API_KEY" not in env
assert env["MY_SAFE_LOCAL_STT"] == "keep"
def test_command_fallback_with_template(self, monkeypatch, sample_ogg, tmp_path):
out_dir = tmp_path / "local-out"
out_dir.mkdir()
monkeypatch.setenv(
"HERMES_LOCAL_STT_COMMAND",
"whisper {input_path} --model {model} --output_dir {output_dir} --language {language}",
)
monkeypatch.setenv("HERMES_LOCAL_STT_LANGUAGE", "en")
def fake_tempdir(prefix=None):
class _TempDir:
def __enter__(self_inner):
return str(out_dir)
def __exit__(self_inner, exc_type, exc, tb):
return False
return _TempDir()
def fake_run(cmd, *args, **kwargs):
assert isinstance(cmd, list)
(out_dir / "test.txt").write_text("hello from local command\n", encoding="utf-8")
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
monkeypatch.setattr("tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir)
monkeypatch.setattr("tools.transcription_tools._find_ffmpeg_binary", lambda: "/opt/homebrew/bin/ffmpeg")
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
from tools.transcription_tools import _transcribe_local_command
result = _transcribe_local_command(sample_ogg, "base")
assert result["success"] is True
assert result["transcript"] == "hello from local command"
assert result["provider"] == "local_command"
# ============================================================================
# _transcribe_local — additional tests
# ============================================================================
@pytest.mark.skipif(
not __import__("importlib").util.find_spec("faster_whisper"),
reason="faster_whisper not installed",
)
class TestTranscribeLocalExtended:
def test_model_reuse_on_second_call(self, tmp_path):
"""Second call with same model should NOT reload the model."""
audio = tmp_path / "test.ogg"
audio.write_bytes(b"fake")
mock_segment = MagicMock()
mock_segment.text = "hi"
mock_info = MagicMock()
mock_info.language = "en"
mock_info.duration = 1.0
mock_model = MagicMock()
mock_model.transcribe.return_value = ([mock_segment], mock_info)
mock_whisper_cls = MagicMock(return_value=mock_model)
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
patch("faster_whisper.WhisperModel", mock_whisper_cls), \
patch("tools.transcription_tools._local_model", None), \
patch("tools.transcription_tools._local_model_name", None):
from tools.transcription_tools import _transcribe_local
_transcribe_local(str(audio), "base")
_transcribe_local(str(audio), "base")
# WhisperModel should be created only once
assert mock_whisper_cls.call_count == 1
def test_config_device_and_compute_type_passed_to_whisper(self, tmp_path):
"""User-configured device and compute_type should be forwarded to WhisperModel.
Regression test for #8319: these values were hardcoded to "auto".
"""
audio = tmp_path / "test.ogg"
audio.write_bytes(b"fake")
mock_segment = MagicMock()
mock_segment.text = "hi"
mock_info = MagicMock()
mock_info.language = "en"
mock_info.duration = 1.0
mock_model = MagicMock()
mock_model.transcribe.return_value = ([mock_segment], mock_info)
mock_whisper_cls = MagicMock(return_value=mock_model)
fake_config = {
"local": {
"device": "cpu",
"compute_type": "float32",
}
}
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
patch("faster_whisper.WhisperModel", mock_whisper_cls), \
patch("tools.transcription_tools._local_model", None), \
patch("tools.transcription_tools._local_model_name", None), \
patch("tools.transcription_tools._load_stt_config", return_value=fake_config):
from tools.transcription_tools import _transcribe_local
result = _transcribe_local(str(audio), "base")
assert result["success"] is True
mock_whisper_cls.assert_called_once_with("base", device="cpu", compute_type="float32")
def test_cuda_out_of_memory_does_not_trigger_cpu_fallback(self, tmp_path):
"""'CUDA out of memory' is a real error, not a missing lib — surface it."""
audio = tmp_path / "test.ogg"
audio.write_bytes(b"fake")
mock_whisper_cls = MagicMock(side_effect=RuntimeError("CUDA out of memory"))
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
patch("faster_whisper.WhisperModel", mock_whisper_cls), \
patch("tools.transcription_tools._local_model", None), \
patch("tools.transcription_tools._local_model_name", None):
from tools.transcription_tools import _transcribe_local
result = _transcribe_local(str(audio), "base")
# Single call — no CPU retry, because OOM isn't a missing-lib symptom.
assert mock_whisper_cls.call_count == 1
assert result["success"] is False
assert "CUDA out of memory" in result["error"]
# ============================================================================
# Model auto-correction
# ============================================================================
class TestModelAutoCorrection:
def test_groq_corrects_openai_model(self, monkeypatch, sample_wav):
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
mock_client = MagicMock()
mock_client.audio.transcriptions.create.return_value = "hello world"
with patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("openai.OpenAI", return_value=mock_client):
from tools.transcription_tools import _transcribe_groq, DEFAULT_GROQ_STT_MODEL
_transcribe_groq(sample_wav, "whisper-1")
call_kwargs = mock_client.audio.transcriptions.create.call_args
assert call_kwargs.kwargs["model"] == DEFAULT_GROQ_STT_MODEL
def test_unknown_model_passes_through_groq(self, monkeypatch, sample_wav):
"""A model not in either known set should not be overridden."""
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
mock_client = MagicMock()
mock_client.audio.transcriptions.create.return_value = "test"
with patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("openai.OpenAI", return_value=mock_client):
from tools.transcription_tools import _transcribe_groq
_transcribe_groq(sample_wav, "my-custom-model")
call_kwargs = mock_client.audio.transcriptions.create.call_args
assert call_kwargs.kwargs["model"] == "my-custom-model"
# ============================================================================
# _validate_audio_file — edge cases
# ============================================================================
class TestValidateAudioFileEdgeCases:
def test_directory_is_not_a_file(self, tmp_path):
from tools.transcription_tools import _validate_audio_file
# tmp_path itself is a directory with an .ogg-ish name? No.
# Create a directory with a valid audio extension
d = tmp_path / "audio.ogg"
d.mkdir()
result = _validate_audio_file(str(d))
assert result is not None
assert "not a file" in result["error"]
def test_symlink_with_supported_extension_is_rejected(self, tmp_path):
if not hasattr(os, "symlink"):
pytest.skip("symlinks are not supported on this platform")
target = tmp_path / "target.txt"
target.write_bytes(b"not audio")
link = tmp_path / "linked.wav"
try:
os.symlink(target, link)
except (OSError, NotImplementedError) as exc:
pytest.skip(f"symlink creation unavailable: {exc}")
from tools.transcription_tools import _validate_audio_file
result = _validate_audio_file(str(link))
assert result is not None
assert "symbolic link" in result["error"]
def test_all_supported_formats_accepted(self, tmp_path):
from tools.transcription_tools import _validate_audio_file, SUPPORTED_FORMATS
for fmt in SUPPORTED_FORMATS:
f = tmp_path / f"test{fmt}"
f.write_bytes(b"data")
assert _validate_audio_file(str(f)) is None, f"Format {fmt} should be accepted"
# ============================================================================
# transcribe_audio — end-to-end dispatch
# ============================================================================
class TestTranscribeAudioDispatch:
def test_oversized_local_file_reaches_dispatcher(self, oversized_wav):
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "local"}), \
patch("tools.transcription_tools._get_provider", return_value="local"), \
patch("tools.transcription_tools._transcribe_local",
return_value={"success": True, "transcript": "hi"}) as mock_local:
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(oversized_wav)
assert result["success"] is True
mock_local.assert_called_once()
def test_no_provider_returns_error(self, sample_ogg):
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("tools.transcription_tools._get_provider", return_value="none"):
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(sample_ogg)
assert result["success"] is False
assert "No STT provider" in result["error"]
assert "faster-whisper" in result["error"]
assert "GROQ_API_KEY" in result["error"]
def test_silk_symlink_is_rejected_before_preprocessing(self, tmp_path):
"""A Silk symlink must not reach the decoder before path safety validation."""
if not hasattr(os, "symlink"):
pytest.skip("symlinks are not supported on this platform")
target = tmp_path / "voice.silk"
target.write_bytes(b"\x02#!SILK_V3fake")
link = tmp_path / "linked.silk"
try:
os.symlink(target, link)
except (OSError, NotImplementedError) as exc:
pytest.skip(f"symlink creation unavailable: {exc}")
with patch(
"tools.transcription_tools._prepare_audio_for_transcription", create=True
) as mock_prepare:
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(str(link))
assert result["success"] is False
assert "symbolic link" in result["error"]
mock_prepare.assert_not_called()
def test_config_local_model_used(self, sample_ogg):
config = {"local": {"model": "small"}}
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
patch("tools.transcription_tools._get_provider", return_value="local"), \
patch("tools.transcription_tools._transcribe_local",
return_value={"success": True, "transcript": "hi"}) as mock_local:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model=None)
assert mock_local.call_args[0][1] == "small"
# ============================================================================
# _transcribe_mistral
# ============================================================================
@pytest.fixture
def mock_mistral_module():
"""Inject a fake mistralai module into sys.modules for testing."""
mock_client = MagicMock()
mock_client.__enter__ = MagicMock(return_value=mock_client)
mock_client.__exit__ = MagicMock(return_value=False)
mock_mistral_cls = MagicMock(return_value=mock_client)
fake_module = MagicMock()
fake_module.Mistral = mock_mistral_cls
with patch.dict("sys.modules", {"mistralai": fake_module, "mistralai.client": fake_module}):
yield mock_client
class TestTranscribeMistral:
def test_successful_transcription(self, monkeypatch, sample_ogg, mock_mistral_module):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
mock_result = MagicMock()
mock_result.text = "hello from mistral"
mock_mistral_module.audio.transcriptions.complete.return_value = mock_result
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral(sample_ogg, "voxtral-mini-latest")
assert result["success"] is True
assert result["transcript"] == "hello from mistral"
assert result["provider"] == "mistral"
mock_mistral_module.audio.transcriptions.complete.assert_called_once()
mock_mistral_module.__exit__.assert_called_once()
def test_api_error_returns_failure(self, monkeypatch, sample_ogg, mock_mistral_module):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
mock_mistral_module.audio.transcriptions.complete.side_effect = RuntimeError("secret-key-leaked")
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral(sample_ogg, "voxtral-mini-latest")
assert result["success"] is False
assert "RuntimeError" in result["error"]
assert "secret-key-leaked" not in result["error"]
# ============================================================================
# _get_provider — Mistral
# ============================================================================
class TestGetProviderMistral:
"""Mistral-specific provider selection tests."""
def test_mistral_explicit_no_sdk_returns_none(self, monkeypatch):
"""Explicit mistral with key but no SDK returns none."""
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_MISTRAL", False):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "mistral"}) == "none"
def test_auto_detect_mistral_after_openai(self, monkeypatch):
"""Auto-detect: mistral is tried after openai when both are unavailable."""
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "mistral"
# ============================================================================
# transcribe_audio — Mistral dispatch
# ============================================================================
class TestTranscribeAudioMistralDispatch:
def test_config_mistral_model_used(self, sample_ogg):
config = {"provider": "mistral", "mistral": {"model": "voxtral-mini-2602"}}
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
patch("tools.transcription_tools._get_provider", return_value="mistral"), \
patch("tools.transcription_tools._transcribe_mistral",
return_value={"success": True, "transcript": "hi"}) as mock_mistral:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model=None)
assert mock_mistral.call_args[0][1] == "voxtral-mini-2602"
# ============================================================================
# _transcribe_xai
# ============================================================================
@pytest.fixture
def mock_xai_http_module():
"""Inject a fake tools.xai_http module for testing."""
fake_module = MagicMock()
fake_module.hermes_xai_user_agent = MagicMock(return_value="hermes-xai/test")
with patch.dict("sys.modules", {"tools.xai_http": fake_module}):
yield fake_module
class TestTranscribeXAI:
def test_successful_transcription(self, monkeypatch, sample_ogg, mock_xai_http_module):
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"text": "bonjour le monde",
"language": "fr",
"duration": 3.2,
}
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("requests.post", return_value=mock_response):
from tools.transcription_tools import _transcribe_xai
result = _transcribe_xai(sample_ogg, "grok-stt")
assert result["success"] is True
assert result["transcript"] == "bonjour le monde"
assert result["provider"] == "xai"
@pytest.mark.parametrize("rejected_status", [401])
def test_retries_auth_rejection_with_refreshed_oauth_credentials(
self, sample_ogg, mock_xai_http_module, rejected_status
):
mock_xai_http_module.resolve_xai_http_credentials.side_effect = [
{
"api_key": "stale-oauth-token",
"base_url": "https://api.x.ai/v1",
"provider": "xai-oauth",
},
{
"api_key": "fresh-oauth-token",
"base_url": "https://api.x.ai/v1",
"provider": "xai-oauth",
},
]
rejected = MagicMock()
rejected.status_code = rejected_status
rejected.json.return_value = {
"error": {"message": "OAuth2 access token could not be validated"}
}
accepted = MagicMock()
accepted.status_code = 200
accepted.json.return_value = {
"text": "fleet speech transcription proof",
"language": "en",
"duration": 2.1,
}
stt_config = {"provider": "xai"}
with patch("tools.transcription_tools._load_stt_config", return_value=stt_config), \
patch("tools.transcription_tools._get_provider", return_value="xai"), \
patch("requests.post", side_effect=[rejected, accepted]) as mock_post:
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(sample_ogg)
assert result == {
"success": True,
"transcript": "fleet speech transcription proof",
"provider": "xai",
}
assert mock_post.call_count == 2
assert mock_post.call_args_list[0].kwargs["headers"]["Authorization"] == (
"Bearer stale-oauth-token"
)
assert mock_post.call_args_list[1].kwargs["headers"]["Authorization"] == (
"Bearer fresh-oauth-token"
)
assert mock_xai_http_module.resolve_xai_http_credentials.call_args_list == [
call(),
call(force_refresh=True, api_key_hint="stale-oauth-token"),
]
def test_sends_language_and_format(self, monkeypatch, sample_ogg, mock_xai_http_module):
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
# Explicitly set language via env to exercise the override chain
# (config > env > DEFAULT_LOCAL_STT_LANGUAGE)
monkeypatch.setenv("HERMES_LOCAL_STT_LANGUAGE", "fr")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"text": "test", "language": "fr", "duration": 1.0}
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("requests.post", return_value=mock_response) as mock_post:
from tools.transcription_tools import _transcribe_xai
_transcribe_xai(sample_ogg, "grok-stt")
call_kwargs = mock_post.call_args
data = call_kwargs.kwargs.get("data", call_kwargs[1].get("data", {}))
assert data.get("language") == "fr"
assert data.get("format") == "true"
def test_oauth_credentials_ignore_stt_base_url_override(
self,
monkeypatch,
sample_ogg,
mock_xai_http_module,
):
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.setenv("XAI_STT_BASE_URL", "https://attacker.example/v1")
mock_xai_http_module.resolve_xai_http_credentials.return_value = {
"provider": "xai-oauth",
"api_key": "oauth-bearer-token",
"base_url": "https://api.x.ai/v1",
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"text": "test", "language": "en", "duration": 1.0}
with patch(
"tools.transcription_tools._load_stt_config",
return_value={"xai": {"base_url": "https://attacker.example/config"}},
), patch("requests.post", return_value=mock_response) as mock_post:
from tools.transcription_tools import _transcribe_xai
result = _transcribe_xai(sample_ogg, "grok-stt")
assert result["success"] is True
call_args = mock_post.call_args
url = call_args[0][0] if call_args[0] else call_args.kwargs.get("url", "")
assert url == "https://api.x.ai/v1/stt"
assert call_args.kwargs["headers"]["Authorization"] == "Bearer oauth-bearer-token"
# ============================================================================
# _get_provider — xAI
# ============================================================================
class TestGetProviderXAI:
"""xAI-specific provider selection tests."""
def test_auto_detect_xai_after_mistral(self, monkeypatch):
"""Auto-detect: xai is tried after mistral when all above are unavailable."""
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.setenv("XAI_API_KEY", "xai-test")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", False):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "xai"
# ============================================================================
# transcribe_audio — xAI dispatch
# ============================================================================
class TestTranscribeAudioXAIDispatch:
def test_model_default_is_grok_stt(self, sample_ogg):
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "xai"}), \
patch("tools.transcription_tools._get_provider", return_value="xai"), \
patch("tools.transcription_tools._transcribe_xai",
return_value={"success": True, "transcript": "hi"}) as mock_xai:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model=None)
assert mock_xai.call_args[0][1] == "grok-stt"
# ============================================================================
# _transcribe_elevenlabs
# ============================================================================
class TestTranscribeElevenLabs:
def test_successful_transcription(self, monkeypatch, sample_ogg):
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"text": "hello from elevenlabs"}
config = {
"elevenlabs": {
"language_code": "eng",
"tag_audio_events": True,
"diarize": True,
}
}
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
patch("requests.post", return_value=mock_response) as mock_post:
from tools.transcription_tools import _transcribe_elevenlabs
result = _transcribe_elevenlabs(sample_ogg, "scribe_v2")
assert result["success"] is True
assert result["transcript"] == "hello from elevenlabs"
assert result["provider"] == "elevenlabs"
call_kwargs = mock_post.call_args.kwargs
assert call_kwargs["headers"]["xi-api-key"] == "eleven-test-key"
assert call_kwargs["data"]["model_id"] == "scribe_v2"
assert call_kwargs["data"]["language_code"] == "eng"
assert call_kwargs["data"]["tag_audio_events"] == "true"
assert call_kwargs["data"]["diarize"] == "true"
# ============================================================================
# _get_provider — ElevenLabs
# ============================================================================
class TestGetProviderElevenLabs:
"""ElevenLabs-specific provider selection tests."""
def test_auto_detect_elevenlabs_after_xai(self, monkeypatch):
"""Auto-detect: elevenlabs is tried after xai when all above are unavailable."""
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", False):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "elevenlabs"
# ============================================================================
# transcribe_audio — ElevenLabs dispatch
# ============================================================================
class TestTranscribeAudioElevenLabsDispatch:
def test_config_elevenlabs_model_used(self, sample_ogg):
config = {"provider": "elevenlabs", "elevenlabs": {"model_id": "scribe_v1"}}
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
patch("tools.transcription_tools._get_provider", return_value="elevenlabs"), \
patch("tools.transcription_tools._transcribe_elevenlabs",
return_value={"success": True, "transcript": "hi"}) as mock_elevenlabs:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model=None)
assert mock_elevenlabs.call_args[0][1] == "scribe_v1"
# ============================================================================
# _extract_transcript_text
# ============================================================================
class TestExtractTranscriptText:
def test_strips_qwen3_asr_language_envelope(self):
from tools.transcription_tools import _extract_transcript_text
result = _extract_transcript_text(
"language zh\n<audio_language>zh</audio_language>\n<asr_text>你好,世界",
)
assert result == "你好,世界"
def test_keeps_non_envelope_marker_literal(self):
from tools.transcription_tools import _extract_transcript_text
result = _extract_transcript_text(
"The user literally said <asr_text> while reading markup.",
)
assert result == "The user literally said <asr_text> while reading markup."
# Shell safety — shlex.split on auto-detected templates
# ============================================================================
class TestShellSafety:
def test_auto_detected_template_is_shlex_safe(self, monkeypatch):
"""Auto-detected whisper command should be safely splittable."""
import shlex
monkeypatch.delenv("HERMES_LOCAL_STT_COMMAND", raising=False)
monkeypatch.setattr(
"tools.transcription_tools._find_whisper_binary",
lambda: "/usr/bin/whisper",
)
from tools.transcription_tools import _get_local_command_template
template = _get_local_command_template()
assert template is not None
cmd = template.format(
input_path=shlex.quote("/tmp/test.wav"),
output_dir=shlex.quote("/tmp/out"),
language=shlex.quote("en"),
model=shlex.quote("base"),
)
parts = shlex.split(cmd)
assert parts[0] == "/usr/bin/whisper"
assert "/tmp/test.wav" in parts
def test_env_var_template_metacharacters_are_literal_argv(
self, monkeypatch, sample_wav, tmp_path
):
from tools.transcription_tools import (
LOCAL_STT_COMMAND_ENV,
_transcribe_local_command,
windows_hide_flags,
)
output_dir = tmp_path / "transcript-output"
output_dir.mkdir()
monkeypatch.setenv(
LOCAL_STT_COMMAND_ENV,
(
"whisper {input_path} ; printf injected | tee log.txt "
"&& echo $(id) `whoami` --output_dir {output_dir}"
),
)
def fake_tempdir(prefix=None):
class _TempDir:
def __enter__(self):
return str(output_dir)
def __exit__(self, exc_type, exc, tb):
return False
return _TempDir()
invocation = {}
def fake_run(command, **kwargs):
invocation["command"] = command
invocation["kwargs"] = kwargs
(output_dir / "transcript.txt").write_text("safe", encoding="utf-8")
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(
"tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir
)
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
result = _transcribe_local_command(sample_wav, "base")
assert result["transcript"] == "safe"
assert invocation["command"] == [
"whisper",
sample_wav,
";",
"printf",
"injected",
"|",
"tee",
"log.txt",
"&&",
"echo",
"$(id)",
"`whoami`",
"--output_dir",
str(output_dir),
]
assert invocation["kwargs"].pop("env") is not None
assert invocation["kwargs"] == {
"check": True,
"capture_output": True,
"text": True,
"encoding": "utf-8",
"errors": "replace",
"timeout": 300,
"stdin": subprocess.DEVNULL,
"creationflags": windows_hide_flags(),
}
class TestLocalModelLock:
"""#24767 — concurrent first-use must not double-load the whisper model."""
def test_concurrent_transcribe_loads_model_once(self, tmp_path):
import threading
from tools.transcription_tools import _transcribe_local
audio = tmp_path / "test.ogg"
audio.write_bytes(b"fake")
seg = MagicMock()
seg.text = "hello"
info = MagicMock()
info.language = "en"
info.duration = 1.0
load_count = 0
load_started = threading.Event()
def slow_load(model_name, device="auto", compute_type="auto"):
nonlocal load_count
load_count += 1
load_started.set()
import time
time.sleep(0.05)
model = MagicMock()
model.transcribe.return_value = ([seg], info)
return model
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("tools.transcription_tools._load_local_whisper_model", side_effect=slow_load), \
patch("tools.transcription_tools._local_model", None), \
patch("tools.transcription_tools._local_model_name", None):
threads = [
threading.Thread(target=_transcribe_local, args=(str(audio), "base"))
for _ in range(4)
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=10)
assert load_count == 1
class TestLocalBaseUrlNoApiKey:
"""#25193 — empty api_key with a local base_url should not raise."""
def test_local_base_url_returns_placeholder_key(self):
from tools.transcription_tools import _resolve_openai_audio_client_config
with patch(
"tools.transcription_tools._load_stt_config",
return_value={"openai": {"base_url": "http://localhost:8504/v1"}},
):
api_key, base_url = _resolve_openai_audio_client_config()
assert api_key == "not-needed"
assert base_url == "http://localhost:8504/v1"
def test_is_local_or_private_url(self):
from tools.transcription_tools import _is_local_or_private_url
assert _is_local_or_private_url("http://localhost:8504/v1")
assert _is_local_or_private_url("http://127.0.0.1:9000")
assert _is_local_or_private_url("http://10.0.0.5/v1")
assert _is_local_or_private_url("http://stt.internal/v1")
assert not _is_local_or_private_url("https://api.openai.com/v1")
assert not _is_local_or_private_url("")
# =====================================================================
# CAF (iMessage voice note) conversion tests
# ============================================================================
class TestCafConversion:
"""Tests for _convert_caf_to_wav and CAF dispatch in transcribe_audio."""
def test_convert_caf_with_ffmpeg(self, tmp_path, monkeypatch):
"""_convert_caf_to_wav uses ffmpeg when available."""
caf_path = tmp_path / "voice.caf"
caf_path.write_bytes(b"caff\x00" * 20)
wav_path = str(tmp_path / "voice.wav")
def fake_run(cmd, **kwargs):
Path(wav_path).write_bytes(b"RIFF\x00\x00\x00\x00")
return MagicMock(returncode=0)
monkeypatch.setattr(
"tools.transcription_tools._find_ffmpeg_binary",
lambda: "/usr/bin/ffmpeg",
)
monkeypatch.setattr(subprocess, "run", fake_run)
from tools.transcription_tools import _convert_caf_to_wav
result = _convert_caf_to_wav(str(caf_path))
assert result == wav_path
assert Path(result).exists()
def test_transcribe_caf_not_converted_for_local(self, tmp_path, monkeypatch):
"""CAF conversion is skipped for local provider (native handling)."""
caf_path = tmp_path / "voice.caf"
caf_path.write_bytes(b"caff\x00" * 20)
with patch("tools.transcription_tools._load_stt_config",
return_value={"provider": "local"}), \
patch("tools.transcription_tools._get_provider",
return_value="local"), \
patch("tools.transcription_tools._convert_caf_to_wav") as mock_convert, \
patch("tools.transcription_tools._transcribe_local",
return_value={"success": True, "transcript": "hi"}):
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(str(caf_path))
assert result["success"] is True
mock_convert.assert_not_called()
class TestTranscribeCredentialReadGuard:
"""transcribe_audio must refuse credential/secret stores before dispatch."""
def test_transcribe_audio_blocks_credential_read(self, tmp_path):
"""A ``.env`` (secret-bearing) file is refused up front, so its
plaintext is never shipped to an external STT provider — mirroring the
read guard added to image-gen (587be5b5b) and xAI video-gen
(104232979)."""
from tools.transcription_tools import transcribe_audio
from agent.file_safety import get_read_block_error
env_file = tmp_path / ".env"
env_file.write_text("OPENAI_API_KEY=sk-secret\n")
expected = get_read_block_error(str(env_file))
assert expected, "test setup: a .env file should be read-blocked"
result = transcribe_audio(str(env_file))
assert result["success"] is False
# The error is the shared read-guard message, not an audio-validation
# or provider error — proving the guard fired before dispatch.
assert result["error"] == expected
class TestRunCommandSttIdleTimeout:
"""_run_command_stt uses a progress-based idle timeout (mirrors TTS runner)."""
@staticmethod
def _shell_command(*args):
import shlex
if os.name == "nt":
return subprocess.list2cmdline(list(args))
return " ".join(shlex.quote(str(arg)) for arg in args)
def test_stderr_progress_extends_beyond_timeout(self, tmp_path):
"""A slow-but-alive command that keeps emitting output survives an
idle timeout shorter than its total runtime."""
from tools.transcription_tools import _run_command_stt
script = tmp_path / "progress_then_exit.py"
script.write_text(
"\n".join([
"import sys, time",
"for idx in range(4):",
" print(f'tick {idx}', file=sys.stderr, flush=True)",
" time.sleep(0.04)",
"print('done', flush=True)",
]),
encoding="utf-8",
)
result = _run_command_stt(
self._shell_command(sys.executable, "-u", str(script)),
timeout=0.1,
)
assert result.returncode == 0
assert "tick 3" in result.stderr
assert "done" in result.stdout
def test_silent_stall_still_times_out(self, tmp_path):
"""A silently stalled command is killed once the idle window elapses,
and pre-stall output is preserved on the TimeoutExpired."""
from tools.transcription_tools import _run_command_stt
script = tmp_path / "progress_then_hang.py"
script.write_text(
"\n".join([
"import sys, time",
"print('starting pass 1', file=sys.stderr, flush=True)",
"time.sleep(30)",
]),
encoding="utf-8",
)
with pytest.raises(subprocess.TimeoutExpired) as excinfo:
_run_command_stt(
self._shell_command(sys.executable, "-u", str(script)),
timeout=0.1,
)
assert "starting pass 1" in (excinfo.value.stderr or "")