diff options
Diffstat (limited to 'app/tests/test_hub.py')
| -rw-r--r-- | app/tests/test_hub.py | 227 |
1 files changed, 195 insertions, 32 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 0e7c2bd..61f0da1 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -8,7 +8,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from backends import BackendStatus, ServerSpec +from backends import BackendInfo, BackendStatus, ServerSpec from tests.test_tui import FakeCurses, FakeScreen from ui import hub, tui @@ -38,7 +38,15 @@ class HubHelperTests(unittest.TestCase): def test_status_mark(self): from backends import BackendStatus running = BackendStatus("k", "l", installed=True, configured=True, - running=True) + running=True, managed=True) + remote = BackendStatus("k", "l", installed=True, configured=True, + running=True) + models = BackendStatus("k", "l", installed=True, configured=True, + running=True, managed=True, + running_models=["Base", "CustomVoice"]) + remote_models = BackendStatus("k", "l", installed=True, + configured=True, running=True, + running_models=["Base"]) installed = BackendStatus("k", "l", installed=True, configured=False) none = BackendStatus("k", "l", installed=False, configured=False) @@ -46,6 +54,14 @@ class HubHelperTests(unittest.TestCase): # only a backend that is neither installed nor running is dimmed. self.assertEqual(hub._status_mark(running), ("running", "ok", "body")) + # A server without a live recorded pid was started externally. + self.assertEqual(hub._status_mark(remote), + ("running [remote]", "ok", "body")) + # Multi-model backends name the models that answered. + self.assertEqual(hub._status_mark(models), + ("running (Base, CustomVoice)", "ok", "body")) + self.assertEqual(hub._status_mark(remote_models), + ("running [remote] (Base)", "ok", "body")) self.assertEqual(hub._status_mark(installed), ("installed", "warn", "body")) self.assertEqual(hub._status_mark(none), @@ -133,15 +149,16 @@ class HubMenuTests(unittest.TestCase): patch.object(hub, "detect_all", return_value=[dead, external]): hub._hub_menu(screen) - # Unusable backend: dim name. Running-but-not-installed stays bright. + # Unusable backend: dim name. Running-but-not-installed stays + # bright and is tagged remote (no pid file → not started by us). self.assertEqual( captured["rows"], [("audio.cpp", "unavailable", "err", "dim"), - ("qwen-tts", "running", "ok", "body")]) + ("qwen-tts", "running [remote]", "ok", "body")]) - def test_menu_has_all_six_when_one_running_only(self): - # Running but not installed (an external server) still unlocks the - # Convert/Configure/Server entries. + def test_menu_hides_configure_and_server_when_only_running(self): + # Running but not installed (an external server) still unlocks + # Convert — but Configure/Server need the backend on this machine. captured = {} def fake_menu(stdscr, title, options, **kwargs): @@ -157,9 +174,7 @@ class HubMenuTests(unittest.TestCase): labels = [label for label, _ in captured["options"]] self.assertEqual( labels, - ["Convert books", "Set up a backend", - "Configure a backend", "Start/Stop Backend Servers", - "Settings", "Quit"]) + ["Convert books", "Set up a backend", "Settings", "Quit"]) def test_ffmpeg_warning_shown_when_missing(self): # ffmpeg not on PATH → a red notice is passed above the table. @@ -192,37 +207,182 @@ class HubMenuTests(unittest.TestCase): hub._hub_menu(screen) self.assertIsNone(captured["notice_lines"]) - def test_convert_with_no_available_backend_offers_setup(self): - # One installed-but-not-ready backend → Convert is offered. The - # convert menu lists no available backend, so only "Set up a - # backend" is shown; Enter selects it → setup menu lists 3 - # backends; Esc goes back → convert returns None → main menu loops. - # Then quit: main menu now has 5 options, Quit is the 5th (Down x4). - from backends import BackendInfo, BackendStatus - none = BackendStatus("k", "l", installed=True, configured=False) - infos = [BackendInfo("audiocpp", "audio.cpp", lambda: none, - lambda: 0), - BackendInfo("qwen", "qwen-tts", lambda: none, lambda: 0), - BackendInfo("faster", "faster", lambda: none, lambda: 0)] - # installed=True so the main menu shows Convert; but ready/running - # is False so the convert menu's available list is empty. + def test_convert_with_no_available_backend_flashes(self): + # Installed-but-not-ready backends → Convert is offered, but the + # convert flow has nothing to list: it flashes a hint (no "Set up + # a backend" detour anymore) and returns to the main menu. Then + # quit: 6 main-menu options, Quit is the 6th (Down x5). + from backends import BackendStatus statuses = [BackendStatus("audiocpp", "audio.cpp", installed=True, configured=False), BackendStatus("qwen", "qwen-tts", installed=True, configured=False), BackendStatus("faster", "faster", installed=True, configured=False)] + flashed = [] + + def fake_flash(stdscr, text, kind="warn"): + flashed.append(text) + with patch.object(hub, "detect_all", return_value=statuses), \ - patch.object(hub, "REGISTRY", infos): - # Convert(Enter), setup-entry(Enter), Esc on setup menu, - # back at main menu -> Down x5 -> Enter (Quit; Settings sits - # just before it). - screen = FakeScreen(keys=[10, 10, 27, - FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, - FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, - FakeCurses.KEY_DOWN, 10]) + patch.object(hub.tui, "flash", fake_flash): + # Convert(Enter) → flash → main menu; Down x5 -> Quit, Enter. + screen = FakeScreen(keys=[10, + FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, + FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, + FakeCurses.KEY_DOWN, 10]) result = hub._hub_menu(screen) self.assertIsNone(result) + self.assertEqual(len(flashed), 1) + self.assertIn("No backend is ready", flashed[0]) + + +class SubmenuStatusTableTests(unittest.TestCase): + """First picker screen of every flow repeats the backend status table. + + Entries themselves stay clean: setup lists bare labels, and the + Start/Stop menu offers only installed backends. + """ + + def _capture_menu(self, captured): + def fake_menu(stdscr, title, options, **kwargs): + captured["title"] = title + captured["options"] = options + captured.update(kwargs) + return hub._GO_BACK # Esc: back out immediately + + return fake_menu + + def test_setup_menu_lists_bare_labels_and_status_table(self): + captured = {} + infos = [BackendInfo("audiocpp", "audio.cpp", lambda: None, lambda: 0), + BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)] + statuses = [ + BackendStatus("audiocpp", "audio.cpp", installed=True, + configured=True), + BackendStatus("qwen", "qwen-tts", installed=False, + configured=False, running=True), + ] + with patch.object(hub, "REGISTRY", infos), \ + patch.object(hub.tui, "menu", + self._capture_menu(captured)), \ + patch.object(hub.shutil, "which", + return_value="/usr/bin/ffmpeg"): + result = hub._setup_menu(None, statuses) + self.assertIsNone(result) + # No inline "(running)"-style suffix on the entries anymore... + self.assertEqual([label for label, _ in captured["options"]], + ["audio.cpp", "qwen-tts"]) + # ...the shared status table carries the states instead. + self.assertEqual(captured["table_title"], "Backend status") + self.assertEqual( + captured["table_rows"], + [("audio.cpp", "installed", "warn", "body"), + ("qwen-tts", "running [remote]", "ok", "body")]) + self.assertIsNone(captured["notice_lines"]) + + def test_convert_menu_shows_status_table(self): + captured = {} + st = BackendStatus("qwen", "qwen-tts", installed=True, + configured=True) + with patch.object(hub.tui, "menu", self._capture_menu(captured)), \ + patch.object(hub.shutil, "which", return_value="/x"): + result = hub._convert_menu(None, [st]) + self.assertIsNone(result) + self.assertEqual(captured["title"], "Convert books with...") + # Only convertible backends are listed — no "Set up a backend" + # detour inside the Convert flow. + self.assertEqual([label for label, _ in captured["options"]], + ["qwen-tts"]) + self.assertEqual( + captured["table_rows"], [("qwen-tts", "installed", "warn", + "body")]) + + def test_convert_with_nothing_ready_flashes_instead_of_menu(self): + # Installed-but-unconfigured → nothing convertible: a hint flash + # replaces the old fallback menu entirely. + flashed = [] + menus = [] + + def fake_menu(*args, **kwargs): + menus.append((args, kwargs)) + return hub._GO_BACK + + def fake_flash(stdscr, text, kind="warn"): + flashed.append(text) + + st = BackendStatus("qwen", "qwen-tts", installed=True, + configured=False) + with patch.object(hub.tui, "menu", fake_menu), \ + patch.object(hub.tui, "flash", fake_flash): + result = hub._convert_menu(None, [st]) + self.assertIsNone(result) + self.assertEqual(menus, []) + self.assertIn("No backend is ready", flashed[0]) + + def test_configure_menu_shows_status_table(self): + captured = {} + infos = [BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)] + statuses = [BackendStatus("qwen", "qwen-tts", installed=True, + configured=True)] + with patch.object(hub, "REGISTRY", infos), \ + patch.object(hub.tui, "menu", + self._capture_menu(captured)), \ + patch.object(hub.shutil, "which", return_value="/x"): + result = hub._configure_menu(None, statuses) + self.assertIsNone(result) + self.assertEqual(captured["table_title"], "Backend status") + self.assertEqual( + captured["table_rows"], [("qwen-tts", "installed", "warn", + "body")]) + + def test_server_menu_lists_only_installed_backends(self): + captured = {} + installed = BackendStatus("audiocpp", "audio.cpp", installed=True, + configured=True) + remote = BackendStatus("qwen", "qwen-tts", installed=False, + configured=False, running=True) + gone = BackendStatus("faster", "faster-qwen3-tts", installed=False, + configured=False) + with patch.object(hub.tui, "menu", + self._capture_menu(captured)), \ + patch.object(hub.shutil, "which", return_value="/x"): + result = hub._server_menu(None, [installed, remote, gone]) + self.assertIsNone(result) + # Only the installed backend is offered; a running external server + # (remote) can't be stopped from here and must not appear. + self.assertEqual([label for label, _ in captured["options"]], + ["audio.cpp"]) + # The status table still shows all three, states included. + self.assertEqual([row[0] for row in captured["table_rows"]], + ["audio.cpp", "qwen-tts", "faster-qwen3-tts"]) + + def test_server_menu_flashes_when_nothing_installed(self): + flashed = [] + + def fake_flash(stdscr, text, kind="warn"): + flashed.append(text) + + remote = BackendStatus("qwen", "qwen-tts", installed=False, + configured=False, running=True) + with patch.object(hub.tui, "flash", fake_flash): + result = hub._server_menu(None, [remote]) + self.assertIsNone(result) + self.assertEqual(len(flashed), 1) + self.assertIn("No backend is installed", flashed[0]) + + def test_submenu_repeats_ffmpeg_warning(self): + captured = {} + infos = [BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)] + statuses = [BackendStatus("qwen", "qwen-tts", installed=True, + configured=True)] + with patch.object(hub, "REGISTRY", infos), \ + patch.object(hub.tui, "menu", + self._capture_menu(captured)), \ + patch.object(hub.shutil, "which", return_value=None): + hub._setup_menu(None, statuses) + self.assertEqual(captured["notice_lines"], + [("Warning: ffmpeg not installed!", "err")]) class SelectSpecTests(unittest.TestCase): @@ -492,6 +652,9 @@ class SettingsTests(unittest.TestCase): self.assertEqual(kinds["audio_format"], "choice") self.assertEqual(kinds["audio_bitrate"], "text") self.assertEqual(kinds["audiocpp_port"], "text") + labels = {f["key"]: f["label"] for f in captured["fields"]} + self.assertEqual(labels["qwen_clone_port"], "qwen-tts Base port") + self.assertNotIn("(clone)", " ".join(labels.values())) # The ports section note hangs off the first port field so it # renders between the output settings and the ports. notes = {f["key"]: f.get("note") for f in captured["fields"]} |
