Files
hermes-agent/tests/tools/test_stt_resolution_passive.py
ethernet 398867326d fix(voice): requirement checks answer from state, never install
Provider RESOLUTION ran an installer. `_detect_local_backend` called
`_try_lazy_install_stt` -> `pm.ensure_import`, and the TTS requirement entries called the
SDK importers, which install their extra on import. Both sit in status paths:
`wake.status` fires on every desktop gateway-open, `/voice status` and
`GET /api/audio/voice-config` resolve the same chain, and `check_tts_requirements` is the
`text_to_speech` tool's `check_fn`, so it runs on every tool listing.

The install is a whole-environment rebuild (a fresh generation, `--all-packages` over the
enabled set) under the per-install lock, so a passive probe could hold a sibling profile's
backend off its port for tens of seconds — the `hermes-setup` onboarding backend gave up at
40 s while an unrelated status probe's rebuild held the lock.

Availability now reads state: `pm.extras.available` for the extra (pm owns the extra->anchor
table, counts sys.modules fakes, and honours platform gates), `find_spec` for the rest. A
provider whose SDK installs at first use still counts as READY when lazy installs are allowed
and its own credential and platform gate permit — otherwise the tool would vanish from the
schema for exactly the users that install serves. Synthesis keeps its on-demand install, and
the lazy STT path now asks for `stt-whisper`, the extra carrying only faster-whisper.

Validated: tests/tools/ for the TTS/STT suites (test_tts_mistral's importable-SDK contract
included). A/B on HEAD: the two new files' non-guard tests fail there (3 + 1), pass here; the
guard test that transcription still installs passes on both.
2026-09-15 10:48:09 -04:00

39 lines
1.6 KiB
Python

"""STT provider RESOLUTION never installs; transcription still does.
Regression: ``_detect_local_backend`` lazy-installed faster-whisper, and it sits in the provider
resolution chain every status surface uses — ``wake.status`` (fired on every desktop
gateway-open), ``/voice status`` and ``GET /api/audio/voice-config``. Each of those could start a
full dependency rebuild, holding the per-install lock long enough to keep a sibling profile's
backend off its port.
"""
import pytest
from tools import transcription_tools
@pytest.fixture
def no_faster_whisper(monkeypatch):
"""A machine without faster-whisper or a whisper CLI, recording install attempts."""
attempts: list[int] = []
monkeypatch.setattr(transcription_tools, "_HAS_FASTER_WHISPER", False)
monkeypatch.setattr(transcription_tools, "_has_local_command", lambda: False)
monkeypatch.setattr(transcription_tools, "_try_lazy_install_stt",
lambda: attempts.append(1) or False)
return transcription_tools, attempts
def test_resolution_reports_unavailable_without_installing(no_faster_whisper):
tools, attempts = no_faster_whisper
assert tools._detect_local_backend() is None
assert tools._resolve_explicit_local() == "none"
assert attempts == []
def test_transcription_is_what_installs(no_faster_whisper):
tools, attempts = no_faster_whisper
result = tools._transcribe_local("/tmp/absent.wav", "base")
assert attempts == [1], "the action path lost its on-demand install"
assert result["success"] is False
assert "faster-whisper not installed" in result["error"]