"""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.clear_session") def test_clear_active_session(self, mock_clear): mock_clear.return_value = True response, is_cmd = route_message("ch-1", "user-1", "/clear") assert response == "Session cleared." assert is_cmd is True mock_clear.assert_called_once_with("ch-1") @patch("src.router.clear_session") def test_clear_no_session(self, mock_clear): mock_clear.return_value = False response, is_cmd = route_message("ch-1", "user-1", "/clear") assert response == "No active session." assert is_cmd is True # --- /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") @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") @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 # --- _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") @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") @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")