Spaces:
Paused
Paused
| """Tests for configurable background process notification modes. | |
| The gateway process watcher pushes status updates to users' chats when | |
| background terminal commands run. ``display.background_process_notifications`` | |
| controls verbosity: off | result | error | all (default). | |
| Contributed by @PeterFile (PR #593), reimplemented on current main. | |
| """ | |
| import asyncio | |
| from types import SimpleNamespace | |
| from unittest.mock import AsyncMock, patch | |
| import pytest | |
| from gateway.config import GatewayConfig, Platform | |
| from gateway.run import GatewayRunner | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| class _FakeRegistry: | |
| """Return pre-canned sessions, then None once exhausted.""" | |
| def __init__(self, sessions): | |
| self._sessions = list(sessions) | |
| def get(self, session_id): | |
| if self._sessions: | |
| return self._sessions.pop(0) | |
| return None | |
| def _build_runner(monkeypatch, tmp_path, mode: str) -> GatewayRunner: | |
| """Create a GatewayRunner with a fake config for the given mode.""" | |
| (tmp_path / "config.yaml").write_text( | |
| f"display:\n background_process_notifications: {mode}\n", | |
| encoding="utf-8", | |
| ) | |
| import gateway.run as gateway_run | |
| monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) | |
| runner = GatewayRunner(GatewayConfig()) | |
| adapter = SimpleNamespace(send=AsyncMock()) | |
| runner.adapters[Platform.TELEGRAM] = adapter | |
| return runner | |
| def _watcher_dict(session_id="proc_test", thread_id=""): | |
| d = { | |
| "session_id": session_id, | |
| "check_interval": 0, | |
| "platform": "telegram", | |
| "chat_id": "123", | |
| } | |
| if thread_id: | |
| d["thread_id"] = thread_id | |
| return d | |
| # --------------------------------------------------------------------------- | |
| # _load_background_notifications_mode unit tests | |
| # --------------------------------------------------------------------------- | |
| class TestLoadBackgroundNotificationsMode: | |
| def test_defaults_to_all(self, monkeypatch, tmp_path): | |
| import gateway.run as gw | |
| monkeypatch.setattr(gw, "_hermes_home", tmp_path) | |
| monkeypatch.delenv("HERMES_BACKGROUND_NOTIFICATIONS", raising=False) | |
| assert GatewayRunner._load_background_notifications_mode() == "all" | |
| def test_reads_config_yaml(self, monkeypatch, tmp_path): | |
| (tmp_path / "config.yaml").write_text( | |
| "display:\n background_process_notifications: error\n" | |
| ) | |
| import gateway.run as gw | |
| monkeypatch.setattr(gw, "_hermes_home", tmp_path) | |
| monkeypatch.delenv("HERMES_BACKGROUND_NOTIFICATIONS", raising=False) | |
| assert GatewayRunner._load_background_notifications_mode() == "error" | |
| def test_env_var_overrides_config(self, monkeypatch, tmp_path): | |
| (tmp_path / "config.yaml").write_text( | |
| "display:\n background_process_notifications: error\n" | |
| ) | |
| import gateway.run as gw | |
| monkeypatch.setattr(gw, "_hermes_home", tmp_path) | |
| monkeypatch.setenv("HERMES_BACKGROUND_NOTIFICATIONS", "off") | |
| assert GatewayRunner._load_background_notifications_mode() == "off" | |
| def test_false_value_maps_to_off(self, monkeypatch, tmp_path): | |
| (tmp_path / "config.yaml").write_text( | |
| "display:\n background_process_notifications: false\n" | |
| ) | |
| import gateway.run as gw | |
| monkeypatch.setattr(gw, "_hermes_home", tmp_path) | |
| monkeypatch.delenv("HERMES_BACKGROUND_NOTIFICATIONS", raising=False) | |
| assert GatewayRunner._load_background_notifications_mode() == "off" | |
| def test_invalid_value_defaults_to_all(self, monkeypatch, tmp_path): | |
| (tmp_path / "config.yaml").write_text( | |
| "display:\n background_process_notifications: banana\n" | |
| ) | |
| import gateway.run as gw | |
| monkeypatch.setattr(gw, "_hermes_home", tmp_path) | |
| monkeypatch.delenv("HERMES_BACKGROUND_NOTIFICATIONS", raising=False) | |
| assert GatewayRunner._load_background_notifications_mode() == "all" | |
| # --------------------------------------------------------------------------- | |
| # _run_process_watcher integration tests | |
| # --------------------------------------------------------------------------- | |
| async def test_run_process_watcher_respects_notification_mode( | |
| monkeypatch, tmp_path, mode, sessions, expected_calls, expected_fragment | |
| ): | |
| import tools.process_registry as pr_module | |
| monkeypatch.setattr(pr_module, "process_registry", _FakeRegistry(sessions)) | |
| # Patch asyncio.sleep to avoid real delays | |
| async def _instant_sleep(*_a, **_kw): | |
| pass | |
| monkeypatch.setattr(asyncio, "sleep", _instant_sleep) | |
| runner = _build_runner(monkeypatch, tmp_path, mode) | |
| adapter = runner.adapters[Platform.TELEGRAM] | |
| await runner._run_process_watcher(_watcher_dict()) | |
| assert adapter.send.await_count == expected_calls, ( | |
| f"mode={mode}: expected {expected_calls} sends, got {adapter.send.await_count}" | |
| ) | |
| if expected_fragment is not None: | |
| sent_message = adapter.send.await_args.args[1] | |
| assert expected_fragment in sent_message | |
| async def test_thread_id_passed_to_send(monkeypatch, tmp_path): | |
| """thread_id from watcher dict is forwarded as metadata to adapter.send().""" | |
| import tools.process_registry as pr_module | |
| sessions = [SimpleNamespace(output_buffer="done\n", exited=True, exit_code=0)] | |
| monkeypatch.setattr(pr_module, "process_registry", _FakeRegistry(sessions)) | |
| async def _instant_sleep(*_a, **_kw): | |
| pass | |
| monkeypatch.setattr(asyncio, "sleep", _instant_sleep) | |
| runner = _build_runner(monkeypatch, tmp_path, "all") | |
| adapter = runner.adapters[Platform.TELEGRAM] | |
| await runner._run_process_watcher(_watcher_dict(thread_id="42")) | |
| assert adapter.send.await_count == 1 | |
| _, kwargs = adapter.send.call_args | |
| assert kwargs["metadata"] == {"thread_id": "42"} | |
| async def test_no_thread_id_sends_no_metadata(monkeypatch, tmp_path): | |
| """When thread_id is empty, metadata should be None (general topic).""" | |
| import tools.process_registry as pr_module | |
| sessions = [SimpleNamespace(output_buffer="done\n", exited=True, exit_code=0)] | |
| monkeypatch.setattr(pr_module, "process_registry", _FakeRegistry(sessions)) | |
| async def _instant_sleep(*_a, **_kw): | |
| pass | |
| monkeypatch.setattr(asyncio, "sleep", _instant_sleep) | |
| runner = _build_runner(monkeypatch, tmp_path, "all") | |
| adapter = runner.adapters[Platform.TELEGRAM] | |
| await runner._run_process_watcher(_watcher_dict()) | |
| assert adapter.send.await_count == 1 | |
| _, kwargs = adapter.send.call_args | |
| assert kwargs["metadata"] is None | |