Files
voculinux/tests/test_recommendation_is_actionable.py
T
Jatin K MalikandGitHub 26f90b6998 chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch.

Fixes tracked in the PR. Post-release follow-ups: #834.
2026-09-16 00:18:09 -07:00

204 lines
7.7 KiB
Python

"""The recommendation must be applicable, and must not ignore the disk (#778)."""
from __future__ import annotations
import importlib
import sys
from collections.abc import Iterator
from typing import Any
from unittest.mock import MagicMock, Mock, call, patch
import pytest
import vocalinux.ui
@pytest.fixture(scope="module")
def settings_dialog() -> Iterator[Any]:
"""Import settings_dialog with real base classes for its GTK subclasses.
Same reasoning as tests/test_settings_model_state.py; both sys.modules and the
package attribute are restored so later tests patching the module by name do
not reach a second module object (#686).
"""
repository = sys.modules["gi.repository"]
bases = {name: type(name, (), {}) for name in ("Box", "ListBoxRow", "Dialog")}
saved_module = sys.modules.pop("vocalinux.ui.settings_dialog", None)
saved_attribute = getattr(vocalinux.ui, "settings_dialog", None)
try:
with patch.object(repository, "Gtk", MagicMock(**bases)):
module = importlib.import_module("vocalinux.ui.settings_dialog")
yield module
finally:
sys.modules.pop("vocalinux.ui.settings_dialog", None)
if saved_module is not None:
sys.modules["vocalinux.ui.settings_dialog"] = saved_module
if saved_attribute is not None:
vocalinux.ui.settings_dialog = saved_attribute
elif hasattr(vocalinux.ui, "settings_dialog"):
del vocalinux.ui.settings_dialog
@pytest.fixture
def dialog_class(settings_dialog: Any) -> Any:
return settings_dialog.SettingsDialog
def _dialog_stub(language: str = "en-us") -> Mock:
dialog = Mock()
dialog.language = language
dialog.language_combo.get_active_id.return_value = language
dialog._populating_models = False
dialog._processing_language_change = False
# Plain Mock attrs are truthy; these guards must be real False or the
# language-changed handler returns before updating dialog.language.
dialog._initializing = False
dialog._applying_settings = False
dialog._simple_driving = False
return dialog
def _with_disk(settings_dialog: Any, downloaded: list[str]) -> tuple[Any, Any]:
"""Patch the disk so only ``downloaded`` counts as present."""
return (
patch.object(settings_dialog, "list_downloaded_whispercpp_models", return_value=downloaded),
patch.object(
settings_dialog,
"is_whispercpp_model_downloaded",
side_effect=lambda name: name in downloaded,
),
)
def test_a_bigger_model_on_disk_is_offered_instead_of_a_download(
settings_dialog: Any, dialog_class: Any
) -> None:
"""The reported case: recommended small.en, medium.en already paid for."""
dialog = _dialog_stub()
listed, downloaded = _with_disk(settings_dialog, ["medium.en", "small", "tiny"])
with listed, downloaded:
alternative = dialog_class._downloaded_alternative_for(dialog, "small.en")
assert alternative == "medium.en"
def test_nothing_is_offered_when_the_recommendation_is_already_on_disk(
settings_dialog: Any, dialog_class: Any
) -> None:
dialog = _dialog_stub()
listed, downloaded = _with_disk(settings_dialog, ["small.en", "medium.en"])
with listed, downloaded:
assert dialog_class._downloaded_alternative_for(dialog, "small.en") is None
def test_a_smaller_model_is_never_offered(settings_dialog: Any, dialog_class: Any) -> None:
"""Reusing a download must not quietly cost accuracy."""
dialog = _dialog_stub()
listed, downloaded = _with_disk(settings_dialog, ["tiny", "tiny.en"])
with listed, downloaded:
assert dialog_class._downloaded_alternative_for(dialog, "medium.en") is None
def test_english_only_weights_are_not_offered_for_another_language(
settings_dialog: Any, dialog_class: Any
) -> None:
dialog = _dialog_stub(language="pl")
listed, downloaded = _with_disk(settings_dialog, ["medium.en"])
with listed, downloaded:
assert dialog_class._downloaded_alternative_for(dialog, "small") is None
def test_the_smallest_qualifying_model_wins(settings_dialog: Any, dialog_class: Any) -> None:
"""Between two usable downloads, take the cheaper one to run."""
dialog = _dialog_stub()
listed, downloaded = _with_disk(settings_dialog, ["large", "medium.en"])
with listed, downloaded:
assert dialog_class._downloaded_alternative_for(dialog, "small.en") == "medium.en"
def test_applying_the_recommendation_sets_both_pickers(
settings_dialog: Any, dialog_class: Any
) -> None:
"""Clicking must move size and specialization together, not just one."""
dialog = _dialog_stub()
dialog._recommended_target_model = "small.en"
dialog_class._on_apply_recommendation(dialog, None)
dialog.model_combo.set_active_id.assert_called_once_with("small")
dialog._populate_whispercpp_variant_options.assert_called_once_with("small", "small.en")
dialog.model_variant_combo.set_active_id.assert_called_once_with("small.en")
dialog._sync_language_options_for_selected_model.assert_called_once_with()
dialog._update_model_info.assert_called_once_with()
dialog._refresh_unused_downloads.assert_called_once_with()
dialog._auto_apply_settings.assert_called_once_with()
assert dialog._populating_models is False
def test_applying_does_nothing_without_a_target(settings_dialog: Any, dialog_class: Any) -> None:
dialog = _dialog_stub()
dialog._recommended_target_model = None
dialog_class._on_apply_recommendation(dialog, None)
dialog.model_combo.set_active_id.assert_not_called()
dialog._auto_apply_settings.assert_not_called()
def test_applying_recommendation_suppresses_model_changed_auto_apply(
settings_dialog: Any, dialog_class: Any
) -> None:
"""Size change must not briefly auto-apply the default size before the target.
Without ``_populating_models``, ``set_active_id(large)`` would fire
``_on_model_changed`` and download full large before turbo-q5_0 lands.
"""
dialog = _dialog_stub()
dialog._recommended_target_model = "large-v3-turbo-q5_0"
seen_while_setting_size: list[bool] = []
def capture_flag(_model_size: str) -> bool:
seen_while_setting_size.append(dialog._populating_models)
return True
dialog.model_combo.set_active_id.side_effect = capture_flag
dialog_class._on_apply_recommendation(dialog, None)
assert seen_while_setting_size == [True]
dialog._populate_whispercpp_variant_options.assert_called_once_with(
"large", "large-v3-turbo-q5_0"
)
dialog.model_variant_combo.set_active_id.assert_called_once_with("large-v3-turbo-q5_0")
# One apply after both pickers settle — not during the size change.
dialog._auto_apply_settings.assert_called_once_with()
assert dialog._populating_models is False
def test_language_change_refreshes_recommendation_ui(
settings_dialog: Any, dialog_class: Any
) -> None:
"""EN→PL must refresh recommendation target / label so Use it is not stale."""
dialog = _dialog_stub(language="en-us")
dialog.language_combo.get_active_id.return_value = "pl"
dialog.engine_combo.get_active_text.return_value = "whisper.cpp"
dialog._recommended_target_model = "small.en"
dialog_class._on_language_changed(dialog, None)
assert dialog.language == "pl"
dialog._populate_model_options.assert_called_once_with()
dialog._update_language_warning.assert_called_once_with()
dialog._update_model_info.assert_called_once_with()
dialog._auto_apply_settings.assert_called_once_with()
# Refresh happens before auto-apply so the card is current when settings land.
assert dialog.mock_calls.index(call._update_model_info()) < dialog.mock_calls.index(
call._auto_apply_settings()
)
assert dialog._processing_language_change is False