aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py227
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"]}