"""Cron media-send timeout resolution and failure-reason formatting. Covers two salvaged fixes: - PR #87965 (@AiwendilInTheWoods): an argument-less exception (notably TimeoutError from ``future.result(timeout=...)``) has an empty ``str()``, which used to render "failed to send media : " with no reason at all — in both the log line and the delivery error recorded on the run. - PR #87967 (@AiwendilInTheWoods): the per-attachment send timeout was a hardcoded 30s; large attachments (long TTS audio, big exports) failed on slow uplinks with no way to raise it. Now resolved via HERMES_CRON_MEDIA_SEND_TIMEOUT → cron.media_send_timeout_seconds → 300s. """ import pytest from cron.scheduler import ( _DEFAULT_MEDIA_SEND_TIMEOUT, _get_media_send_timeout, _send_media_via_adapter, ) class TestMediaSendTimeoutResolution: def test_default(self, monkeypatch): monkeypatch.delenv("HERMES_CRON_MEDIA_SEND_TIMEOUT", raising=False) monkeypatch.setattr("cron.scheduler.load_config", lambda: {}) assert _get_media_send_timeout() == _DEFAULT_MEDIA_SEND_TIMEOUT == 300 def test_env_wins(self, monkeypatch): monkeypatch.setenv("HERMES_CRON_MEDIA_SEND_TIMEOUT", "45") monkeypatch.setattr( "cron.scheduler.load_config", lambda: {"cron": {"media_send_timeout_seconds": 900}}, ) assert _get_media_send_timeout() == 45 def test_config_value(self, monkeypatch): monkeypatch.delenv("HERMES_CRON_MEDIA_SEND_TIMEOUT", raising=False) monkeypatch.setattr( "cron.scheduler.load_config", lambda: {"cron": {"media_send_timeout_seconds": 900}}, ) assert _get_media_send_timeout() == 900 @pytest.mark.parametrize("bad", ["abc", "-5", "0", ""]) def test_invalid_env_falls_back(self, monkeypatch, bad): monkeypatch.setenv("HERMES_CRON_MEDIA_SEND_TIMEOUT", bad) monkeypatch.setattr("cron.scheduler.load_config", lambda: {}) assert _get_media_send_timeout() == _DEFAULT_MEDIA_SEND_TIMEOUT def test_invalid_config_falls_back(self, monkeypatch): monkeypatch.delenv("HERMES_CRON_MEDIA_SEND_TIMEOUT", raising=False) monkeypatch.setattr( "cron.scheduler.load_config", lambda: {"cron": {"media_send_timeout_seconds": "nope"}}, ) assert _get_media_send_timeout() == _DEFAULT_MEDIA_SEND_TIMEOUT class TestEmptyReasonFallback: def _run(self, tmp_path, monkeypatch, exc): """Drive _send_media_via_adapter into its generic except handler.""" media = tmp_path / "clip.mp3" media.write_bytes(b"x") monkeypatch.setattr( "gateway.platforms.base.BasePlatformAdapter.filter_media_delivery_paths", staticmethod(lambda files, session_key="": [(str(media), False)]), ) def boom(coro, loop): coro.close() raise exc monkeypatch.setattr("agent.async_utils.safe_schedule_threadsafe", boom) class _Adapter: async def send_voice(self, **kw): # pragma: no cover - never awaited pass errors = _send_media_via_adapter( _Adapter(), "C123", [(str(media), False)], None, loop=object(), job={"id": "job-x"}, ) assert len(errors) == 1 return errors[0] def test_timeout_error_names_the_class(self, tmp_path, monkeypatch): # TimeoutError() has an empty str() — the recorded reason must not # be blank (the trailing-colon-nothing log from the field report). err = self._run(tmp_path, monkeypatch, TimeoutError()) assert err.rstrip() != f"failed to send media {tmp_path / 'clip.mp3'}:" assert "TimeoutError" in err def test_exception_with_message_keeps_it(self, tmp_path, monkeypatch): err = self._run(tmp_path, monkeypatch, RuntimeError("bridge closed")) assert "bridge closed" in err