from __future__ import annotations
import importlib
import sys
import types
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from helpers import plugins, settings
def test_builtin_speech_plugins_are_discoverable_and_toggleable() -> None:
discovered = {
item.name: item
for item in plugins.get_enhanced_plugins_list(
custom=True,
builtin=True,
plugin_names=["_kokoro_tts", "_whisper_stt"],
)
}
assert "_kokoro_tts" in discovered
assert "_whisper_stt" in discovered
assert discovered["_kokoro_tts"].always_enabled is False
assert discovered["_whisper_stt"].always_enabled is False
assert "agent" in discovered["_kokoro_tts"].settings_sections
assert "agent" in discovered["_whisper_stt"].settings_sections
def test_legacy_core_speech_artifacts_are_removed() -> None:
removed_paths = [
"api/synthesize.py",
"api/transcribe.py",
"helpers/kokoro_tts.py",
"helpers/whisper.py",
"webui/components/chat/speech/speech-store.js",
"webui/components/settings/agent/speech.html",
"webui/components/settings/speech/microphone-setting-store.js",
"webui/components/settings/speech/microphone.html",
"webui/css/speech.css",
"webui/js/speech_browser.js",
]
for relative_path in removed_paths:
assert not (PROJECT_ROOT / relative_path).exists(), relative_path
def test_plugin_owned_voice_files_exist() -> None:
expected_paths = [
"plugins/_kokoro_tts/plugin.yaml",
"plugins/_kokoro_tts/api/synthesize.py",
"plugins/_kokoro_tts/extensions/webui/page-head/runtime.html",
"plugins/_kokoro_tts/extensions/webui/voice-settings-main/kokoro-card.html",
"plugins/_whisper_stt/plugin.yaml",
"plugins/_whisper_stt/api/transcribe.py",
"plugins/_whisper_stt/extensions/webui/page-head/runtime.html",
"plugins/_whisper_stt/extensions/webui/chat-input-box-end/microphone-button.html",
"plugins/_whisper_stt/extensions/webui/voice-settings-main/whisper-card.html",
"plugins/_whisper_stt/webui/whisper-stt-store.js",
]
for relative_path in expected_paths:
assert (PROJECT_ROOT / relative_path).exists(), relative_path
def test_core_settings_no_longer_expose_legacy_speech_keys() -> None:
defaults = settings.get_default_settings()
output = settings.convert_out(defaults)
legacy_keys = {
"tts_kokoro",
"stt_model_size",
"stt_language",
"stt_silence_threshold",
"stt_silence_duration",
"stt_waiting_timeout",
}
assert legacy_keys.isdisjoint(defaults.keys())
assert legacy_keys.isdisjoint(output["settings"].keys())
assert "stt_models" not in output["additional"]
def test_voice_prefix_prompt_rule_is_removed() -> None:
core_prompt = (PROJECT_ROOT / "prompts/agent.system.main.communication_additions.md").read_text(
encoding="utf-8"
)
whisper_store = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
).read_text(encoding="utf-8")
voice_surface = (PROJECT_ROOT / "webui/components/settings/agent/voice.html").read_text(
encoding="utf-8"
)
assert "if starts (voice) then transcribed can contain errors consider compensation" not in core_prompt
assert "(voice)" not in whisper_store
assert not (
PROJECT_ROOT / "plugins/_whisper_stt/prompts/agent.system.voice_transcription.md"
).exists()
assert not (
PROJECT_ROOT
/ "plugins/_whisper_stt/extensions/python/system_prompt/_20_voice_transcription.py"
).exists()
assert '' in voice_surface
assert '' in voice_surface
assert '' in voice_surface
def test_whisper_message_mode_defaults_to_send_and_supports_draft() -> None:
sys.modules.setdefault(
"whisper",
types.SimpleNamespace(load_model=lambda *args, **kwargs: None),
)
runtime = importlib.import_module("plugins._whisper_stt.helpers.runtime")
assert runtime.normalize_config({})["message_mode"] == "send"
assert runtime.normalize_config({"message_mode": "draft"})["message_mode"] == "draft"
assert runtime.normalize_config({"message_mode": "DRAFT"})["message_mode"] == "draft"
assert runtime.normalize_config({"message_mode": "invalid"})["message_mode"] == "send"
default_config = (
PROJECT_ROOT / "plugins/_whisper_stt/default_config.yaml"
).read_text(encoding="utf-8")
migration = (
PROJECT_ROOT / "plugins/_whisper_stt/helpers/migration.py"
).read_text(encoding="utf-8")
config_ui = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/config.html"
).read_text(encoding="utf-8")
status_ui = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/main.html"
).read_text(encoding="utf-8")
voice_card = (
PROJECT_ROOT
/ "plugins/_whisper_stt/extensions/webui/voice-settings-main/whisper-card.html"
).read_text(encoding="utf-8")
whisper_store = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
).read_text(encoding="utf-8")
assert "message_mode: send" in default_config
assert '"message_mode": "send"' in migration
assert '' in config_ui
assert '' in config_ui
assert "messageModeLabel" in status_ui
assert "messageModeLabel" in voice_card
assert 'message_mode: "send"' in whisper_store
assert 'status?.config?.message_mode === "draft" ? "draft" : "send"' in whisper_store
assert "updateChatInput(message)" in whisper_store
assert "sendMessage()" in whisper_store
def test_browser_tool_speech_action_uses_shared_tts_service() -> None:
browser_handler = (
PROJECT_ROOT
/ "plugins/_browser/extensions/webui/get_tool_message_handler/browser-tool-handler.js"
).read_text(encoding="utf-8")
assert "/components/chat/speech/speech-store.js" not in browser_handler
assert "/js/tts-service.js" in browser_handler
assert "ttsService.speak(contentText)" in browser_handler
def test_chat_bar_keeps_existing_send_and_mic_icon_contract() -> None:
chat_bar = (
PROJECT_ROOT / "webui/components/chat/input/chat-bar-input.html"
).read_text(encoding="utf-8")
mic_extension = (
PROJECT_ROOT
/ "plugins/_whisper_stt/extensions/webui/chat-input-box-end/microphone-button.html"
).read_text(encoding="utf-8")
whisper_store = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
).read_text(encoding="utf-8")
whisper_css = (
PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt.css"
).read_text(encoding="utf-8")
assert 'id="send-button"' in chat_bar
assert 'x-text="$store.chatInput.sendButtonIcon"' in chat_bar
assert ':class="$store.chatInput.sendButtonClass"' in chat_bar
assert ':title="$store.chatInput.sendButtonTitle"' in chat_bar
assert 'id="microphone-button"' in mic_extension
assert "