"""Tests for src/router.py — message router.""" import pytest from unittest.mock import MagicMock, patch from src.router import route_message, _get_channel_config @pytest.fixture(autouse=True) def reset_router_config(): """Reset the module-level _config before each test.""" import src.router original = src.router._config src.router._config = None yield src.router._config = original # --- /clear command --- class TestClearCommand: @patch("src.router._get_config") @patch("src.router.clear_session") def test_clear_active_session(self, mock_clear, mock_get_config): mock_clear.return_value = True mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "/clear") assert response == "Session cleared. Model reset to sonnet." assert is_cmd is True mock_clear.assert_called_once_with("ch-1") @patch("src.router._get_config") @patch("src.router.clear_session") def test_clear_no_session(self, mock_clear, mock_get_config): mock_clear.return_value = False mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "/clear") assert response == "No active session." assert is_cmd is True @patch("src.router._get_config") @patch("src.router.clear_session") def test_clear_mentions_model_reset(self, mock_clear, mock_get_config): mock_clear.return_value = True mock_cfg = MagicMock() mock_cfg.get.return_value = "opus" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "/clear") assert "model reset" in response.lower() assert "opus" in response # --- /model command --- class TestModelCommand: @patch("src.router.get_active_session") def test_model_show_current_with_session(self, mock_get): mock_get.return_value = {"model": "opus"} response, is_cmd = route_message("ch-1", "user-1", "/model") assert is_cmd is True assert "opus" in response assert "haiku" in response # available models listed @patch("src.router._get_config") @patch("src.router._get_channel_config") @patch("src.router.get_active_session") def test_model_show_current_no_session(self, mock_get, mock_chan_cfg, mock_get_config): mock_get.return_value = None mock_chan_cfg.return_value = None mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "/model") assert is_cmd is True assert "sonnet" in response @patch("src.router.set_session_model") @patch("src.router.get_active_session") def test_model_change_opus(self, mock_get, mock_set): mock_get.return_value = {"model": "sonnet", "session_id": "abc"} response, is_cmd = route_message("ch-1", "user-1", "/model opus") assert is_cmd is True mock_set.assert_called_once_with("ch-1", "opus") assert "opus" in response def test_model_invalid_choice(self): response, is_cmd = route_message("ch-1", "user-1", "/model gpt4") assert is_cmd is True assert "invalid" in response.lower() assert "gpt4" in response @patch("src.claude_session._save_sessions") @patch("src.claude_session._load_sessions") @patch("src.router.get_active_session") def test_model_change_no_session_presets(self, mock_get, mock_load, mock_save): mock_get.return_value = None mock_load.return_value = {} response, is_cmd = route_message("ch-1", "user-1", "/model haiku") assert is_cmd is True mock_save.assert_called_once() saved = mock_save.call_args[0][0] assert saved["ch-1"]["model"] == "haiku" assert "haiku" in response # --- /status command --- class TestStatusCommand: @patch("src.router.get_active_session") def test_status_active_session(self, mock_get): mock_get.return_value = { "model": "sonnet", "session_id": "abcdef123456789", "message_count": 5, } response, is_cmd = route_message("ch-1", "user-1", "/status") assert is_cmd is True assert "sonnet" in response assert "abcdef123456" in response # first 12 chars assert "5" in response @patch("src.router.get_active_session") def test_status_no_session(self, mock_get): mock_get.return_value = None response, is_cmd = route_message("ch-1", "user-1", "/status") assert response == "No active session." assert is_cmd is True # --- Unknown command --- class TestUnknownCommand: def test_unknown_command(self): response, is_cmd = route_message("ch-1", "user-1", "/foo") assert response == "Unknown command: /foo" assert is_cmd is True def test_unknown_command_with_args(self): response, is_cmd = route_message("ch-1", "user-1", "/bar baz") assert response == "Unknown command: /bar" assert is_cmd is True # --- Regular messages --- class TestRegularMessage: @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_sends_to_claude(self, mock_send, mock_get_config, mock_chan_cfg): mock_send.return_value = "Hello from Claude!" mock_chan_cfg.return_value = None mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "hello") assert response == "Hello from Claude!" assert is_cmd is False mock_send.assert_called_once_with("ch-1", "hello", model="sonnet", on_text=None) @patch("src.router.send_message") def test_model_override(self, mock_send): mock_send.return_value = "Response" response, is_cmd = route_message("ch-1", "user-1", "hello", model="opus") assert response == "Response" assert is_cmd is False mock_send.assert_called_once_with("ch-1", "hello", model="opus", on_text=None) @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_claude_error(self, mock_send, mock_get_config, mock_chan_cfg): mock_send.side_effect = RuntimeError("API timeout") mock_chan_cfg.return_value = None mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg response, is_cmd = route_message("ch-1", "user-1", "hello") assert "Error: API timeout" in response assert is_cmd is False @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_on_text_passed_through(self, mock_send, mock_get_config, mock_chan_cfg): mock_send.return_value = "ok" mock_chan_cfg.return_value = None mock_cfg = MagicMock() mock_cfg.get.return_value = "sonnet" mock_get_config.return_value = mock_cfg cb = lambda t: None route_message("ch-1", "user-1", "hello", on_text=cb) mock_send.assert_called_once_with("ch-1", "hello", model="sonnet", on_text=cb) # --- _get_channel_config --- class TestGetChannelConfig: @patch("src.router._get_config") def test_finds_by_id(self, mock_get_config): mock_cfg = MagicMock() mock_cfg.get.return_value = { "general": {"id": "ch-1", "default_model": "haiku"}, "dev": {"id": "ch-2"}, } mock_get_config.return_value = mock_cfg result = _get_channel_config("ch-1") assert result == {"id": "ch-1", "default_model": "haiku"} @patch("src.router._get_config") def test_returns_none_when_not_found(self, mock_get_config): mock_cfg = MagicMock() mock_cfg.get.return_value = {"general": {"id": "ch-1"}} mock_get_config.return_value = mock_cfg result = _get_channel_config("ch-999") assert result is None # --- Model resolution --- class TestModelResolution: @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_channel_default_model(self, mock_send, mock_get_config, mock_chan_cfg): """Channel config default_model takes priority.""" mock_send.return_value = "ok" mock_chan_cfg.return_value = {"id": "ch-1", "default_model": "haiku"} route_message("ch-1", "user-1", "hello") mock_send.assert_called_once_with("ch-1", "hello", model="haiku", on_text=None) @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_global_default_model(self, mock_send, mock_get_config, mock_chan_cfg): """Falls back to bot.default_model when channel has no default.""" mock_send.return_value = "ok" mock_chan_cfg.return_value = {"id": "ch-1"} # no default_model mock_cfg = MagicMock() mock_cfg.get.return_value = "opus" mock_get_config.return_value = mock_cfg route_message("ch-1", "user-1", "hello") mock_send.assert_called_once_with("ch-1", "hello", model="opus", on_text=None) @patch("src.router._get_channel_config") @patch("src.router._get_config") @patch("src.router.send_message") def test_sonnet_fallback(self, mock_send, mock_get_config, mock_chan_cfg): """Falls back to 'sonnet' when no channel or global default.""" mock_send.return_value = "ok" mock_chan_cfg.return_value = None mock_cfg = MagicMock() mock_cfg.get.side_effect = lambda key, default=None: default mock_get_config.return_value = mock_cfg route_message("ch-1", "user-1", "hello") mock_send.assert_called_once_with("ch-1", "hello", model="sonnet", on_text=None) @patch("src.router.get_active_session") @patch("src.router.send_message") def test_session_model_takes_priority(self, mock_send, mock_get_session): """Session model takes priority over channel and global defaults.""" mock_send.return_value = "ok" mock_get_session.return_value = {"model": "opus", "session_id": "abc"} route_message("ch-1", "user-1", "hello") mock_send.assert_called_once_with("ch-1", "hello", model="opus", on_text=None)