mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Merge master: Groq, and cleanup on a subscription
Three of master's changes land on the same lines as this branch, so most of this is picking the newer shape and putting the local half back into it. cleanup.py arrived while this was being written and is the right place for a fourth provider, so the Target refactor of api.cleanup goes away: llama.cpp becomes a name in cleanup.PROVIDERS next to OpenRouter, Claude Code and Codex, and worker.py, meeting.py and filetranscribe.py go back to master's. The settings window keeps master's one row per provider, hidden with setRowVisible, rather than the two wrapper widgets this branch had. So does speech to text, which was doing the same thing its own way. The transcriber table has no room for a provider with no key and no base URL, so the local one is answered before the lookup rather than added to it, and an unknown name now falls back to openai by name: the shipped default is no longer a key of that table. The minutes stay on OpenRouter, which master already decided by routing only the transcript through cleanup.run, so meeting_provider goes.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ atexit.register(shutil.rmtree, _SANDBOX, True)
|
||||
# A key sitting in the environment would otherwise reach the code that falls
|
||||
# back to it, and the tests for "there is no key" would pass only on a machine
|
||||
# without one.
|
||||
for _var in ("OPENAI_API_KEY", "OPENROUTER_API_KEY"):
|
||||
for _var in ("OPENAI_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY"):
|
||||
os.environ.pop(_var, None)
|
||||
|
||||
# The interface language leaks through module-level state, so the tests fix it
|
||||
|
||||
+57
-86
@@ -22,24 +22,29 @@ from tests.support import (
|
||||
)
|
||||
|
||||
OPENAI = api.Target("openai", "OpenAI", "sk-test", api.OPENAI_URL, "gpt-4o-transcribe")
|
||||
GROQ = api.Target("groq", "Groq", "gsk-test", api.GROQ_URL, "whisper-large-v3-turbo")
|
||||
OPENROUTER = api.Target("openrouter", "OpenRouter", "sk-or-test",
|
||||
api.OPENROUTER_URL, "openai/gpt-4o-transcribe")
|
||||
|
||||
|
||||
class TimestampModel(unittest.TestCase):
|
||||
def test_only_whisper_returns_segment_times(self):
|
||||
self.assertEqual(api.timestamp_model("openai", "gpt-4o-transcribe"),
|
||||
"whisper-1")
|
||||
self.assertEqual(api.timestamp_model("openai"), "whisper-1")
|
||||
|
||||
def test_openrouter_namespaces_the_id(self):
|
||||
self.assertEqual(api.timestamp_model("openrouter", "openai/gpt-4o-transcribe"),
|
||||
"openai/whisper-1")
|
||||
self.assertEqual(api.timestamp_model("openrouter"), "openai/whisper-1")
|
||||
|
||||
def test_the_local_server_stays_on_the_model_it_loaded(self):
|
||||
# Asking it for whisper-1 would name a model it has never heard of, and
|
||||
# it is running whisper whatever the file is called.
|
||||
self.assertEqual(api.timestamp_model("local", "ggml-base.bin"),
|
||||
"ggml-base.bin")
|
||||
def test_groq_keeps_the_model_that_was_chosen(self):
|
||||
"""Every model it transcribes with is a whisper, so all of them do times."""
|
||||
self.assertEqual(api.timestamp_model("groq", "whisper-large-v3"),
|
||||
"whisper-large-v3")
|
||||
|
||||
def test_groq_with_nothing_chosen_falls_back(self):
|
||||
self.assertEqual(api.timestamp_model("groq"), "whisper-large-v3-turbo")
|
||||
|
||||
def test_the_others_ignore_what_was_chosen(self):
|
||||
self.assertEqual(api.timestamp_model("openai", "gpt-4o-transcribe"),
|
||||
"whisper-1")
|
||||
|
||||
|
||||
class Explain(DikteTest):
|
||||
@@ -185,13 +190,28 @@ class Transcribe(DikteTest):
|
||||
self.assertEqual(multipart_fields(calls[0])["language"], "tr")
|
||||
self.assertNotIn("language", multipart_fields(calls[1]))
|
||||
|
||||
def test_the_glossary_goes_to_openai_only(self):
|
||||
def test_the_glossary_goes_everywhere_but_openrouter(self):
|
||||
"""OpenRouter takes the field and throws it away, so spare it the bytes."""
|
||||
with fake_urlopen({"text": "hi"}) as calls:
|
||||
api.transcribe(OPENAI, self.wav, prompt="Paraşüt, OpenFrame")
|
||||
api.transcribe(GROQ, self.wav, prompt="Paraşüt, OpenFrame")
|
||||
api.transcribe(OPENROUTER, self.wav, prompt="Paraşüt, OpenFrame")
|
||||
self.assertIn("prompt", multipart_fields(calls[0]))
|
||||
self.assertNotIn("prompt", multipart_fields(calls[1]))
|
||||
self.assertIn("prompt", multipart_fields(calls[1]))
|
||||
self.assertNotIn("prompt", multipart_fields(calls[2]))
|
||||
|
||||
def test_groq_goes_to_groq(self):
|
||||
with fake_urlopen({"text": "hi"}) as calls:
|
||||
api.transcribe(GROQ, self.wav)
|
||||
self.assertEqual(calls[0].full_url,
|
||||
"https://api.groq.com/openai/v1/audio/transcriptions")
|
||||
self.assertEqual(multipart_fields(calls[0])["model"], "whisper-large-v3-turbo")
|
||||
|
||||
def test_a_refused_groq_key_is_explained_in_groq_s_name(self):
|
||||
with fake_urlopen(http_error(401, '{"error": {"message": "bad key"}}')), \
|
||||
self.assertRaises(api.ApiError) as caught:
|
||||
api.transcribe(GROQ, self.wav)
|
||||
self.assertIn("Groq", str(caught.exception))
|
||||
|
||||
def test_openrouter_is_attributed(self):
|
||||
with fake_urlopen({"text": "hi"}) as calls:
|
||||
@@ -251,6 +271,12 @@ class TranscribeSegments(DikteTest):
|
||||
api.transcribe_segments(OPENROUTER, self.wav)
|
||||
self.assertEqual(multipart_fields(calls[0])["model"], "openai/whisper-1")
|
||||
|
||||
def test_groq_stays_on_the_model_it_was_given(self):
|
||||
target = GROQ._replace(model="whisper-large-v3")
|
||||
with fake_urlopen(self.reply([{"start": 0, "end": 1, "text": "hi"}])) as calls:
|
||||
api.transcribe_segments(target, self.wav)
|
||||
self.assertEqual(multipart_fields(calls[0])["model"], "whisper-large-v3")
|
||||
|
||||
def test_the_segments_come_back_as_numbers(self):
|
||||
with fake_urlopen(self.reply([
|
||||
{"start": "0.5", "end": "2.25", "text": " hello "},
|
||||
@@ -287,15 +313,10 @@ def chat_reply(content):
|
||||
return {"choices": [{"message": {"content": content}}]}
|
||||
|
||||
|
||||
def openrouter(model="some/model", key="sk-or-test", reasoning="",
|
||||
base_url="https://openrouter.ai/api/v1"):
|
||||
return api.Target("openrouter", "OpenRouter", key, base_url, model, reasoning)
|
||||
|
||||
|
||||
class Cleanup(DikteTest):
|
||||
def call(self, replies, target=None, **kwargs):
|
||||
def call(self, replies, **kwargs):
|
||||
with fake_urlopen(replies) as calls:
|
||||
result = api.cleanup(target or openrouter(), "uh, hello",
|
||||
result = api.cleanup("uh, hello", "sk-or-test", "some/model",
|
||||
"you clean up text", **kwargs)
|
||||
return result, calls
|
||||
|
||||
@@ -325,34 +346,32 @@ class Cleanup(DikteTest):
|
||||
self.assertNotIn("reasoning", sent_json(calls[0]))
|
||||
|
||||
def test_an_effort_is_passed_on_and_the_thinking_left_out(self):
|
||||
_, calls = self.call(chat_reply("Hello."),
|
||||
target=openrouter(reasoning="high"))
|
||||
_, calls = self.call(chat_reply("Hello."), reasoning="high")
|
||||
self.assertEqual(sent_json(calls[0])["reasoning"],
|
||||
{"effort": "high", "exclude": True})
|
||||
|
||||
def test_a_local_base_url(self):
|
||||
_, calls = self.call(chat_reply("Hello."),
|
||||
target=openrouter(base_url="http://localhost:1234/v1"))
|
||||
_, calls = self.call(chat_reply("Hello."), base_url="http://localhost:1234/v1")
|
||||
self.assertEqual(calls[0].full_url, "http://localhost:1234/v1/chat/completions")
|
||||
|
||||
def test_no_key(self):
|
||||
with self.assertRaises(api.ApiError):
|
||||
api.cleanup(openrouter(key=""), "hello", "prompt")
|
||||
api.cleanup("hello", "", "some/model", "prompt")
|
||||
|
||||
def test_a_reply_with_no_choices_says_why(self):
|
||||
with fake_urlopen({"error": {"message": "model is offline"}}), \
|
||||
self.assertRaises(api.ApiError) as caught:
|
||||
api.cleanup(openrouter(), "hello", "p")
|
||||
api.cleanup("hello", "k", "m", "p")
|
||||
self.assertIn("model is offline", str(caught.exception))
|
||||
|
||||
def test_an_empty_answer(self):
|
||||
with fake_urlopen(chat_reply(" ")), self.assertRaises(api.ApiError):
|
||||
api.cleanup(openrouter(), "hello", "p")
|
||||
api.cleanup("hello", "k", "m", "p")
|
||||
|
||||
def test_a_rate_limit_is_explained(self):
|
||||
with fake_urlopen(http_error(429)), \
|
||||
self.assertRaises(api.ApiError) as caught:
|
||||
api.cleanup(openrouter(), "hello", "p")
|
||||
api.cleanup("hello", "k", "m", "p")
|
||||
self.assertIn("OpenRouter", str(caught.exception))
|
||||
|
||||
|
||||
@@ -448,6 +467,18 @@ class ModelLists(DikteTest):
|
||||
with self.assertRaises(api.ApiError):
|
||||
api.openai_models("")
|
||||
|
||||
def test_the_same_list_read_from_groq(self):
|
||||
with fake_urlopen({"data": [{"id": "llama-3.3-70b"},
|
||||
{"id": "whisper-large-v3"}]}) as calls:
|
||||
models = api.openai_models("gsk-test", api.GROQ_URL, "Groq")
|
||||
self.assertEqual(calls[0].full_url, "https://api.groq.com/openai/v1/models")
|
||||
self.assertEqual(models, ["whisper-large-v3"])
|
||||
|
||||
def test_a_missing_groq_key_says_groq(self):
|
||||
with self.assertRaises(api.ApiError) as caught:
|
||||
api.openai_models("", api.GROQ_URL, "Groq")
|
||||
self.assertIn("Groq", str(caught.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -473,7 +504,6 @@ class FakeServer:
|
||||
|
||||
|
||||
LOCAL = api.Target("local", "Local whisper", "", "", "ggml-base.bin")
|
||||
LOCAL_LLM = api.Target("local-llm", "Local model", "", "", "gemma.gguf", "none")
|
||||
|
||||
|
||||
class TranscribeHere(DikteTest):
|
||||
@@ -548,62 +578,3 @@ class TranscribeHere(DikteTest):
|
||||
with fake_urlopen({"segments": [{"start": 0, "end": 1, "text": " hi"}]}) as calls:
|
||||
api.transcribe_segments(LOCAL, self.wav)
|
||||
self.assertEqual(multipart_fields(calls[0])["model"], "ggml-base.bin")
|
||||
|
||||
|
||||
class CleanupHere(DikteTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.server = FakeServer("http://127.0.0.1:8888/v1")
|
||||
self.patch_attr(ggml, "llm", self.server)
|
||||
|
||||
def test_it_goes_to_the_server_it_starts(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
result = api.cleanup(LOCAL_LLM, "uh, hello", "clean it up")
|
||||
self.assertEqual(result, "Hello.")
|
||||
self.assertEqual(calls[0].full_url,
|
||||
"http://127.0.0.1:8888/v1/chat/completions")
|
||||
|
||||
def test_no_key_is_wanted_and_none_is_sent(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(LOCAL_LLM, "hello", "prompt")
|
||||
self.assertNotIn("Authorization", calls[0].headers)
|
||||
|
||||
def test_thinking_is_turned_off_in_the_words_llama_cpp_uses(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(LOCAL_LLM, "hello", "prompt")
|
||||
self.assertEqual(sent_json(calls[0])["chat_template_kwargs"],
|
||||
{"enable_thinking": False})
|
||||
|
||||
def test_the_models_own_default_asks_for_nothing(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(LOCAL_LLM._replace(reasoning=""), "hello", "prompt")
|
||||
self.assertNotIn("chat_template_kwargs", sent_json(calls[0]))
|
||||
|
||||
def test_a_reply_that_was_all_thinking_names_the_setting_that_fixes_it(self):
|
||||
reply = {"choices": [{"message": {"content": "", "reasoning": "hmm"}}]}
|
||||
with fake_urlopen(reply), self.assertRaises(api.ApiError) as caught:
|
||||
api.cleanup(LOCAL_LLM, "hello", "prompt")
|
||||
self.assertIn("Thinking", str(caught.exception))
|
||||
|
||||
def test_a_reply_longer_than_the_transcript_is_cut_off(self):
|
||||
# A small model will repeat the transcript until the context is full,
|
||||
# and every one of those tokens is a second of somebody waiting.
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(LOCAL_LLM, "x" * 4000, "prompt")
|
||||
self.assertEqual(sent_json(calls[0])["max_tokens"], 4000)
|
||||
|
||||
def test_a_short_dictation_still_gets_room_to_answer(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(LOCAL_LLM, "uh, hi", "prompt")
|
||||
self.assertEqual(sent_json(calls[0])["max_tokens"], 512)
|
||||
|
||||
def test_a_hosted_model_is_left_to_answer_at_length(self):
|
||||
with fake_urlopen(chat_reply("Hello.")) as calls:
|
||||
api.cleanup(openrouter(), "uh, hi", "prompt")
|
||||
self.assertNotIn("max_tokens", sent_json(calls[0]))
|
||||
|
||||
def test_a_server_that_will_not_start_is_the_error_shown(self):
|
||||
self.patch_attr(ggml, "llm", FakeServer(fails="llama.cpp is not installed"))
|
||||
with self.assertRaises(api.ApiError) as caught:
|
||||
api.cleanup(LOCAL_LLM, "hello", "prompt")
|
||||
self.assertIn("llama.cpp", str(caught.exception))
|
||||
|
||||
@@ -79,9 +79,12 @@ class Effort(unittest.TestCase):
|
||||
self.assertEqual(assistant.CODEX_EFFORT["xhigh"], "high")
|
||||
self.assertEqual(assistant.CODEX_EFFORT["max"], "high")
|
||||
|
||||
def test_claude_has_no_rung_below_low(self):
|
||||
self.assertEqual(assistant.CLAUDE_EFFORT["none"], "low")
|
||||
self.assertEqual(assistant.CLAUDE_EFFORT["minimal"], "low")
|
||||
def test_neither_one_asks_for_a_rung_below_low(self):
|
||||
# Claude has none; Codex has one, but calls it "minimal" on the older
|
||||
# models and "none" on the newer ones, and refuses the wrong word.
|
||||
for scale in (assistant.CLAUDE_EFFORT, assistant.CODEX_EFFORT):
|
||||
self.assertEqual(scale["none"], "low")
|
||||
self.assertEqual(scale["minimal"], "low")
|
||||
|
||||
def test_an_empty_setting_asks_for_nothing(self):
|
||||
self.assertEqual(assistant.CLAUDE_EFFORT.get("", ""), "")
|
||||
@@ -232,10 +235,10 @@ class SessionMissing(unittest.TestCase):
|
||||
self.assertFalse(assistant._session_missing(text))
|
||||
|
||||
def test_the_last_line_is_the_one_worth_showing(self):
|
||||
self.assertEqual(assistant._last_line("warning\n\nreal error\n"),
|
||||
self.assertEqual(assistant.last_line("warning\n\nreal error\n"),
|
||||
"real error")
|
||||
self.assertEqual(assistant._last_line(""), "")
|
||||
self.assertEqual(assistant._last_line(None), "")
|
||||
self.assertEqual(assistant.last_line(""), "")
|
||||
self.assertEqual(assistant.last_line(None), "")
|
||||
|
||||
|
||||
class Conclude(DikteTest):
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
"""Who cleans the transcript up, and what they are asked.
|
||||
|
||||
The CLIs are faked at subprocess.run: what the tests read is the argument list
|
||||
each one is given, where the answer is picked up from, and what happens to the
|
||||
chain when the program is missing, slow or unhappy. The OpenRouter path is the
|
||||
one that was always there and is checked here only for still being taken.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import api
|
||||
import cleanup
|
||||
import ggml
|
||||
from tests.support import DikteTest, fake_urlopen, sent_json, url_error
|
||||
from tests.test_api import FakeServer, chat_reply
|
||||
|
||||
|
||||
def fake_run(stdout="", code=0, stderr="", last_message=""):
|
||||
"""Stand in for subprocess.run, writing the file Codex would have written."""
|
||||
calls = []
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
calls.append(cmd)
|
||||
if last_message and "-o" in cmd:
|
||||
with open(cmd[cmd.index("-o") + 1], "w", encoding="utf-8") as fh:
|
||||
fh.write(last_message)
|
||||
return subprocess.CompletedProcess(cmd, code, stdout, stderr)
|
||||
|
||||
return mock.patch.object(subprocess, "run", side_effect=run), calls
|
||||
|
||||
|
||||
class Provider(DikteTest):
|
||||
def test_the_default_is_still_openrouter(self):
|
||||
self.assertEqual(cleanup.provider(self.config()), "openrouter")
|
||||
|
||||
def test_a_provider_this_version_does_not_have(self):
|
||||
self.assertEqual(
|
||||
cleanup.provider(self.config(cleanup_provider="ollama")), "openrouter")
|
||||
|
||||
def test_each_one_is_recognised(self):
|
||||
for name in cleanup.PROVIDERS:
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(
|
||||
cleanup.provider(self.config(cleanup_provider=name)), name)
|
||||
|
||||
def test_what_each_one_runs(self):
|
||||
self.assertEqual(cleanup.executable("claude"), "claude")
|
||||
self.assertEqual(cleanup.executable("codex"), "codex")
|
||||
self.assertEqual(cleanup.executable("openrouter"), "")
|
||||
|
||||
def test_the_model_named_in_the_history_is_the_one_that_did_it(self):
|
||||
self.assertEqual(cleanup.model(self.config(cleanup_model="some/model")),
|
||||
"some/model")
|
||||
self.assertEqual(
|
||||
cleanup.model(self.config(cleanup_provider="claude")), "haiku")
|
||||
self.assertEqual(
|
||||
cleanup.model(self.config(cleanup_provider="claude",
|
||||
cleanup_claude_model="opus")), "opus")
|
||||
# Codex on its own default has no model id to report, only a name.
|
||||
self.assertEqual(
|
||||
cleanup.model(self.config(cleanup_provider="codex")), "codex")
|
||||
self.assertEqual(
|
||||
cleanup.model(self.config(cleanup_provider="codex",
|
||||
cleanup_codex_model="gpt-5.4")), "gpt-5.4")
|
||||
|
||||
|
||||
class OpenRouter(DikteTest):
|
||||
def test_it_is_still_one_request_with_the_settings_as_they_were(self):
|
||||
conf = self.config(openrouter_api_key="sk-or-test",
|
||||
cleanup_model="some/model", cleanup_reasoning="low")
|
||||
with mock.patch.object(api, "cleanup", return_value="Done.") as call:
|
||||
self.assertEqual(cleanup.run("uh, done", conf, "the rules"), "Done.")
|
||||
text, key, model, prompt = call.call_args.args
|
||||
self.assertEqual((text, key, model, prompt),
|
||||
("uh, done", "sk-or-test", "some/model", "the rules"))
|
||||
self.assertEqual(call.call_args.kwargs["reasoning"], "low")
|
||||
|
||||
def test_no_cli_is_started_for_it(self):
|
||||
conf = self.config(openrouter_api_key="sk-or-test")
|
||||
patcher, calls = fake_run(stdout="never")
|
||||
with patcher, mock.patch.object(api, "cleanup", return_value="Done."):
|
||||
cleanup.run("uh, done", conf, "the rules")
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
|
||||
class ClaudeCode(DikteTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.conf = self.config(cleanup_provider="claude")
|
||||
self.patch_attr(cleanup.shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||
|
||||
def run_cleanup(self, text="uh, book it", **kwargs):
|
||||
patcher, calls = fake_run(**kwargs)
|
||||
with patcher:
|
||||
answer = cleanup.run(text, self.conf, "the rules")
|
||||
return answer, calls[0]
|
||||
|
||||
def test_the_transcript_goes_in_fenced_and_the_rules_go_in_as_the_prompt(self):
|
||||
answer, cmd = self.run_cleanup(stdout="Book it.\n")
|
||||
self.assertEqual(answer, "Book it.")
|
||||
self.assertEqual(cmd[0], "claude")
|
||||
self.assertIn("<transcript>\nuh, book it\n</transcript>", cmd)
|
||||
self.assertEqual(cmd[cmd.index("--system-prompt") + 1], "the rules")
|
||||
self.assertEqual(cmd[cmd.index("--model") + 1], "haiku")
|
||||
|
||||
def test_it_is_given_nothing_to_run_and_nothing_to_remember(self):
|
||||
_, cmd = self.run_cleanup(stdout="Book it.")
|
||||
self.assertEqual(cmd[cmd.index("--tools") + 1], "")
|
||||
self.assertIn("--strict-mcp-config", cmd)
|
||||
self.assertIn("--no-session-persistence", cmd)
|
||||
|
||||
def test_the_thinking_setting_is_carried_over_in_its_own_words(self):
|
||||
self.conf["cleanup_reasoning"] = "none"
|
||||
_, cmd = self.run_cleanup(stdout="Book it.")
|
||||
self.assertEqual(cmd[cmd.index("--effort") + 1], "low")
|
||||
|
||||
def test_no_thinking_setting_means_no_flag(self):
|
||||
_, cmd = self.run_cleanup(stdout="Book it.")
|
||||
self.assertNotIn("--effort", cmd)
|
||||
|
||||
def test_a_model_of_your_own(self):
|
||||
self.conf["cleanup_claude_model"] = "claude-sonnet-5"
|
||||
_, cmd = self.run_cleanup(stdout="Book it.")
|
||||
self.assertEqual(cmd[cmd.index("--model") + 1], "claude-sonnet-5")
|
||||
|
||||
def test_an_answer_of_nothing_is_a_failure_rather_than_an_empty_paste(self):
|
||||
with self.assertRaises(cleanup.CleanupError):
|
||||
self.run_cleanup(stdout=" \n")
|
||||
|
||||
def test_the_last_line_of_the_complaint_is_what_gets_shown(self):
|
||||
with self.assertRaises(cleanup.CleanupError) as caught:
|
||||
self.run_cleanup(code=1, stderr="a warning\nout of credit\n")
|
||||
self.assertEqual(str(caught.exception), "out of credit")
|
||||
|
||||
def test_a_failure_is_the_same_kind_the_chain_already_catches(self):
|
||||
# worker, the file transcriber and the meeting all keep the raw
|
||||
# transcript when an ApiError comes out of here.
|
||||
self.assertTrue(issubclass(cleanup.CleanupError, api.ApiError))
|
||||
|
||||
def test_a_program_that_is_not_installed_says_so_before_running_anything(self):
|
||||
self.patch_attr(cleanup.shutil, "which", lambda name: "")
|
||||
with self.assertRaises(cleanup.CleanupError) as caught:
|
||||
self.run_cleanup(stdout="Book it.")
|
||||
self.assertIn("claude", str(caught.exception))
|
||||
|
||||
def test_a_run_that_never_ends(self):
|
||||
def run(cmd, **kwargs):
|
||||
raise subprocess.TimeoutExpired(cmd, 180)
|
||||
|
||||
with mock.patch.object(subprocess, "run", side_effect=run):
|
||||
with self.assertRaises(cleanup.CleanupError) as caught:
|
||||
cleanup.run("uh, book it", self.conf, "the rules")
|
||||
self.assertIn("180", str(caught.exception))
|
||||
|
||||
|
||||
class Codex(DikteTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.conf = self.config(cleanup_provider="codex")
|
||||
self.patch_attr(cleanup.shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||
|
||||
def run_cleanup(self, text="uh, book it", **kwargs):
|
||||
patcher, calls = fake_run(**kwargs)
|
||||
with patcher:
|
||||
answer = cleanup.run(text, self.conf, "the rules")
|
||||
return answer, calls[0]
|
||||
|
||||
def test_the_rules_ride_in_front_of_the_transcript(self):
|
||||
answer, cmd = self.run_cleanup(last_message="Book it.\n")
|
||||
self.assertEqual(answer, "Book it.")
|
||||
self.assertEqual(cmd[:2], ["codex", "exec"])
|
||||
self.assertEqual(cmd[-1],
|
||||
"the rules\n\n---\n\n<transcript>\nuh, book it\n</transcript>")
|
||||
|
||||
def test_the_answer_is_read_from_the_file_rather_than_the_noise_on_stdout(self):
|
||||
answer, _ = self.run_cleanup(
|
||||
stdout="workdir: /home\nmodel: gpt-5.4\ntokens used 400\n",
|
||||
last_message="Book it.",
|
||||
)
|
||||
self.assertEqual(answer, "Book it.")
|
||||
|
||||
def test_that_file_does_not_stay_behind(self):
|
||||
_, cmd = self.run_cleanup(last_message="Book it.")
|
||||
self.assertFalse(os.path.exists(cmd[cmd.index("-o") + 1]))
|
||||
|
||||
def test_it_may_read_but_not_write_and_has_nobody_to_ask(self):
|
||||
_, cmd = self.run_cleanup(last_message="Book it.")
|
||||
self.assertEqual(cmd[cmd.index("--sandbox") + 1], "read-only")
|
||||
self.assertIn('approval_policy="never"', cmd)
|
||||
self.assertIn("--ephemeral", cmd)
|
||||
|
||||
def test_the_model_is_left_alone_until_one_is_typed_in(self):
|
||||
_, cmd = self.run_cleanup(last_message="Book it.")
|
||||
self.assertNotIn("-m", cmd)
|
||||
self.conf["cleanup_codex_model"] = "gpt-5.4"
|
||||
_, cmd = self.run_cleanup(last_message="Book it.")
|
||||
self.assertEqual(cmd[cmd.index("-m") + 1], "gpt-5.4")
|
||||
|
||||
def test_the_thinking_setting_lands_on_the_nearest_rung_codex_has(self):
|
||||
self.conf["cleanup_reasoning"] = "xhigh"
|
||||
_, cmd = self.run_cleanup(last_message="Book it.")
|
||||
self.assertIn('model_reasoning_effort="high"', cmd)
|
||||
|
||||
def test_an_answer_of_nothing(self):
|
||||
with self.assertRaises(cleanup.CleanupError):
|
||||
self.run_cleanup(stdout="tokens used 400", last_message="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class Here(DikteTest):
|
||||
"""llama.cpp, answering the request OpenRouter answers."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.conf = self.config(cleanup_provider="local",
|
||||
local_llm_model="gemma.gguf")
|
||||
self.server = FakeServer()
|
||||
self.patch_attr(ggml, "llm", self.server)
|
||||
|
||||
def test_the_address_comes_from_the_server_it_starts(self):
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
self.assertEqual(cleanup.run("uh, done", self.conf, "the rules"),
|
||||
"Done.")
|
||||
self.assertEqual(self.server.starts, 1)
|
||||
self.assertEqual(calls[0].full_url,
|
||||
"http://127.0.0.1:9999/v1/chat/completions")
|
||||
|
||||
def test_no_key_is_wanted_and_none_is_sent(self):
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertNotIn("Authorization", calls[0].headers)
|
||||
|
||||
def test_thinking_is_turned_off_in_the_words_llama_cpp_uses(self):
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertEqual(sent_json(calls[0])["chat_template_kwargs"],
|
||||
{"enable_thinking": False})
|
||||
|
||||
def test_the_models_own_default_asks_for_nothing(self):
|
||||
self.conf["local_llm_reasoning"] = ""
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertNotIn("chat_template_kwargs", sent_json(calls[0]))
|
||||
|
||||
def test_a_reply_longer_than_the_transcript_is_cut_off(self):
|
||||
# A small model will repeat the transcript until the context is full,
|
||||
# and every one of those tokens is a second of somebody waiting.
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
cleanup.run("x" * 4000, self.conf, "the rules")
|
||||
self.assertEqual(sent_json(calls[0])["max_tokens"], 4000)
|
||||
|
||||
def test_a_short_dictation_still_gets_room_to_answer(self):
|
||||
with fake_urlopen(chat_reply("Done.")) as calls:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertEqual(sent_json(calls[0])["max_tokens"], 512)
|
||||
|
||||
def test_a_reply_that_was_all_thinking_names_the_setting_that_fixes_it(self):
|
||||
reply = {"choices": [{"message": {"content": "", "reasoning": "hmm"}}]}
|
||||
with fake_urlopen(reply), self.assertRaises(api.ApiError) as caught:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertIn("Thinking", str(caught.exception))
|
||||
|
||||
def test_a_server_that_will_not_start_is_the_error_shown(self):
|
||||
self.patch_attr(ggml, "llm", FakeServer(fails="llama.cpp is not installed"))
|
||||
with self.assertRaises(api.ApiError) as caught:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertIn("llama.cpp", str(caught.exception))
|
||||
|
||||
def test_a_server_that_dies_mid_request_says_what_it_printed(self):
|
||||
self.patch_attr(ggml, "llm", FakeServer(log="out of memory"))
|
||||
with fake_urlopen(url_error("connection reset")):
|
||||
with self.assertRaises(api.ApiError) as caught:
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertIn("out of memory", str(caught.exception))
|
||||
|
||||
def test_no_cli_is_started_for_it(self):
|
||||
patcher, calls = fake_run(stdout="never")
|
||||
with patcher, fake_urlopen(chat_reply("Done.")):
|
||||
cleanup.run("uh, done", self.conf, "the rules")
|
||||
self.assertEqual(calls, [])
|
||||
+79
-1
@@ -14,8 +14,9 @@ from unittest import mock
|
||||
|
||||
import cli
|
||||
import config as cfg
|
||||
import hotkey
|
||||
import ipc
|
||||
from tests.support import DikteTest
|
||||
from tests.support import DikteTest, fake_urlopen
|
||||
|
||||
|
||||
class Options:
|
||||
@@ -144,6 +145,22 @@ class Parser(unittest.TestCase):
|
||||
self.assertIsNone(opts.verb)
|
||||
self.assertEqual(opts.func, cli.cmd_plain)
|
||||
|
||||
def test_every_global_shortcut_runs_a_verb_that_exists(self):
|
||||
"""A shortcut registers a command line; a verb the parser never heard of
|
||||
is a key that does nothing at all when it is pressed."""
|
||||
for name, spec in hotkey.SHORTCUTS.items():
|
||||
with self.subTest(name=name):
|
||||
opts = self.parse(spec.verb)
|
||||
self.assertTrue(callable(opts.func))
|
||||
|
||||
def test_every_shortcut_can_be_installed_and_removed_by_name(self):
|
||||
for name in hotkey.SHORTCUTS:
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(self.parse("shortcut", "install", name).which,
|
||||
name)
|
||||
self.assertEqual(self.parse("shortcut", "remove", name).which,
|
||||
name)
|
||||
|
||||
def test_every_verb_is_wired_to_something(self):
|
||||
for verb in ("record", "toggle", "start", "stop", "cancel", "ask",
|
||||
"session", "transcribe", "meeting", "meetings", "history",
|
||||
@@ -316,6 +333,67 @@ class ConfigCommands(DikteTest):
|
||||
{"cleanup", "subtitles", "meeting", "agent"})
|
||||
|
||||
|
||||
class Providers(DikteTest):
|
||||
"""The terminal reaches every provider the settings window does."""
|
||||
|
||||
def run_cmd(self, func, **values):
|
||||
with captured() as (out, err):
|
||||
code = func(Options(**values))
|
||||
return code, out.getvalue(), err.getvalue()
|
||||
|
||||
def test_a_provider_the_settings_window_offers_is_a_choice_here_too(self):
|
||||
parser = cli.build_parser()
|
||||
for provider in cfg.TRANSCRIBERS:
|
||||
with self.subTest(provider=provider):
|
||||
opts = parser.parse_args(["models", "--provider", provider])
|
||||
self.assertEqual(opts.provider, provider)
|
||||
self.assertEqual(parser.parse_args(["test-key", provider]).which,
|
||||
provider)
|
||||
|
||||
def test_the_model_list_is_read_from_the_chosen_provider(self):
|
||||
self.write_config({"groq_api_key": "gsk-test"})
|
||||
with fake_urlopen({"data": [{"id": "whisper-large-v3"}]}) as calls:
|
||||
code, out, _ = self.run_cmd(cli.cmd_models, provider="groq",
|
||||
transcription=False)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(calls[0].full_url, "https://api.groq.com/openai/v1/models")
|
||||
self.assertEqual(out.strip(), "whisper-large-v3")
|
||||
|
||||
def test_a_key_that_is_not_there_is_reported_under_its_own_name(self):
|
||||
code, out, _ = self.run_cmd(cli.cmd_test_key, which="groq")
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("groq", out)
|
||||
self.assertIn("Groq", out)
|
||||
|
||||
|
||||
class Doctor(DikteTest):
|
||||
"""One pass over everything the settings window checks behind its buttons."""
|
||||
|
||||
def run_doctor(self, as_json=True, **settings):
|
||||
self.write_config(settings)
|
||||
with mock.patch.object(ipc, "send", return_value=None), \
|
||||
captured() as (out, _err):
|
||||
cli.cmd_doctor(Options(json=as_json))
|
||||
return json.loads(out.getvalue()) if as_json else out.getvalue()
|
||||
|
||||
def test_cleanup_on_openrouter_is_a_question_about_the_key(self):
|
||||
reply = self.run_doctor(cleanup_model="some/model")
|
||||
self.assertEqual(reply["cleanup"]["provider"], "openrouter")
|
||||
self.assertEqual(reply["cleanup"]["model"], "some/model")
|
||||
self.assertIn("OpenRouter key, cleaning up on some/model",
|
||||
self.run_doctor(as_json=False, cleanup_model="some/model"))
|
||||
|
||||
def test_cleanup_on_a_cli_is_a_question_about_the_program(self):
|
||||
reply = self.run_doctor(cleanup_provider="codex",
|
||||
cleanup_codex_model="gpt-5.4")
|
||||
self.assertEqual(reply["cleanup"]["provider"], "codex")
|
||||
self.assertEqual(reply["cleanup"]["model"], "gpt-5.4")
|
||||
self.assertIn("codex", reply["programs"])
|
||||
self.assertIn("codex, cleaning up on gpt-5.4",
|
||||
self.run_doctor(as_json=False, cleanup_provider="codex",
|
||||
cleanup_codex_model="gpt-5.4"))
|
||||
|
||||
|
||||
class Finding(DikteTest):
|
||||
def test_no_history_at_all(self):
|
||||
self.assertIsNone(cli._find_history("last"))
|
||||
|
||||
+36
-20
@@ -12,6 +12,7 @@ import unittest
|
||||
from unittest import mock
|
||||
|
||||
import api
|
||||
import cleanup
|
||||
import config as cfg
|
||||
import ggml
|
||||
import i18n
|
||||
@@ -126,6 +127,10 @@ class Keys(DikteTest):
|
||||
def test_no_key_anywhere(self):
|
||||
self.assertEqual(cfg.Config().openai_key(), "")
|
||||
|
||||
def test_every_provider_falls_back_to_the_variable_of_its_own_name(self):
|
||||
with mock.patch.dict(os.environ, {"GROQ_API_KEY": "gsk-env"}):
|
||||
self.assertEqual(cfg.Config().groq_key(), "gsk-env")
|
||||
|
||||
|
||||
class TranscribeTarget(DikteTest):
|
||||
def test_this_machine_by_default(self):
|
||||
@@ -155,6 +160,21 @@ class TranscribeTarget(DikteTest):
|
||||
self.assertEqual(target.api_key, "sk-or-test")
|
||||
self.assertEqual(target.model, "openai/whisper-1")
|
||||
|
||||
def test_groq_when_it_is_picked(self):
|
||||
conf = self.config(transcribe_provider="groq", groq_api_key="gsk-test",
|
||||
groq_transcribe_model="whisper-large-v3")
|
||||
target = conf.transcribe_target()
|
||||
self.assertEqual(target.provider, "groq")
|
||||
self.assertEqual(target.service, "Groq")
|
||||
self.assertEqual(target.api_key, "gsk-test")
|
||||
self.assertEqual(target.base_url, api.GROQ_URL)
|
||||
self.assertEqual(target.model, "whisper-large-v3")
|
||||
|
||||
def test_a_provider_this_version_has_never_heard_of(self):
|
||||
"""A config written by a fork, or by a version that dropped one."""
|
||||
target = self.config(transcribe_provider="deepgram").transcribe_target()
|
||||
self.assertEqual(target.provider, "openai")
|
||||
|
||||
def test_a_self_hosted_endpoint(self):
|
||||
conf = self.config(transcribe_provider="openai",
|
||||
openai_base_url="http://localhost:8080/v1")
|
||||
@@ -438,31 +458,27 @@ if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class LocalTargets(DikteTest):
|
||||
def test_cleanup_can_run_here_while_the_minutes_do_not(self):
|
||||
# The two jobs are not the same size: a small model on this machine
|
||||
# strips filler words perfectly well and will not write up an hour.
|
||||
conf = self.config(cleanup_provider="local", local_llm_model="gemma.gguf")
|
||||
self.assertEqual(conf.cleanup_target().provider, "local-llm")
|
||||
self.assertEqual(conf.minutes_target().provider, "openrouter")
|
||||
self.assertEqual(conf.minutes_target().model, cfg.DEFAULTS["meeting_model"])
|
||||
class LocalCleanup(DikteTest):
|
||||
def test_the_local_model_is_what_the_history_records(self):
|
||||
conf = self.config(cleanup_provider="local",
|
||||
local_llm_model="gemma-3-4b-it-Q4_K_M.gguf")
|
||||
self.assertEqual(cleanup.provider(conf), "local")
|
||||
self.assertEqual(cleanup.model(conf), "gemma-3-4b-it-Q4_K_M.gguf")
|
||||
|
||||
def test_the_minutes_can_run_here_on_their_own(self):
|
||||
conf = self.config(meeting_provider="local", local_llm_model="gemma.gguf")
|
||||
self.assertEqual(conf.minutes_target().model, "gemma.gguf")
|
||||
self.assertEqual(conf.cleanup_target().provider, "openrouter")
|
||||
def test_it_needs_no_program_on_the_path(self):
|
||||
# whisper.cpp and llama.cpp are fetched rather than installed, so unlike
|
||||
# Claude Code and Codex there is no executable to look for.
|
||||
self.assertEqual(cleanup.executable("local"), "")
|
||||
|
||||
def test_the_local_cleanup_target_carries_the_thinking_level(self):
|
||||
conf = self.config(cleanup_provider="local", local_llm_model="gemma.gguf",
|
||||
local_llm_reasoning="none")
|
||||
target = conf.cleanup_target()
|
||||
self.assertEqual(target.reasoning, "none")
|
||||
self.assertEqual(target.api_key, "")
|
||||
def test_the_minutes_do_not_follow_the_cleanup_provider(self):
|
||||
# A 4B model here will strip the filler words out of a dictation and
|
||||
# will not write up an hour long meeting.
|
||||
conf = self.config(cleanup_provider="local")
|
||||
self.assertEqual(conf["meeting_model"], cfg.DEFAULTS["meeting_model"])
|
||||
|
||||
def test_either_of_them_counts_as_using_the_local_model(self):
|
||||
def test_only_the_cleanup_setting_asks_for_the_local_model(self):
|
||||
self.assertFalse(cfg.Config().uses_local_llm())
|
||||
self.assertTrue(self.config(cleanup_provider="local").uses_local_llm())
|
||||
self.assertTrue(self.config(meeting_provider="local").uses_local_llm())
|
||||
|
||||
|
||||
class ReadyToRun(DikteTest):
|
||||
|
||||
@@ -197,7 +197,7 @@ class Transcriber(DikteTest):
|
||||
|
||||
def test_cleanup_is_told_it_is_writing_subtitles(self):
|
||||
_, _, _, cleanup_call = self.run_chain(cleanup=True)
|
||||
prompt = cleanup_call.call_args.args[2]
|
||||
prompt = cleanup_call.call_args.args[3]
|
||||
self.assertEqual(prompt, self.conf.cleanup_prompt(subtitles=True))
|
||||
|
||||
def test_timestamps_come_back_as_segments_and_as_stamped_lines(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import config as cfg
|
||||
import hotkey
|
||||
from tests.support import DikteTest, FakeCompleted, linux_only
|
||||
|
||||
@@ -56,6 +57,28 @@ class ParseShortcut(unittest.TestCase):
|
||||
self.assertEqual(hotkey.parse_shortcut(None), (None, None))
|
||||
|
||||
|
||||
class Table(unittest.TestCase):
|
||||
"""The one list of global shortcuts. The command line, the settings window
|
||||
and install.sh read it instead of keeping a copy each, so what it has to
|
||||
hold together is checked here rather than in three places."""
|
||||
|
||||
def test_every_shortcut_remembers_itself_in_a_real_setting(self):
|
||||
for name, spec in hotkey.SHORTCUTS.items():
|
||||
with self.subTest(name=name):
|
||||
self.assertIn(spec.setting, cfg.DEFAULTS)
|
||||
|
||||
def test_no_two_share_a_desktop_entry(self):
|
||||
ids = [spec.desktop_id for spec in hotkey.SHORTCUTS.values()]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
|
||||
def test_only_the_toggle_falls_back_to_a_key_of_its_own(self):
|
||||
"""The rest are off until you pick one, and emptying the box is how you
|
||||
turn them off again."""
|
||||
self.assertEqual(hotkey.SHORTCUTS["toggle"].fallback, "Ctrl+Space")
|
||||
self.assertEqual([name for name, spec in hotkey.SHORTCUTS.items()
|
||||
if spec.fallback], ["toggle"])
|
||||
|
||||
|
||||
class ModsMatch(unittest.TestCase):
|
||||
"""The combination has to be exact, or Ctrl+Space fires on Ctrl+Shift+Space."""
|
||||
|
||||
@@ -122,6 +145,23 @@ class Bindings(DikteTest):
|
||||
thread.assert_called_once()
|
||||
self.assertEqual(len(listener._bindings[57]), 2)
|
||||
|
||||
def test_starting_and_discarding_do_not_fire_on_each_other(self):
|
||||
"""The two defaults are one modifier apart on the same key code, so the
|
||||
modifier set is the only thing keeping them apart."""
|
||||
listener = hotkey.EvdevHotkey()
|
||||
self.addCleanup(listener.stop)
|
||||
with mock.patch.object(listener, "_open_devices", return_value=[99]), \
|
||||
mock.patch.object(hotkey.threading, "Thread"):
|
||||
listener.start({"toggle": "Ctrl+Space", "cancel": "Ctrl+Alt+Space"})
|
||||
|
||||
def fired(held):
|
||||
return [name for mods, name in listener._bindings[57]
|
||||
if hotkey.EvdevHotkey._mods_match(held, mods)]
|
||||
|
||||
self.assertEqual(fired({29}), ["toggle"]) # ctrl
|
||||
self.assertEqual(fired({29, 56}), ["cancel"]) # ctrl + alt
|
||||
self.assertEqual(fired({29, 42}), []) # ctrl + shift
|
||||
|
||||
|
||||
@linux_only
|
||||
class Chooser(DikteTest):
|
||||
|
||||
+98
-13
@@ -11,7 +11,9 @@ from unittest import mock
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
import cleanup
|
||||
import config as cfg
|
||||
import hotkey
|
||||
import overlay as overlay_module
|
||||
import settings_ui
|
||||
from tests.support import DikteTest, only_these_tools
|
||||
@@ -36,13 +38,17 @@ CHANGED = {
|
||||
"filter_hallucinations": False,
|
||||
"keep_audio": True,
|
||||
"openai_api_key": "sk-test-key",
|
||||
"groq_api_key": "gsk-test-key",
|
||||
"openrouter_api_key": "sk-or-test-key",
|
||||
"transcribe_provider": "openrouter",
|
||||
"transcribe_model": "whisper-1",
|
||||
"groq_transcribe_model": "whisper-large-v3",
|
||||
"openrouter_transcribe_model": "openai/whisper-1",
|
||||
"cleanup_enabled": False,
|
||||
"cleanup_provider": "local",
|
||||
"cleanup_model": "some/other-model",
|
||||
"cleanup_claude_model": "opus",
|
||||
"cleanup_codex_model": "gpt-5",
|
||||
"cleanup_reasoning": "high",
|
||||
"local_model": "ggml-small.bin",
|
||||
"local_gpu": False,
|
||||
@@ -84,6 +90,7 @@ CHANGED = {
|
||||
"file_timestamps": True,
|
||||
"file_cleanup": False,
|
||||
"shortcut": "Ctrl+Alt+Space",
|
||||
"cancel_shortcut": "Meta+Shift+Space",
|
||||
"evdev_hotkey": True,
|
||||
"history_limit": 50,
|
||||
}
|
||||
@@ -106,7 +113,7 @@ class Settings(DikteTest):
|
||||
self.path("kglobalshortcutsrc")))
|
||||
|
||||
def window(self, conf):
|
||||
window = settings_ui.SettingsWindow(conf, "dikte toggle")
|
||||
window = settings_ui.SettingsWindow(conf)
|
||||
self.addCleanup(window.deleteLater)
|
||||
self.addCleanup(window.close)
|
||||
return window
|
||||
@@ -134,6 +141,20 @@ class Settings(DikteTest):
|
||||
with self.subTest(key=key):
|
||||
self.assertEqual(stored[key], value)
|
||||
|
||||
def test_the_model_box_on_screen_belongs_to_whoever_cleans_up(self):
|
||||
"""An OpenRouter id and a Claude alias are not the same field."""
|
||||
window = self.window(cfg.Config())
|
||||
boxes = {"openrouter": window.cleanup_model_row,
|
||||
"claude": window.cleanup_claude_model,
|
||||
"codex": window.cleanup_codex_model}
|
||||
for provider, box in boxes.items():
|
||||
with self.subTest(provider=provider):
|
||||
window._select_data(window.cleanup_provider, provider)
|
||||
shown = [name for name, other in boxes.items()
|
||||
if not other.isHidden()]
|
||||
self.assertEqual(shown, [provider])
|
||||
self.assertFalse(box.isHidden())
|
||||
|
||||
def test_the_settings_the_window_does_not_show_are_left_alone(self):
|
||||
"""A tab nobody wrote must not reset what the command line set."""
|
||||
self.write_config({"silence_db": -42.0, "speech_margin_db": 15.0,
|
||||
@@ -144,6 +165,37 @@ class Settings(DikteTest):
|
||||
self.assertEqual(stored["speech_margin_db"], 15.0)
|
||||
self.assertEqual(stored["openrouter_base_url"], "http://localhost:1234/v1")
|
||||
|
||||
def test_every_global_shortcut_has_a_row_of_its_own(self):
|
||||
window = self.window(cfg.Config())
|
||||
self.assertEqual(set(window._shortcut_rows), set(hotkey.SHORTCUTS))
|
||||
|
||||
def test_emptying_a_shortcut_turns_it_off_but_not_the_toggle(self):
|
||||
"""The application is unusable without the toggle, so that one box
|
||||
falls back. The rest stay empty, which is how they are switched off."""
|
||||
conf = cfg.Config()
|
||||
window = self.window(conf)
|
||||
for box, _status, _missing in window._shortcut_rows.values():
|
||||
box.setCurrentText("")
|
||||
window._save()
|
||||
self.assertEqual(conf["shortcut"], "Ctrl+Space")
|
||||
self.assertEqual(conf["cancel_shortcut"], "")
|
||||
self.assertEqual(conf["assistant_shortcut"], "")
|
||||
self.assertEqual(conf["meeting_shortcut"], "")
|
||||
|
||||
def test_installing_the_discard_key_writes_its_own_entry(self):
|
||||
conf = cfg.Config()
|
||||
window = self.window(conf)
|
||||
window._shortcut_rows["cancel"][0].setCurrentText("Meta+Shift+Space")
|
||||
with mock.patch.object(settings_ui.hotkey, "install_shortcut",
|
||||
return_value=(True, "saved")) as install:
|
||||
window._install_shortcut("cancel")
|
||||
combo, command = install.call_args.args
|
||||
self.assertEqual(combo, "Meta+Shift+Space")
|
||||
self.assertTrue(command.endswith(" cancel"))
|
||||
self.assertEqual(install.call_args.kwargs["desktop_id"],
|
||||
hotkey.CANCEL_DESKTOP_ID)
|
||||
self.assertEqual(conf["cancel_shortcut"], "Meta+Shift+Space")
|
||||
|
||||
def test_a_prompt_left_at_its_default_is_stored_as_empty(self):
|
||||
"""So that switching the interface language switches the prompt too."""
|
||||
conf = cfg.Config()
|
||||
@@ -155,14 +207,44 @@ class Settings(DikteTest):
|
||||
def test_each_provider_keeps_its_own_transcription_model(self):
|
||||
self.write_config({"transcribe_provider": "openai",
|
||||
"transcribe_model": "gpt-4o-transcribe",
|
||||
"groq_transcribe_model": "whisper-large-v3",
|
||||
"openrouter_transcribe_model": "openai/whisper-1"})
|
||||
conf = cfg.Config()
|
||||
window = self.window(conf)
|
||||
window.transcribe_provider.setCurrentIndex(
|
||||
window.transcribe_provider.findData("openrouter"))
|
||||
for provider in ("groq", "openrouter"):
|
||||
window.transcribe_provider.setCurrentIndex(
|
||||
window.transcribe_provider.findData(provider))
|
||||
window._save()
|
||||
self.assertEqual(conf["transcribe_provider"], "openrouter")
|
||||
self.assertEqual(conf["transcribe_model"], "gpt-4o-transcribe")
|
||||
self.assertEqual(conf["groq_transcribe_model"], "whisper-large-v3")
|
||||
|
||||
def test_the_provider_box_offers_every_provider_config_knows(self):
|
||||
window = self.window(cfg.Config())
|
||||
offered = [window.transcribe_provider.itemData(i)
|
||||
for i in range(window.transcribe_provider.count())]
|
||||
self.assertEqual(offered, ["local"] + list(cfg.TRANSCRIBERS))
|
||||
|
||||
def test_the_cleanup_box_offers_everyone_cleanup_py_dispatches_to(self):
|
||||
window = self.window(cfg.Config())
|
||||
offered = [window.cleanup_provider.itemData(i)
|
||||
for i in range(window.cleanup_provider.count())]
|
||||
self.assertEqual(sorted(offered), sorted(cleanup.PROVIDERS))
|
||||
|
||||
def test_the_answer_to_a_test_lands_under_the_key_it_was_asked_about(self):
|
||||
"""One signal serves all three buttons, so it carries which one asked."""
|
||||
window = self.window(cfg.Config())
|
||||
window._on_test_done("groq", True, "it works")
|
||||
button, answer = window._testers["groq"]
|
||||
self.assertEqual(answer.text(), "✓ it works")
|
||||
self.assertTrue(button.isEnabled())
|
||||
self.assertEqual(window._testers["openai"][1].text(), "")
|
||||
|
||||
def test_a_key_lands_in_the_field_of_its_own_provider(self):
|
||||
self.write_config({"groq_api_key": "gsk-mine"})
|
||||
window = self.window(cfg.Config())
|
||||
self.assertEqual(window.groq_key.text(), "gsk-mine")
|
||||
self.assertEqual(window.openai_key.text(), "")
|
||||
|
||||
def test_saving_applies_the_lowered_history_limit_at_once(self):
|
||||
for index in range(10):
|
||||
@@ -275,7 +357,7 @@ class LocalModels(DikteTest):
|
||||
"""The download boxes, without a network and without either program."""
|
||||
|
||||
def window(self, conf):
|
||||
window = settings_ui.SettingsWindow(conf, "dikte toggle")
|
||||
window = settings_ui.SettingsWindow(conf)
|
||||
self.addCleanup(window.deleteLater)
|
||||
self.addCleanup(window.close)
|
||||
return window
|
||||
@@ -327,17 +409,20 @@ class LocalModels(DikteTest):
|
||||
for row in range(box.repo.count()))
|
||||
self.assertGreaterEqual(view.minimumWidth(), widest)
|
||||
|
||||
def test_the_hosted_boxes_go_away_when_the_work_happens_here(self):
|
||||
def test_only_the_chosen_transcriber_is_on_screen(self):
|
||||
window = self.window(self.config(transcribe_provider="openai"))
|
||||
self.assertTrue(window.hosted_stt.isVisibleTo(window))
|
||||
self.assertFalse(window.local_whisper.isVisibleTo(window))
|
||||
self.assertTrue(window.stt_form.isRowVisible(window.transcribe_model_row))
|
||||
self.assertFalse(window.stt_form.isRowVisible(window.local_whisper))
|
||||
window._select_data(window.transcribe_provider, "local")
|
||||
self.assertFalse(window.hosted_stt.isVisibleTo(window))
|
||||
self.assertTrue(window.local_whisper.isVisibleTo(window))
|
||||
self.assertFalse(window.stt_form.isRowVisible(window.transcribe_model_row))
|
||||
self.assertTrue(window.stt_form.isRowVisible(window.local_whisper))
|
||||
|
||||
def test_the_same_for_cleanup(self):
|
||||
def test_only_the_chosen_cleaner_is_on_screen(self):
|
||||
window = self.window(cfg.Config())
|
||||
self.assertTrue(window.hosted_cleanup.isVisibleTo(window))
|
||||
self.assertTrue(window.cleanup_form.isRowVisible(window.cleanup_model_row))
|
||||
self.assertFalse(window.cleanup_form.isRowVisible(window.local_llm))
|
||||
window._select_data(window.cleanup_provider, "local")
|
||||
self.assertTrue(window.local_llm.isVisibleTo(window))
|
||||
self.assertFalse(window.hosted_cleanup.isVisibleTo(window))
|
||||
self.assertTrue(window.cleanup_form.isRowVisible(window.local_llm))
|
||||
self.assertFalse(window.cleanup_form.isRowVisible(window.cleanup_model_row))
|
||||
# Its own thinking box, because the two default to opposite things.
|
||||
self.assertFalse(window.cleanup_form.isRowVisible(window.cleanup_reasoning))
|
||||
|
||||
Reference in New Issue
Block a user