From a4fc156c8d65d275d82f04afcf5a977d787873dc Mon Sep 17 00:00:00 2001 From: dev-lead Date: Fri, 8 May 2026 13:47:38 -0700 Subject: [PATCH] fix(voice_mode): restore audio-env detection across clean/WSL/Termux scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 5e1197a4 swapped the inline `os.path.exists('/.dockerenv')` check in `detect_audio_environment()` for the more thorough `is_container()` helper in `hermes_constants` (also matches /run/.containerenv and /proc/1/cgroup markers, with module-level caching). That helper correctly returns True on CI runners that themselves run inside Docker, which silently appended a "Running inside Docker container" warning to every detection scenario and broke four tests whose contract is "should be available": - test_clean_environment_is_available - test_wsl_with_pulse_allows_voice - test_wsl_device_query_fails_with_pulse_continues - test_termux_api_microphone_allows_voice_without_sounddevice The five "should be blocked" sibling tests passed only by coincidence — the extra container warning still left `available=False`. Fix: - Hoist `is_container` to a module-level import in tools/voice_mode.py so it's reachable as `tools.voice_mode.is_container` (matches the monkeypatch convention used elsewhere in the test file for `shutil`, `_import_audio`, `_termux_api_app_installed`, etc). - Add an autouse fixture in `TestDetectAudioEnvironment` defaulting `is_container` to False, so tests don't inherit the host runner's container state. Per `feedback_no_such_thing_as_flakes`: the failures were a real environmental coupling bug, not a flake. - Add `test_docker_container_blocks_voice` to preserve and pin the container-blocks-voice intent that the original inline check encoded. Partial close hermes-agent#9. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/tools/test_voice_mode.py | 24 ++++++++++++++++++++++++ tools/voice_mode.py | 3 +-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_voice_mode.py b/tests/tools/test_voice_mode.py index 1d35c486..001aae60 100644 --- a/tests/tools/test_voice_mode.py +++ b/tests/tools/test_voice_mode.py @@ -61,6 +61,16 @@ def mock_sd(monkeypatch): # ============================================================================ class TestDetectAudioEnvironment: + @pytest.fixture(autouse=True) + def _isolate_container_detection(self, monkeypatch): + """Default `is_container` to False so tests don't inherit the host + runner's container state (e.g. CI itself runs inside Docker, where + the production `is_container()` returns True via /.dockerenv or + /proc/1/cgroup and silently appended a 'Running inside Docker' + warning to every scenario). Individual tests opt in via setattr. + """ + monkeypatch.setattr("tools.voice_mode.is_container", lambda: False) + def test_clean_environment_is_available(self, monkeypatch): """No SSH, Docker, or WSL — should be available.""" monkeypatch.delenv("SSH_CLIENT", raising=False) @@ -85,6 +95,20 @@ class TestDetectAudioEnvironment: assert result["available"] is False assert any("SSH" in w for w in result["warnings"]) + def test_docker_container_blocks_voice(self, monkeypatch): + """Running inside a Docker/Podman container should block voice mode.""" + monkeypatch.delenv("SSH_CLIENT", raising=False) + monkeypatch.delenv("SSH_TTY", raising=False) + monkeypatch.delenv("SSH_CONNECTION", raising=False) + monkeypatch.setattr("tools.voice_mode.is_container", lambda: True) + monkeypatch.setattr("tools.voice_mode._import_audio", + lambda: (MagicMock(), MagicMock())) + + from tools.voice_mode import detect_audio_environment + result = detect_audio_environment() + assert result["available"] is False + assert any("Docker container" in w for w in result["warnings"]) + def test_wsl_without_pulse_blocks_voice(self, monkeypatch, tmp_path): """WSL without PULSE_SERVER should block voice mode.""" monkeypatch.delenv("SSH_CLIENT", raising=False) diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 66ecb242..a940957c 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -49,7 +49,7 @@ def _audio_available() -> bool: return False -from hermes_constants import is_termux as _is_termux_environment +from hermes_constants import is_container, is_termux as _is_termux_environment def _voice_capture_install_hint() -> str: @@ -103,7 +103,6 @@ def detect_audio_environment() -> dict: warnings.append("Running over SSH -- no audio devices available") # Docker/Podman container detection - from hermes_constants import is_container if is_container(): warnings.append("Running inside Docker container -- no audio devices")