From 2b494851a6dfadc197cc9ec5cffa98dafed41eb9 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Thu, 26 Feb 2026 16:40:14 -0300 Subject: [PATCH] Require cancel listener availability before entering recording state --- src/aman.py | 17 +++++++++++++++-- tests/test_aman.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/aman.py b/src/aman.py index adffaa7..61b28fb 100755 --- a/src/aman.py +++ b/src/aman.py @@ -77,11 +77,13 @@ class Daemon: self.vocabulary = VocabularyEngine(cfg.vocabulary) self._stt_hint_kwargs_cache: dict[str, Any] | None = None - def _arm_cancel_listener(self): + def _arm_cancel_listener(self) -> bool: try: self.desktop.start_cancel_listener(lambda: self.cancel_recording()) + return True except Exception as exc: logging.error("failed to start cancel listener: %s", exc) + return False def _disarm_cancel_listener(self): try: @@ -130,12 +132,23 @@ class Daemon: except Exception as exc: logging.error("record start failed: %s", exc) return + if not self._arm_cancel_listener(): + try: + stream.stop() + except Exception: + pass + try: + stream.close() + except Exception: + pass + logging.error("recording start aborted because cancel listener is unavailable") + return + self.stream = stream self.record = record prev = self.state self.state = State.RECORDING logging.debug("state: %s -> %s", prev, self.state) - self._arm_cancel_listener() logging.info("recording started") if self.timer: self.timer.cancel() diff --git a/tests/test_aman.py b/tests/test_aman.py index 50bbc3e..f38134d 100644 --- a/tests/test_aman.py +++ b/tests/test_aman.py @@ -15,14 +15,17 @@ from config import Config, VocabularyReplacement class FakeDesktop: - def __init__(self): + def __init__(self, *, fail_cancel_listener: bool = False): self.inject_calls = [] self.quit_calls = 0 self.cancel_listener_start_calls = 0 self.cancel_listener_stop_calls = 0 self.cancel_listener_callback = None + self.fail_cancel_listener = fail_cancel_listener def start_cancel_listener(self, callback) -> None: + if self.fail_cancel_listener: + raise RuntimeError("cancel listener unavailable") self.cancel_listener_start_calls += 1 self.cancel_listener_callback = callback @@ -93,6 +96,18 @@ class FakeAudio: self.size = size +class FakeStream: + def __init__(self): + self.stop_calls = 0 + self.close_calls = 0 + + def stop(self): + self.stop_calls += 1 + + def close(self): + self.close_calls += 1 + + class DaemonTests(unittest.TestCase): def _config(self) -> Config: cfg = Config() @@ -272,6 +287,21 @@ class DaemonTests(unittest.TestCase): self.assertEqual(desktop.cancel_listener_stop_calls, 1) self.assertIsNone(desktop.cancel_listener_callback) + @patch("aman.start_audio_recording") + def test_recording_does_not_start_when_cancel_listener_fails(self, start_mock): + stream = FakeStream() + start_mock.return_value = (stream, object()) + desktop = FakeDesktop(fail_cancel_listener=True) + daemon = self._build_daemon(desktop, FakeModel(), verbose=False) + + daemon.toggle() + + self.assertEqual(daemon.get_state(), aman.State.IDLE) + self.assertIsNone(daemon.stream) + self.assertIsNone(daemon.record) + self.assertEqual(stream.stop_calls, 1) + self.assertEqual(stream.close_calls, 1) + class LockTests(unittest.TestCase): def test_lock_rejects_second_instance(self):