"""Unit tests for __main__.py — process_audio logic and argument parsing helpers.""" import numpy as np import pytest from tests.unit.conftest import make_state def make_audio_chunk(samples: int = 1024, value: float = 0.0) -> np.ndarray: return np.full(samples, value, dtype=np.float32) # --------------------------------------------------------------------------- # process_audio — mic volume scaling # --------------------------------------------------------------------------- class TestProcessAudioMicVolume: def test_mic_volume_scalar_at_100(self, tmp_path): """mic_volume=100 → scalar=1.0 → no attenuation.""" state = make_state(tmp_path) state.mic_volume = 100 state.satellite = None # no satellite → loop exits immediately # The scalar clamp: max(0.1, min(1.0, 100/100)) = 1.0 scalar = max(0.1, min(1.0, state.mic_volume / 100.0)) assert scalar == pytest.approx(1.0) def test_mic_volume_scalar_at_50(self, tmp_path): state = make_state(tmp_path) state.mic_volume = 50 scalar = max(0.1, min(1.0, state.mic_volume / 100.0)) assert scalar == pytest.approx(0.5) def test_mic_volume_scalar_clamped_above_100(self, tmp_path): state = make_state(tmp_path) state.mic_volume = 200 scalar = max(0.1, min(1.0, state.mic_volume / 100.0)) assert scalar == pytest.approx(1.0) def test_mic_volume_scalar_minimum_is_0_1(self, tmp_path): state = make_state(tmp_path) state.mic_volume = 0 scalar = max(0.1, min(1.0, state.mic_volume / 100.0)) assert scalar == pytest.approx(0.1) def test_audio_chunk_scaled_by_mic_volume(self): """Verify the numpy scaling produces correct output.""" audio = make_audio_chunk(value=0.5) mic_vol_scalar = 0.5 result = np.clip(audio * mic_vol_scalar, -1.0, 1.0) assert np.allclose(result, 0.25) def test_audio_chunk_clipped_to_bounds(self): """Values that exceed [-1, 1] after scaling should be clipped.""" audio = make_audio_chunk(value=1.0) mic_vol_scalar = 2.0 # would push to 2.0 without clip result = np.clip(audio * mic_vol_scalar, -1.0, 1.0) assert np.all(result <= 1.0) # --------------------------------------------------------------------------- # process_audio — WebRTC integration # --------------------------------------------------------------------------- class TestProcessAudioWebRTC: def test_webrtc_not_created_when_agc_and_ns_zero(self, tmp_path): """If both AGC and NS are 0, WebRTCProcessor should never be instantiated.""" state = make_state(tmp_path) state.preferences.mic_auto_gain = 0 state.preferences.mic_noise_suppression = 0 agc = state.preferences.mic_auto_gain or 0 ns = state.preferences.mic_noise_suppression or 0 should_create = agc > 0 or ns > 0 assert should_create is False def test_webrtc_created_when_agc_nonzero(self, tmp_path): state = make_state(tmp_path) state.preferences.mic_auto_gain = 5 state.preferences.mic_noise_suppression = 0 agc = state.preferences.mic_auto_gain or 0 ns = state.preferences.mic_noise_suppression or 0 should_create = agc > 0 or ns > 0 assert should_create is True def test_webrtc_created_when_ns_nonzero(self, tmp_path): state = make_state(tmp_path) state.preferences.mic_auto_gain = 0 state.preferences.mic_noise_suppression = 2 agc = state.preferences.mic_auto_gain or 0 ns = state.preferences.mic_noise_suppression or 0 should_create = agc > 0 or ns > 0 assert should_create is True # --------------------------------------------------------------------------- # process_audio — wake word refractory # --------------------------------------------------------------------------- class TestWakeWordRefractory: def test_refractory_allows_activation_when_none(self): """First activation (last_active=None) should always be allowed.""" import time last_active = None refractory_seconds = 2.0 now = time.monotonic() allowed = (last_active is None) or ((now - last_active) > refractory_seconds) assert allowed is True def test_refractory_blocks_activation_too_soon(self): """Activation within refractory period should be blocked.""" import time last_active = time.monotonic() # just activated refractory_seconds = 2.0 now = time.monotonic() allowed = (last_active is None) or ((now - last_active) > refractory_seconds) assert allowed is False def test_refractory_allows_activation_after_period(self): """Activation after refractory period should be allowed.""" import time last_active = time.monotonic() - 3.0 # 3 seconds ago refractory_seconds = 2.0 now = time.monotonic() allowed = (last_active is None) or ((now - last_active) > refractory_seconds) assert allowed is True # --------------------------------------------------------------------------- # process_audio — stop word detection logic # --------------------------------------------------------------------------- class TestStopWordLogic: def test_stop_word_only_triggers_when_in_active_set(self, tmp_path): state = make_state(tmp_path) # Stop word not in active set → should not trigger stop state.active_wake_words = set() stopped = True # simulating detection should_stop = stopped and (state.stop_word.id in state.active_wake_words) and not state.muted assert should_stop is False def test_stop_word_triggers_when_in_active_set_and_not_muted(self, tmp_path): state = make_state(tmp_path) state.active_wake_words = {state.stop_word.id} state.muted = False stopped = True should_stop = stopped and (state.stop_word.id in state.active_wake_words) and not state.muted assert should_stop is True def test_stop_word_does_not_trigger_when_muted(self, tmp_path): state = make_state(tmp_path) state.active_wake_words = {state.stop_word.id} state.muted = True stopped = True should_stop = stopped and (state.stop_word.id in state.active_wake_words) and not state.muted assert should_stop is False # --------------------------------------------------------------------------- # Preferences loading from args # --------------------------------------------------------------------------- class TestPreferencesFromArgs: def test_agc_stored_in_preferences_when_nonzero(self): from linux_voice_assistant.models import Preferences prefs = Preferences() mic_auto_gain = 5 if mic_auto_gain > 0: prefs.mic_auto_gain = mic_auto_gain assert prefs.mic_auto_gain == 5 def test_agc_not_stored_when_zero(self): from linux_voice_assistant.models import Preferences prefs = Preferences() mic_auto_gain = 0 if mic_auto_gain > 0: prefs.mic_auto_gain = mic_auto_gain assert prefs.mic_auto_gain == 0 def test_ns_stored_in_preferences_when_nonzero(self): from linux_voice_assistant.models import Preferences prefs = Preferences() mic_noise_suppression = 2 if mic_noise_suppression > 0: prefs.mic_noise_suppression = mic_noise_suppression assert prefs.mic_noise_suppression == 2 def test_volume_clamped_to_one_when_above(self): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=1.5) initial_volume = prefs.volume if prefs.volume is not None else 1.0 initial_volume = max(0.0, min(1.0, float(initial_volume))) assert initial_volume == pytest.approx(1.0) def test_volume_clamped_to_zero_when_below(self): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=-0.5) initial_volume = prefs.volume if prefs.volume is not None else 1.0 initial_volume = max(0.0, min(1.0, float(initial_volume))) assert initial_volume == pytest.approx(0.0) def test_volume_defaults_to_one_when_none(self): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=None) initial_volume = prefs.volume if prefs.volume is not None else 1.0 initial_volume = max(0.0, min(1.0, float(initial_volume))) assert initial_volume == pytest.approx(1.0) def test_thinking_sound_enabled_by_flag(self): from linux_voice_assistant.models import Preferences prefs = Preferences() enable_thinking_sound = True if enable_thinking_sound: prefs.thinking_sound = 1 assert prefs.thinking_sound == 1