diff options
Diffstat (limited to 'app/tests/test_hub.py')
| -rw-r--r-- | app/tests/test_hub.py | 191 |
1 files changed, 139 insertions, 52 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index d244144..f40b72d 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -291,6 +291,15 @@ class SubmenuStatusTableTests(unittest.TestCase): return fake_menu + def _capture_form(self, captured): + def fake_form(stdscr, title, fields, **kwargs): + captured["title"] = title + captured["fields"] = fields + captured.update(kwargs) + return hub._GO_BACK # Cancel: back out immediately + + return fake_form + def test_setup_menu_lists_bare_labels_and_status_table(self): captured = {} infos = [BackendInfo("audiocpp", "audio.cpp", lambda: None, lambda: 0), @@ -319,22 +328,23 @@ class SubmenuStatusTableTests(unittest.TestCase): ("qwen-tts", "running [remote]", "ok", "body")]) self.assertIsNone(captured["notice_lines"]) - def test_convert_menu_shows_status_table(self): + def test_convert_menu_builds_one_form_with_backend_field(self): captured = {} st = BackendStatus("qwen", "qwen-tts", installed=True, configured=True) - with patch.object(hub.tui, "menu", self._capture_menu(captured)), \ + with patch.object(hub.tui, "form", + self._capture_form(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")]) + self.assertEqual(captured["title"], "Convert books") + # One form, no picker menu: the first field is the Backend picker, + # and only convertible backends are offered in it. + self.assertEqual(captured["fields"][0]["key"], "backend") + self.assertEqual(captured["fields"][0]["choices"], + [("qwen-tts", "qwen")]) + self.assertEqual(captured["buttons"], ("Generate!", "Cancel")) + self.assertTrue(captured["start_on_buttons"]) def test_convert_with_nothing_ready_flashes_instead_of_menu(self): # Installed-but-unconfigured → nothing convertible: a hint flash @@ -424,8 +434,8 @@ class SubmenuStatusTableTests(unittest.TestCase): class ConvertFlowTests(unittest.TestCase): - """_convert_audiocpp / _convert_qwen / _convert_faster: each backend - collects its settings on a single form (local config or live remote + """_convert_menu: one form whose first field is the Backend picker, + followed by that backend's options (local config or live remote queries).""" def setUp(self): @@ -449,6 +459,10 @@ class ConvertFlowTests(unittest.TestCase): _, fields, _ = self.tui.forms_seen[form_index] return next(f for f in fields if f["key"] == key) + def _ready(self, key, label): + """A backend status that is ready to convert with.""" + return BackendStatus(key, label, installed=True, configured=True) + # ------------------------------------------------------------------ # audio.cpp: remote server (no local checkout / server.json) # ------------------------------------------------------------------ @@ -472,9 +486,11 @@ class ConvertFlowTests(unittest.TestCase): [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], voices=["narrator"]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="higgs", voice="narrator", - instructions="", speed="1.5") - cmd = hub._convert_audiocpp(None) + self._answer_form(backend="audiocpp", model_id="higgs", + audiocpp_voice="narrator", instructions="", + speed="1.5") + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertEqual(cmd[0], "convert") self.assertEqual(cmd[1], hub.BACKEND_AUDIOCPP) kwargs = cmd[2] @@ -488,14 +504,17 @@ class ConvertFlowTests(unittest.TestCase): # One form, not a cascade of menus/editors. self.assertEqual(len(self.tui.forms_seen), 1) title, fields, form_kwargs = self.tui.forms_seen[0] - self.assertEqual(title, "Convert with audio.cpp") + self.assertEqual(title, "Convert books") self.assertEqual([f["key"] for f in fields], - ["model_id", "voice", "instructions", - "output_format", "speed", "single_file", "debug"]) + ["backend", "model_id", "audiocpp_voice", + "instructions", "output_format", "speed", + "single_file", "debug"]) self.assertEqual(form_kwargs["buttons"], ("Generate!", "Cancel")) self.assertTrue(form_kwargs["start_on_buttons"]) + # The backend field offers the ready backend. + self.assertEqual(fields[0]["choices"], [("audio.cpp", "audiocpp")]) # The model menu was fed from the live query (label, id). - self.assertEqual(fields[0]["choices"], + self.assertEqual(self._field("model_id")["choices"], [("higgs (higgs_audio_tts, tts)", "higgs")]) def test_audiocpp_qwen3_tts_voice_choices_lead_with_builtin_speaker(self): @@ -503,13 +522,15 @@ class ConvertFlowTests(unittest.TestCase): [{"id": "qwen", "family": "qwen3_tts", "task": "tts"}], voices=["narrator"]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="qwen", voice="(built-in speaker)", + self._answer_form(backend="audiocpp", model_id="qwen", + audiocpp_voice="(built-in speaker)", instructions="") - cmd = hub._convert_audiocpp(None) + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) # The sentinel maps to "no voice" (built-in speaker). self.assertIsNone(cmd[2]["voice"]) fields = self.tui.forms_seen[0][1] - voice_field = self._field("voice") + voice_field = self._field("audiocpp_voice") choices = voice_field["choices"](fields) self.assertEqual(choices, [("(built-in speaker)", "(built-in speaker)"), @@ -521,9 +542,11 @@ class ConvertFlowTests(unittest.TestCase): self._patch_remote([{"id": "legacy", "family": "", "task": ""}], voices=[]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="legacy", - voice="(built-in speaker)", instructions="") - cmd = hub._convert_audiocpp(None) + self._answer_form(backend="audiocpp", model_id="legacy", + audiocpp_voice="(built-in speaker)", + instructions="") + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertIsNotNone(cmd) self.assertIsNone(cmd[2]["voice"]) @@ -531,13 +554,15 @@ class ConvertFlowTests(unittest.TestCase): self._patch_remote( [{"id": "design", "family": "qwen3_tts", "task": "vdes"}]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="design", voice=None, + self._answer_form(backend="audiocpp", model_id="design", + audiocpp_voice=None, instructions="A warm British narrator") - cmd = hub._convert_audiocpp(None) + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertIsNone(cmd[2]["voice"]) self.assertEqual(cmd[2]["instructions"], "A warm British narrator") fields = self.tui.forms_seen[0][1] - voice_field = self._field("voice") + voice_field = self._field("audiocpp_voice") self.assertFalse(voice_field["visible"](fields)) instr = self._field("instructions") self.assertIsNotNone(instr["validate"]("")) @@ -549,22 +574,25 @@ class ConvertFlowTests(unittest.TestCase): [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], voices=["narrator"]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="higgs", voice="narrator", - instructions="") - hub._convert_audiocpp(None) - voice_field = self._field("voice") + self._answer_form(backend="audiocpp", model_id="higgs", + audiocpp_voice="narrator", instructions="") + hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) + voice_field = self._field("audiocpp_voice") self.assertIsNotNone(voice_field["validate"]("")) self.assertIsNone(voice_field["validate"]("narrator")) def test_audiocpp_remote_unreachable_models_flash_and_abort(self): self._patch_remote(None) # endpoint did not answer valid JSON - cmd = hub._convert_audiocpp(None) + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertIsNone(cmd) self.assertIn("Could not list models", self.tui.flashes[0]) def test_audiocpp_remote_empty_models_flash_and_abort(self): self._patch_remote([]) - cmd = hub._convert_audiocpp(None) + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertIsNone(cmd) self.assertIn("hosts no model entries", self.tui.flashes[0]) @@ -575,12 +603,14 @@ class ConvertFlowTests(unittest.TestCase): [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], voices=[]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="higgs", voice="", instructions="") - cmd = hub._convert_audiocpp(None) + self._answer_form(backend="audiocpp", model_id="higgs", + audiocpp_voice="", instructions="") + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertIsNotNone(cmd) self.assertIsNone(cmd[2]["voice"]) fields = self.tui.forms_seen[0][1] - voice_field = self._field("voice") + voice_field = self._field("audiocpp_voice") self.assertEqual(voice_field["choices"](fields), []) # ------------------------------------------------------------------ @@ -607,9 +637,10 @@ class ConvertFlowTests(unittest.TestCase): patch.object(hub.audiocpp_backend, "fetch_server_models", must_not_query), \ patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(model_id="qwen", voice="Narrator", - instructions="") - cmd = hub._convert_audiocpp(None) + self._answer_form(backend="audiocpp", model_id="qwen", + audiocpp_voice="Narrator", instructions="") + cmd = hub._convert_menu(None, + [self._ready("audiocpp", "audio.cpp")]) self.assertEqual(queried, []) self.assertIsNotNone(cmd) self.assertEqual(cmd[2]["model_id"], "qwen") @@ -636,9 +667,11 @@ class ConvertFlowTests(unittest.TestCase): with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian", "Serena"]), \ patch.object(hub.config, "SPEAKER", "Vivian"): - self._answer_form(mode="custom", speaker="Serena", clone="") + self._answer_form(backend="qwen", mode="custom", speaker="Serena", + clone="") with patch.object(hub.common, "update_config_value") as mk_update: - cmd = hub._convert_qwen(None) + cmd = hub._convert_menu(None, + [self._ready("qwen", "qwen-tts")]) speaker_in_memory = hub.config.SPEAKER self.assertEqual(cmd[0], "convert") self.assertEqual(cmd[1], hub.BACKEND_QWEN) @@ -648,8 +681,8 @@ class ConvertFlowTests(unittest.TestCase): self.assertEqual(speaker_in_memory, "Serena") fields = self.tui.forms_seen[0][1] self.assertEqual([f["key"] for f in fields], - ["mode", "speaker", "clone", "output_format", - "speed", "single_file", "debug"]) + ["backend", "mode", "speaker", "clone", + "output_format", "speed", "single_file", "debug"]) mode_field = self._field("mode") self.assertEqual(mode_field["choices"], [("Built-in speaker", "custom"), @@ -666,10 +699,11 @@ class ConvertFlowTests(unittest.TestCase): def test_qwen_clone_mode_passes_path_and_keeps_speaker(self): with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \ patch.object(hub.config, "SPEAKER", "Vivian"): - self._answer_form(mode="clone", speaker="Vivian", + self._answer_form(backend="qwen", mode="clone", speaker="Vivian", clone="/tmp/ref.wav") with patch.object(hub.common, "update_config_value") as mk_update: - cmd = hub._convert_qwen(None) + cmd = hub._convert_menu(None, + [self._ready("qwen", "qwen-tts")]) self.assertEqual(cmd[2]["clone"], "/tmp/ref.wav") # Clone mode does not touch the global speaker. mk_update.assert_not_called() @@ -682,12 +716,13 @@ class ConvertFlowTests(unittest.TestCase): with tempfile.TemporaryDirectory() as td: with patch.object(hub.faster_backend, "_checkout", return_value=Path(td)): - self._answer_form(voice="obama") - cmd = hub._convert_faster(None) + self._answer_form(backend="faster", faster_voice="obama") + cmd = hub._convert_menu( + None, [self._ready("faster", "faster-qwen3-tts")]) self.assertEqual(cmd[0], "convert") self.assertEqual(cmd[1], "faster") self.assertEqual(cmd[2]["voice"], "obama") - self.assertEqual(self._field("voice")["kind"], "text") + self.assertEqual(self._field("faster_voice")["kind"], "text") def test_faster_local_still_lists_voices_json(self): with tempfile.TemporaryDirectory() as td: @@ -696,14 +731,66 @@ class ConvertFlowTests(unittest.TestCase): json.dumps({"default": {}, "obama": {}}), encoding="utf-8") with patch.object(hub.faster_backend, "_checkout", return_value=checkout): - self._answer_form(voice="obama") - cmd = hub._convert_faster(None) + self._answer_form(backend="faster", faster_voice="obama") + cmd = hub._convert_menu( + None, [self._ready("faster", "faster-qwen3-tts")]) self.assertEqual(cmd[2]["voice"], "obama") - voice_field = self._field("voice") + voice_field = self._field("faster_voice") self.assertEqual(voice_field["kind"], "choice") self.assertEqual(voice_field["choices"], [("default", "default"), ("obama", "obama")]) + # ------------------------------------------------------------------ + # multiple backends: the Backend picker gates which options show + # ------------------------------------------------------------------ + + def test_multiple_backends_gate_options_on_backend_value(self): + # Two ready backends: the form leads with a Backend picker and the + # per-backend fields are hidden/shown by its value. + self._patch_remote( + [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], + voices=["narrator"]) + with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): + self._answer_form(backend="qwen", mode="custom", speaker="Vivian", + clone="") + cmd = hub._convert_menu(None, [ + self._ready("audiocpp", "audio.cpp"), + self._ready("qwen", "qwen-tts")]) + self.assertEqual(cmd[0], "convert") + self.assertEqual(cmd[1], hub.BACKEND_QWEN) + fields = self.tui.forms_seen[0][1] + self.assertEqual(fields[0]["key"], "backend") + self.assertEqual(fields[0]["choices"], + [("audio.cpp", "audiocpp"), ("qwen-tts", "qwen")]) + self.assertEqual( + [f["key"] for f in fields], + ["backend", "model_id", "audiocpp_voice", "instructions", + "mode", "speaker", "clone", "output_format", "speed", + "single_file", "debug"]) + # The form opens on the configured default (audio.cpp): its fields + # show, the other backend's hide. + for key in ("model_id", "audiocpp_voice", "instructions"): + self.assertTrue(self._field(key)["visible"](fields)) + for key in ("mode", "speaker", "clone"): + self.assertFalse(self._field(key)["visible"](fields)) + # Picking qwen in the Backend field swaps which options show. + fields[0]["value"] = "qwen" + self.assertTrue(self._field("mode")["visible"](fields)) + self.assertTrue(self._field("speaker")["visible"](fields)) + self.assertFalse(self._field("clone")["visible"](fields)) + # qwen's clone mode hides the speaker and shows the .wav path. + self._field("mode")["value"] = "clone" + self.assertFalse(self._field("speaker")["visible"](fields)) + self.assertTrue(self._field("clone")["visible"](fields)) + for key in ("model_id", "audiocpp_voice", "instructions"): + self.assertFalse(self._field(key)["visible"](fields)) + # And back to audio.cpp. + fields[0]["value"] = "audiocpp" + for key in ("model_id", "audiocpp_voice", "instructions"): + self.assertTrue(self._field(key)["visible"](fields)) + for key in ("mode", "speaker", "clone"): + self.assertFalse(self._field(key)["visible"](fields)) + class SelectSpecTests(unittest.TestCase): """_select_spec: mode-aware server selection (qwen has two servers).""" |
