aboutsummaryrefslogtreecommitdiff
path: root/app/tests
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests')
-rw-r--r--app/tests/test_backends_audiocpp.py73
-rw-r--r--app/tests/test_hub.py171
-rw-r--r--app/tests/test_tui.py26
3 files changed, 266 insertions, 4 deletions
diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index d364c06..ab405aa 100644
--- a/app/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
@@ -406,6 +406,18 @@ class FindLocalCheckoutTests(unittest.TestCase):
self.assertIsNone(make_server.build.find_local_checkout())
+def _add_options_to_spec(checkout: Path, family: str, *,
+ options=None) -> None:
+ """Rewrite one family spec with an (optional) options block."""
+ path = checkout / "model_specs" / f"{family}.json"
+ spec = json.loads(path.read_text(encoding="utf-8"))
+ if options is not None:
+ spec["options"] = options
+ elif "options" in spec:
+ del spec["options"]
+ path.write_text(json.dumps(spec), encoding="utf-8")
+
+
class LoadModelCatalogTests(unittest.TestCase):
def setUp(self):
self._td = tempfile.TemporaryDirectory()
@@ -468,6 +480,67 @@ class LoadModelCatalogTests(unittest.TestCase):
make_server.catalog.load_model_catalog(empty)
+class RequestOptionsFamiliesTests(unittest.TestCase):
+ """request_options_families: which specs declare request options."""
+
+ def setUp(self):
+ self._td = tempfile.TemporaryDirectory()
+ self.checkout = _make_checkout(Path(self._td.name))
+ _add_options_to_spec(
+ self.checkout, "higgs_audio_tts",
+ options={"request": [{"id": "temperature", "default": 0.8},
+ {"id": "speed"}]})
+
+ def tearDown(self):
+ self._td.cleanup()
+
+ def test_family_with_request_options_listed_with_display_name(self):
+ families = make_server.request_options_families(self.checkout)
+ self.assertEqual(families.get("higgs_audio_tts"),
+ {"display_name": "Higgs Audio v3 TTS 4B"})
+
+ def test_family_without_options_block_absent(self):
+ families = make_server.request_options_families(self.checkout)
+ self.assertNotIn("qwen3_tts", families)
+ self.assertNotIn("voxcpm2", families)
+
+ def test_empty_request_list_does_not_count_as_support(self):
+ _add_options_to_spec(self.checkout, "supertonic",
+ options={"request": []})
+ families = make_server.request_options_families(self.checkout)
+ self.assertNotIn("supertonic", families)
+
+ def test_missing_specs_dir_yields_empty_map(self):
+ self.assertEqual(make_server.request_options_families(
+ Path(self._td.name)), {})
+
+ def test_unparsable_spec_skipped(self):
+ (self.checkout / "model_specs" / "broken.json").write_text(
+ "{not json", encoding="utf-8")
+ families = make_server.request_options_families(self.checkout)
+ self.assertNotIn("broken", families)
+ self.assertIn("higgs_audio_tts", families)
+
+
+class SupportsRequestOptionsTests(unittest.TestCase):
+ """supports_request_options: True / False / unknown tri-state."""
+
+ FAMILIES = {"higgs_audio_tts": {"display_name": "Higgs"}}
+
+ def test_true_only_for_a_listed_family(self):
+ self.assertTrue(make_server.supports_request_options(
+ self.FAMILIES, "higgs_audio_tts"))
+
+ def test_false_for_a_read_but_unlisted_family(self):
+ self.assertFalse(make_server.supports_request_options(
+ self.FAMILIES, "qwen3_tts"))
+
+ def test_none_when_no_local_specs_exist(self):
+ self.assertIsNone(make_server.supports_request_options({}, "any"))
+ # An entry with no family at all is unclassifiable too.
+ self.assertIsNone(make_server.supports_request_options({}, ""))
+
+
class DetectBackendTests(unittest.TestCase):
"""Backend detection from audio.cpp build directory names."""
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.
diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py
index cd4e535..c836178 100644
--- a/app/tests/test_tui.py
+++ b/app/tests/test_tui.py
@@ -662,6 +662,32 @@ class FormTests(TuiTestCase):
if "Voice of base" in text]
self.assertTrue(titles)
+ def test_help_lines_render_inside_the_edit_dialog(self):
+ # A field's optional "help" list appears as dim lines in its line
+ # editor (and callables resolve against the field list).
+ fields = [
+ {"key": "opt", "kind": "text", "value": "",
+ "label": "Options",
+ "help": lambda fs: ["First hint.", f"Value: {fs[0]['value']!r}"]},
+ {"key": "plain", "label": "Plain", "kind": "text",
+ "value": "", "help": ["Static hint."]},
+ ]
+ # Down to the second field, Enter opens it, Esc backs out; Tab ->
+ # Save, Enter. The first field's editor is never opened, so only
+ # the static second field's hints must appear.
+ screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, 10, 27, 9, 10])
+ tui.form(screen, "Settings", fields)
+ texts = [text for _, _, text, _ in screen.strings]
+ self.assertIn("Static hint.", texts)
+ self.assertNotIn("First hint.", texts)
+ # Open the first field's editor: dynamic help resolves per redraw.
+ screen = FakeScreen(keys=[10, 27, 9, 10])
+ tui.form(screen, "Settings", fields)
+ texts = [text for _, _, text, _ in screen.strings]
+ self.assertIn("First hint.", texts)
+ self.assertIn("Value: ''", texts)
+ self.assert_inside_border(screen)
+
def test_field_note_renders_and_save(self):
fields = self._fields()
fields[1]["note"] = "A short section note"