Transcribe during recording with recoverable chunks and microphone activity icon
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""Tray integration without a microphone, display, network, or clipboard."""
|
||||
import os
|
||||
os.environ.setdefault("PYSTRAY_BACKEND", "dummy")
|
||||
|
||||
import threading
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import blurt
|
||||
|
||||
|
||||
class StreamingAppTests(unittest.TestCase):
|
||||
def app(self):
|
||||
app = blurt.BlurtApp.__new__(blurt.BlurtApp)
|
||||
app._live_lock = threading.RLock()
|
||||
app._delivery_lock = threading.Lock()
|
||||
app.lock = threading.Lock()
|
||||
app._live_sessions = []
|
||||
app._quitting = False
|
||||
app._recording_session = None
|
||||
app._update_icon = Mock()
|
||||
app.platform = SimpleNamespace(notify=Mock(), dispatch=Mock())
|
||||
app.transcription_model = "whisper-1"
|
||||
app.transcription_language = "en"
|
||||
app.api_keys = {"openai": "test-key", "openrouter": "router-key"}
|
||||
return app
|
||||
|
||||
def session(self, text, complete=True):
|
||||
return SimpleNamespace(
|
||||
result=SimpleNamespace(session_id="test", text=text, complete=complete,
|
||||
errors=[] if complete else ["failed"]),
|
||||
acknowledge_delivery=Mock(),
|
||||
release_for_retry=Mock(),
|
||||
)
|
||||
|
||||
@patch("blurt.pyperclip.copy")
|
||||
def test_results_delivered_in_recording_order(self, copy):
|
||||
app = self.app()
|
||||
first = self.session("first")
|
||||
final_result = first.result
|
||||
first.result = None
|
||||
second = self.session("second")
|
||||
app._live_sessions = [first, second]
|
||||
app._session_complete(second)
|
||||
copy.assert_not_called()
|
||||
first.result = final_result
|
||||
app._session_complete(first)
|
||||
self.assertEqual([c.args[0] for c in copy.call_args_list], ["first", "second"])
|
||||
self.assertEqual(app._live_sessions, [])
|
||||
|
||||
@patch("blurt.pyperclip.copy")
|
||||
def test_partial_agent_command_never_dispatched(self, copy):
|
||||
app = self.app()
|
||||
active = self.session("to my agent change […]", complete=False)
|
||||
app._live_sessions = [active]
|
||||
app._session_complete(active)
|
||||
copy.assert_called_once_with("to my agent change […]")
|
||||
app.platform.dispatch.assert_not_called()
|
||||
|
||||
@patch("blurt.pyperclip.copy", side_effect=RuntimeError("clipboard unavailable"))
|
||||
def test_clipboard_failure_does_not_acknowledge(self, copy):
|
||||
app = self.app()
|
||||
active = self.session("keep these words")
|
||||
app._live_sessions = [active]
|
||||
app._session_complete(active)
|
||||
active.acknowledge_delivery.assert_not_called()
|
||||
active.release_for_retry.assert_called_once()
|
||||
|
||||
@patch("blurt.pyperclip.copy")
|
||||
def test_capture_failure_is_not_delivered_until_microphone_stops(self, copy):
|
||||
app = self.app()
|
||||
active = self.session("some words […]", complete=False)
|
||||
active.stop = Mock()
|
||||
app._recording_session = active
|
||||
app._live_sessions = [active]
|
||||
app.stream = Mock()
|
||||
app.recording = True
|
||||
app._session_complete(active)
|
||||
copy.assert_not_called()
|
||||
app._stop_streaming()
|
||||
copy.assert_called_once_with("some words […]")
|
||||
|
||||
@patch("blurt.pyperclip.copy")
|
||||
def test_quitting_keeps_completed_recording_for_recovery(self, copy):
|
||||
app = self.app()
|
||||
app._quitting = True
|
||||
active = self.session("keep these words")
|
||||
app._live_sessions = [active]
|
||||
app._session_complete(active)
|
||||
copy.assert_not_called()
|
||||
active.acknowledge_delivery.assert_not_called()
|
||||
|
||||
def test_settings_snapshot_survives_menu_changes(self):
|
||||
app = self.app()
|
||||
config = app._transcription_config()
|
||||
app.transcription_model = "another-model"
|
||||
app.transcription_language = "de"
|
||||
app.api_keys["openai"] = "replacement"
|
||||
self.assertEqual((config.model, config.language, config.api_key),
|
||||
("whisper-1", "en", "test-key"))
|
||||
|
||||
@patch("blurt.pyperclip.copy")
|
||||
def test_retries_collect_oldest_first_and_never_dispatch(self, copy):
|
||||
app = self.app()
|
||||
app._retrying = True
|
||||
app._scheduler = Mock()
|
||||
app._retryable_files = Mock(return_value=[])
|
||||
app._recovery = Mock()
|
||||
newer = SimpleNamespace(created_at=20, config={"language": "de"})
|
||||
older = SimpleNamespace(created_at=10, config={"language": "en"})
|
||||
app._recovery.discover.return_value = [newer, older]
|
||||
first = self.session("to my agent first")
|
||||
second = self.session("second")
|
||||
first.join = Mock(return_value=first.result)
|
||||
second.join = Mock(return_value=second.result)
|
||||
app._recovery.resume.side_effect = [first, second]
|
||||
app._retry_sessions()
|
||||
copy.assert_called_once_with("to my agent first\n\nsecond")
|
||||
app.platform.dispatch.assert_not_called()
|
||||
self.assertIs(app._recovery.resume.call_args_list[0].args[0], older)
|
||||
first.acknowledge_delivery.assert_called_once()
|
||||
second.acknowledge_delivery.assert_called_once()
|
||||
self.assertFalse(app._retrying)
|
||||
|
||||
@patch("blurt.pyperclip.copy", side_effect=RuntimeError("unavailable"))
|
||||
def test_retry_clipboard_failure_releases_records_without_deleting(self, copy):
|
||||
app = self.app()
|
||||
app._retrying = True
|
||||
app._scheduler = Mock()
|
||||
app._retryable_files = Mock(return_value=[])
|
||||
app._recovery = Mock()
|
||||
app._recovery.discover.return_value = [SimpleNamespace(created_at=10, config={})]
|
||||
active = self.session("recovered words")
|
||||
active.join = Mock(return_value=active.result)
|
||||
app._recovery.resume.return_value = active
|
||||
with self.assertLogs(level="ERROR"):
|
||||
app._retry_sessions()
|
||||
active.acknowledge_delivery.assert_not_called()
|
||||
active.release_for_retry.assert_called_once()
|
||||
|
||||
def test_callback_routes_to_owning_session_and_stop_drains_first(self):
|
||||
app = self.app()
|
||||
order = []
|
||||
active = Mock()
|
||||
active.stop.side_effect = lambda: order.append("session-eof")
|
||||
app._recording_session = active
|
||||
app.recording = True
|
||||
app.stream = Mock()
|
||||
app.stream.stop.side_effect = lambda: order.append("stream-stop")
|
||||
app.stream.close.side_effect = lambda: order.append("stream-close")
|
||||
frames = object()
|
||||
app._audio_callback(frames, 0, None, None)
|
||||
active.feed.assert_called_once_with(frames)
|
||||
app._stop_streaming()
|
||||
self.assertEqual(order, ["stream-stop", "stream-close", "session-eof"])
|
||||
self.assertIsNone(app._recording_session)
|
||||
self.assertFalse(app.recording)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user