aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-26 20:43:05 -0400
committerhistoria <historiavg@proton.me>2026-08-26 20:43:05 -0400
commit65c6f737f1545ef225768af897acd20f163a4fb4 (patch)
tree9c0974944191e50b9caa90591dfcb7f97c659770 /app/tests/test_hub.py
parent975bd960fd07e75799b8e3adc4c0033046b34792 (diff)
downloadtts-audiobook-generator-65c6f737f1545ef225768af897acd20f163a4fb4.tar.gz
fix: settings menu only prompts to save after change
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py171
1 files changed, 167 insertions, 4 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index 7e77642..48cef0b 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -1083,6 +1083,116 @@ class ConvertFlowTests(unittest.TestCase):
# back to no options instead of crashing the mapper.
self.assertEqual(cmd[2]["request_options"], {})
+ # ------------------------------------------------------------------
+ # audio.cpp: Request options gated by model_specs option support
+ # ------------------------------------------------------------------
+
+ def _specs_checkout(self, families_with_options=()):
+ """A fake checkout whose specs mark FAMILIES_WITH_OPTIONS supportive."""
+ root = Path(self.enterContext(tempfile.TemporaryDirectory()))
+ specs = root / "model_specs"
+ specs.mkdir()
+ for family in ("higgs_audio_tts", "qwen3_tts"):
+ request = ([{"id": "temperature"}]
+ if family in families_with_options else [])
+ spec = {"family": family, "display_name": family.title(),
+ "packages": [{"id": f"{family}_q8_0", "default": True,
+ "format": "gguf",
+ "target_directory": f"{family}-GGUF"}]}
+ if request:
+ spec["options"] = {"request": request}
+ (specs / f"{family}.json").write_text(json.dumps(spec),
+ encoding="utf-8")
+ return root
+
+ def test_options_field_visible_when_spec_proves_support(self):
+ self._patch_remote(
+ [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
+ voices=["narrator"])
+ with patch.object(hub.audiocpp_backend, "find_local_checkout",
+ return_value=self._specs_checkout(
+ ("higgs_audio_tts",))), \
+ patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
+ fields = self.tui.forms_seen[0][1]
+ self.assertTrue(self._field("request_options")["visible"](fields))
+ self.assertIsNotNone(cmd)
+
+ def test_options_field_hidden_when_spec_lacks_the_family(self):
+ self._patch_remote(
+ [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
+ voices=["narrator"])
+ # A checkout exists but only qwen3_tts declares request options:
+ # higgs is provably unsupported -> hidden.
+ with patch.object(hub.audiocpp_backend, "find_local_checkout",
+ return_value=self._specs_checkout(
+ ("qwen3_tts",))), \
+ patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
+ fields = self.tui.forms_seen[0][1]
+ self.assertFalse(self._field("request_options")["visible"](fields))
+
+ def test_options_field_hidden_without_a_local_checkout(self):
+ # Unknown support (no specs anywhere) hides the field — strict.
+ self._patch_remote(
+ [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
+ voices=["narrator"])
+ with patch.object(hub.audiocpp_backend, "find_local_checkout",
+ return_value=None), \
+ patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
+ fields = self.tui.forms_seen[0][1]
+ self.assertFalse(self._field("request_options")["visible"](fields))
+
+ def test_instructions_help_is_short_and_shared(self):
+ # One compact static help text for every capability: two lines,
+ # naming style instructions, partial clone-model support, and an
+ # example. (Design entries enforce their requirement by validation.)
+ self._patch_remote([
+ {"id": "design", "family": "qwen3_tts", "task": "vdes"},
+ {"id": "higgs", "family": "higgs_audio_tts", "task": "tts"},
+ ], voices=["narrator"])
+ with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ self._answer_form(backend="audiocpp-remote", model_id="design",
+ audiocpp_voice=None,
+ instructions="A warm British narrator")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
+ instr = self._field("instructions")
+ self.assertEqual(instr["help"], [
+ "TTS style instructions. Supported by some clone models. Example:",
+ '"Speak in a calm, soothing, and happy tone."',
+ ])
+
+ def test_options_help_is_two_lines_with_examples(self):
+ self._patch_remote(
+ [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
+ voices=["narrator"])
+ with patch.object(hub.audiocpp_backend, "find_local_checkout",
+ return_value=self._specs_checkout(
+ ("qwen3_tts", "higgs_audio_tts"))), \
+ patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
+ options = self._field("request_options")
+ fields = self.tui.forms_seen[0][1]
+ self.assertTrue(options["visible"](fields))
+ self.assertEqual(len(options["help"]), 2)
+ help_text = "\n".join(options["help"])
+ self.assertIn("KEY=VALUE", help_text)
+ self.assertIn("emotion=neutral", help_text)
+
def test_language_passes_through_normalized(self):
with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
patch.object(hub.config, "SPEAKER", "Vivian"):
@@ -1453,11 +1563,13 @@ class ConvertFlowTests(unittest.TestCase):
"output_format", "language", "speed",
"single_file", "debug", "stop_and_exit"])
# The form opens on the configured default (audio.cpp): its fields
- # show, the other backend's hide. (Instructions shows too: optional
- # style/delivery control even on the clone-only higgs entry.)
- for key in ("model_id", "audiocpp_voice", "instructions",
- "request_options"):
+ # show, the other backend's hide. Instructions shows too (optional
+ # style/delivery control even on the clone-only higgs entry), while
+ # Request options stays hidden — higgs has no option-supporting
+ # spec on this machine's checkout, so its support is unknown.
+ for key in ("model_id", "audiocpp_voice", "instructions"):
self.assertTrue(self._field(key)["visible"](fields))
+ self.assertFalse(self._field("request_options")["visible"](fields))
# Language shows for every backend except faster entries.
self.assertTrue(self._field("language")["visible"](fields))
for key in ("mode", "speaker", "clone"):
@@ -1976,7 +2088,10 @@ class SettingsTests(unittest.TestCase):
self.assertNotIn("flash", captured)
def test_settings_menu_cancel_does_not_apply(self):
+ # An actual edit triggers the save prompt; "no" discards it.
def fake_form(stdscr, title, fields, back_value=None):
+ next(f for f in fields
+ if f["key"] == "chunk_size")["value"] = "300"
return back_value # user pressed Cancel / q / Esc
applied = []
@@ -1992,6 +2107,54 @@ class SettingsTests(unittest.TestCase):
mk_prompt.assert_called_once_with(None, "Save settings?")
self.assertEqual(applied, [])
+ def test_settings_menu_exit_without_changes_skips_prompt(self):
+ # Leaving with untouched fields never asks about saving.
+ def fake_form(stdscr, title, fields, back_value=None):
+ return back_value # user pressed Cancel / q / Esc
+
+ applied = []
+
+ with patch.object(hub.tui, "form", fake_form), \
+ patch.object(hub.tui, "confirm_yn_cancel") as mk_prompt, \
+ patch.object(hub, "_apply_settings",
+ lambda values: applied.append(values)):
+ hub._Hub(None).screen_settings()
+ mk_prompt.assert_not_called()
+ self.assertEqual(applied, [])
+
+ def test_settings_menu_reverted_edit_skips_the_prompt(self):
+ # Typing a value and typing it back leaves nothing to save.
+ def fake_form(stdscr, title, fields, back_value=None):
+ field = next(f for f in fields if f["key"] == "chunk_size")
+ untouched = field["value"]
+ field["value"] = "300"
+ field["value"] = untouched
+ return back_value
+
+ applied = []
+
+ with patch.object(hub.tui, "form", fake_form), \
+ patch.object(hub.tui, "confirm_yn_cancel") as mk_prompt, \
+ patch.object(hub, "_apply_settings",
+ lambda values: applied.append(values)):
+ hub._Hub(None).screen_settings()
+ mk_prompt.assert_not_called()
+ self.assertEqual(applied, [])
+
+ def test_settings_menu_whitespace_edit_skips_the_prompt(self):
+ # Surrounding whitespace alone is not a change: _apply_settings
+ # trims text values, so saving would be a no-op.
+ def fake_form(stdscr, title, fields, back_value=None):
+ field = next(f for f in fields if f["key"] == "language")
+ field["value"] = " " + field["value"] + " "
+ return back_value
+
+ with patch.object(hub.tui, "form", fake_form), \
+ patch.object(hub.tui, "confirm_yn_cancel") as mk_prompt, \
+ patch.object(hub, "_apply_settings", lambda values: None):
+ hub._Hub(None).screen_settings()
+ mk_prompt.assert_not_called()
+
def test_settings_menu_exit_yes_applies_the_edited_fields(self):
# Leaving via Esc and answering Yes applies a values dict built
# from the (edited) field list.