"""Unit tests for shared models.""" import json from unittest.mock import Mock, patch import pytest from tests.unit.conftest import make_state as make_server_state def make_preferences(**kwargs): from linux_voice_assistant.models import Preferences return Preferences(**kwargs) # --------------------------------------------------------------------------- # WakeWordType # --------------------------------------------------------------------------- class TestWakeWordType: def test_micro_value(self): from linux_voice_assistant.models import WakeWordType assert WakeWordType.MICRO_WAKE_WORD == "micro" def test_open_wake_word_value(self): from linux_voice_assistant.models import WakeWordType assert WakeWordType.OPEN_WAKE_WORD == "openWakeWord" def test_is_string_enum(self): from linux_voice_assistant.models import WakeWordType assert isinstance(WakeWordType.MICRO_WAKE_WORD, str) # --------------------------------------------------------------------------- # Preferences # --------------------------------------------------------------------------- class TestPreferences: def test_default_active_wake_words_is_empty_list(self): p = make_preferences() assert p.active_wake_words == [] def test_default_volume_is_none(self): p = make_preferences() assert p.volume is None def test_default_thinking_sound_is_zero(self): p = make_preferences() assert p.thinking_sound == 0 def test_default_mic_auto_gain_is_zero(self): p = make_preferences() assert p.mic_auto_gain == 0 def test_default_mic_noise_suppression_is_zero(self): p = make_preferences() assert p.mic_noise_suppression == 0 def test_custom_values_stored(self): p = make_preferences( volume=0.8, mic_auto_gain=5, mic_noise_suppression=2, thinking_sound=1, ) assert p.volume == 0.8 assert p.mic_auto_gain == 5 assert p.mic_noise_suppression == 2 assert p.thinking_sound == 1 def test_active_wake_words_are_independent_per_instance(self): """Mutable default must not be shared between instances.""" p1 = make_preferences() p2 = make_preferences() p1.active_wake_words.append("okay_nabu") assert p2.active_wake_words == [] # --------------------------------------------------------------------------- # ServerState.save_preferences() # --------------------------------------------------------------------------- class TestSavePreferences: def test_creates_preferences_file(self, tmp_path): state = make_server_state(tmp_path) state.save_preferences() assert state.preferences_path.exists() def test_saved_json_is_valid(self, tmp_path): state = make_server_state(tmp_path) state.save_preferences() with open(state.preferences_path) as f: data = json.load(f) assert isinstance(data, dict) def test_saved_json_contains_expected_keys(self, tmp_path): state = make_server_state(tmp_path) state.save_preferences() with open(state.preferences_path) as f: data = json.load(f) assert "mic_auto_gain" in data assert "mic_noise_suppression" in data assert "volume" in data assert "thinking_sound" in data def test_saved_values_match_preferences(self, tmp_path): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=0.75, mic_auto_gain=3, mic_noise_suppression=1) state = make_server_state(tmp_path, preferences=prefs) state.save_preferences() with open(state.preferences_path) as f: data = json.load(f) assert data["volume"] == 0.75 assert data["mic_auto_gain"] == 3 assert data["mic_noise_suppression"] == 1 def test_creates_parent_directory_if_missing(self, tmp_path): nested_path = tmp_path / "nested" / "dir" / "preferences.json" state = make_server_state(tmp_path, preferences_path=nested_path) state.save_preferences() assert nested_path.exists() # --------------------------------------------------------------------------- # ServerState.persist_volume() # --------------------------------------------------------------------------- class TestPersistVolume: def test_updates_volume_on_state(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(0.5) assert state.volume == 0.5 def test_updates_volume_on_preferences(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(0.5) assert state.preferences.volume == 0.5 def test_clamps_above_one(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(1.5) assert state.volume == 1.0 def test_clamps_below_zero(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(-0.5) assert state.volume == 0.0 def test_saves_to_file(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(0.6) with open(state.preferences_path) as f: data = json.load(f) assert data["volume"] == pytest.approx(0.6) def test_skips_save_when_volume_unchanged(self, tmp_path): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=0.5) state = make_server_state(tmp_path, preferences=prefs, volume=0.5) with patch.object(state, "save_preferences") as mock_save: state.persist_volume(0.5) mock_save.assert_not_called() def test_saves_when_volume_changed(self, tmp_path): from linux_voice_assistant.models import Preferences prefs = Preferences(volume=0.5) state = make_server_state(tmp_path, preferences=prefs, volume=0.5) with patch.object(state, "save_preferences") as mock_save: state.persist_volume(0.8) mock_save.assert_called_once() def test_boundary_value_zero(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(0.0) assert state.volume == 0.0 def test_boundary_value_one(self, tmp_path): state = make_server_state(tmp_path) state.persist_volume(1.0) assert state.volume == 1.0 def test_emits_volume_muted_event_when_volume_zero(self, tmp_path): from linux_voice_assistant.peripheral_api import LVAEvent state = make_server_state(tmp_path) state.peripheral_api = Mock() state.persist_volume(0.0) state.peripheral_api.emit_event_sync.assert_any_call(LVAEvent.VOLUME_CHANGED, {"volume": 0.0}) state.peripheral_api.emit_event_sync.assert_any_call(LVAEvent.VOLUME_MUTED, {"muted": True}) # --------------------------------------------------------------------------- # ServerState.persist_mic_gain() # --------------------------------------------------------------------------- class TestPersistMicGain: def test_updates_mic_auto_gain_on_state(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_gain(5.0) assert state.mic_auto_gain == 5 def test_updates_mic_auto_gain_on_preferences(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_gain(5.0) assert state.preferences.mic_auto_gain == 5 def test_converts_float_to_int(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_gain(7.9) assert state.mic_auto_gain == 7 def test_skips_save_when_gain_unchanged(self, tmp_path): state = make_server_state(tmp_path) state.mic_auto_gain = 3 state.preferences.mic_auto_gain = 3 with patch.object(state, "save_preferences") as mock_save: state.persist_mic_gain(3.0) mock_save.assert_not_called() def test_saves_when_gain_changed(self, tmp_path): state = make_server_state(tmp_path) state.mic_auto_gain = 3 state.preferences.mic_auto_gain = 3 with patch.object(state, "save_preferences") as mock_save: state.persist_mic_gain(10.0) mock_save.assert_called_once() # --------------------------------------------------------------------------- # ServerState.persist_mic_noise() # --------------------------------------------------------------------------- class TestPersistMicNoise: def test_updates_mic_noise_suppression_on_state(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_noise(2.0) assert state.mic_noise_suppression == 2 def test_updates_mic_noise_suppression_on_preferences(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_noise(2.0) assert state.preferences.mic_noise_suppression == 2 def test_converts_float_to_int(self, tmp_path): state = make_server_state(tmp_path) state.persist_mic_noise(3.9) assert state.mic_noise_suppression == 3 def test_skips_save_when_noise_unchanged(self, tmp_path): state = make_server_state(tmp_path) state.mic_noise_suppression = 2 state.preferences.mic_noise_suppression = 2 with patch.object(state, "save_preferences") as mock_save: state.persist_mic_noise(2.0) mock_save.assert_not_called() def test_saves_when_noise_changed(self, tmp_path): state = make_server_state(tmp_path) state.mic_noise_suppression = 2 state.preferences.mic_noise_suppression = 2 with patch.object(state, "save_preferences") as mock_save: state.persist_mic_noise(4.0) mock_save.assert_called_once() # --------------------------------------------------------------------------- # initial_stop_word_threshold() # --------------------------------------------------------------------------- class TestInitialStopWordThreshold: def test_saved_sensitivity_is_restored(self): from linux_voice_assistant.models import initial_stop_word_threshold assert initial_stop_word_threshold(0.9) == pytest.approx(0.9) def test_falls_back_to_server_state_default_when_never_set(self): from linux_voice_assistant.models import ServerState, initial_stop_word_threshold assert initial_stop_word_threshold(None) == pytest.approx(ServerState.stop_word_threshold) def test_clamped_to_one_when_above(self): from linux_voice_assistant.models import initial_stop_word_threshold assert initial_stop_word_threshold(1.5) == pytest.approx(1.0) def test_clamped_to_zero_when_below(self): from linux_voice_assistant.models import initial_stop_word_threshold assert initial_stop_word_threshold(-0.5) == pytest.approx(0.0)