From ef686c38782b82403714d86fd326c4adfebfa825 Mon Sep 17 00:00:00 2001 From: Slippy87 <76743883+Solitud1nem@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:44:19 +0000 Subject: [PATCH] fix(voice): honor forwarded audio (PIPEWIRE_REMOTE) in WSL detection --- tests/tools/test_voice_wsl_pipewire.py | 58 ++++++++++++++++++++++++++ tools/voice_mode.py | 18 ++++---- 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 tests/tools/test_voice_wsl_pipewire.py diff --git a/tests/tools/test_voice_wsl_pipewire.py b/tests/tools/test_voice_wsl_pipewire.py new file mode 100644 index 000000000000..395d401fbf4e --- /dev/null +++ b/tests/tools/test_voice_wsl_pipewire.py @@ -0,0 +1,58 @@ +"""Regression: WSL voice detection must honor PIPEWIRE_REMOTE, not only PULSE_SERVER. + +detect_audio_environment() honors forwarded audio (has_forwarded_audio = +PULSE_SERVER or PIPEWIRE_REMOTE or a reachable socket) in the SSH and container +blocks, but the WSL block previously checked only PULSE_SERVER — so a WSL user +with PipeWire forwarding (PIPEWIRE_REMOTE) was wrongly blocked from voice mode. +These tests mock /proc/version so they reproduce the WSL path on any host. +""" +import builtins +import io +from unittest.mock import MagicMock + +WSL = "Linux version 5.15.0-microsoft-standard-WSL2 (oe-user@oe-host)" + + +def _force_wsl(monkeypatch, content=WSL): + real_open = builtins.open + def fake_open(file, *a, **k): + if str(file) == "/proc/version": + return io.StringIO(content) + return real_open(file, *a, **k) + monkeypatch.setattr(builtins, "open", fake_open) + + +def _base(monkeypatch): + for v in ("SSH_CLIENT", "SSH_TTY", "SSH_CONNECTION", "PULSE_SERVER"): + monkeypatch.delenv(v, raising=False) + monkeypatch.delenv("PIPEWIRE_REMOTE", raising=False) + monkeypatch.setattr("hermes_constants.is_container", lambda: False) + monkeypatch.setattr("tools.voice_mode._pulse_socket_reachable", lambda: False) + sd = MagicMock(); sd.query_devices.return_value = [{"name": "dev"}] + monkeypatch.setattr("tools.voice_mode._import_audio", lambda: (sd, MagicMock())) + + +def test_wsl_with_pipewire_remote_allows_voice(monkeypatch): + _base(monkeypatch) + monkeypatch.setenv("PIPEWIRE_REMOTE", "/run/user/1000/pipewire-0") + _force_wsl(monkeypatch) + from tools.voice_mode import detect_audio_environment + result = detect_audio_environment() + assert result["available"] is True, result["warnings"] + + +def test_wsl_with_pulse_server_still_allows_voice(monkeypatch): + _base(monkeypatch) + monkeypatch.setenv("PULSE_SERVER", "unix:/mnt/wslg/PulseServer") + _force_wsl(monkeypatch) + from tools.voice_mode import detect_audio_environment + assert detect_audio_environment()["available"] is True + + +def test_wsl_without_forwarding_still_blocks(monkeypatch): + _base(monkeypatch) + _force_wsl(monkeypatch) # no PULSE_SERVER, no PIPEWIRE_REMOTE + from tools.voice_mode import detect_audio_environment + res = detect_audio_environment() + assert res["available"] is False + assert any("WSL" in w for w in res["warnings"]) diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 5b307909587a..e92246ad700e 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -207,19 +207,21 @@ def detect_audio_environment() -> dict: " PipeWire: -e PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0" ) - # WSL detection — PulseAudio bridge makes audio work in WSL. - # Only block if PULSE_SERVER is not configured. + # WSL detection — a reachable sound server makes audio work in WSL. + # Honor any forwarding (PulseAudio bridge OR a forwarded PipeWire/Pulse + # socket), mirroring the SSH and container blocks above. Only block when + # no forwarding is configured. try: with open('/proc/version', 'r', encoding="utf-8") as f: if 'microsoft' in f.read().lower(): - if os.environ.get('PULSE_SERVER'): - notices.append("Running in WSL with PulseAudio bridge") + if has_forwarded_audio: + notices.append("Running in WSL with a reachable PulseAudio/PipeWire sound server") else: warnings.append( - "Running in WSL -- audio requires PulseAudio bridge.\n" - " 1. Set PULSE_SERVER=unix:/mnt/wslg/PulseServer\n" - " 2. Create ~/.asoundrc pointing ALSA at PulseAudio\n" - " 3. Verify with: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wav" + "Running in WSL -- audio requires a forwarded sound server.\n" + " PulseAudio: export PULSE_SERVER=unix:/mnt/wslg/PulseServer\n" + " PipeWire: export PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0\n" + " Then verify: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wav" ) except (FileNotFoundError, PermissionError, OSError): pass