"""Tests for SMS (Twilio) platform integration. Covers config loading, format/truncate, echo prevention, requirements check, and toolset verification. """ import os from unittest.mock import patch import pytest from gateway.config import Platform, PlatformConfig, HomeChannel # ── Config loading ────────────────────────────────────────────────── class TestSmsConfigLoading: """Verify _apply_env_overrides wires SMS correctly.""" def test_sms_platform_enum_exists(self): assert Platform.SMS.value == "sms" def test_env_overrides_create_sms_config(self): from gateway.config import load_gateway_config env = { "TWILIO_ACCOUNT_SID": "ACtest123", "TWILIO_AUTH_TOKEN": "token_abc", "TWILIO_PHONE_NUMBER": "+15551234567", } with patch.dict(os.environ, env, clear=False): config = load_gateway_config() assert Platform.SMS in config.platforms pc = config.platforms[Platform.SMS] assert pc.enabled is True assert pc.api_key == "token_abc" def test_env_overrides_set_home_channel(self): from gateway.config import load_gateway_config env = { "TWILIO_ACCOUNT_SID": "ACtest123", "TWILIO_AUTH_TOKEN": "token_abc", "TWILIO_PHONE_NUMBER": "+15551234567", "SMS_HOME_CHANNEL": "+15559876543", "SMS_HOME_CHANNEL_NAME": "My Phone", } with patch.dict(os.environ, env, clear=False): config = load_gateway_config() hc = config.platforms[Platform.SMS].home_channel assert hc is not None assert hc.chat_id == "+15559876543" assert hc.name == "My Phone" assert hc.platform == Platform.SMS def test_sms_in_connected_platforms(self): from gateway.config import load_gateway_config env = { "TWILIO_ACCOUNT_SID": "ACtest123", "TWILIO_AUTH_TOKEN": "token_abc", } with patch.dict(os.environ, env, clear=False): config = load_gateway_config() connected = config.get_connected_platforms() assert Platform.SMS in connected # ── Format / truncate ─────────────────────────────────────────────── class TestSmsFormatAndTruncate: """Test SmsAdapter.format_message strips markdown.""" def _make_adapter(self): from gateway.platforms.sms import SmsAdapter env = { "TWILIO_ACCOUNT_SID": "ACtest", "TWILIO_AUTH_TOKEN": "tok", "TWILIO_PHONE_NUMBER": "+15550001111", } with patch.dict(os.environ, env): pc = PlatformConfig(enabled=True, api_key="tok") adapter = object.__new__(SmsAdapter) adapter.config = pc adapter._platform = Platform.SMS adapter._account_sid = "ACtest" adapter._auth_token = "tok" adapter._from_number = "+15550001111" return adapter def test_strips_bold(self): adapter = self._make_adapter() assert adapter.format_message("**hello**") == "hello" def test_strips_italic(self): adapter = self._make_adapter() assert adapter.format_message("*world*") == "world" def test_strips_code_blocks(self): adapter = self._make_adapter() result = adapter.format_message("```python\nprint('hi')\n```") assert "```" not in result assert "print('hi')" in result def test_strips_inline_code(self): adapter = self._make_adapter() assert adapter.format_message("`code`") == "code" def test_strips_headers(self): adapter = self._make_adapter() assert adapter.format_message("## Title") == "Title" def test_strips_links(self): adapter = self._make_adapter() assert adapter.format_message("[click](https://example.com)") == "click" def test_collapses_newlines(self): adapter = self._make_adapter() result = adapter.format_message("a\n\n\n\nb") assert result == "a\n\nb" # ── Echo prevention ──────────────────────────────────────────────── class TestSmsEchoPrevention: """Adapter should ignore messages from its own number.""" def test_own_number_detection(self): """The adapter stores _from_number for echo prevention.""" from gateway.platforms.sms import SmsAdapter env = { "TWILIO_ACCOUNT_SID": "ACtest", "TWILIO_AUTH_TOKEN": "tok", "TWILIO_PHONE_NUMBER": "+15550001111", } with patch.dict(os.environ, env): pc = PlatformConfig(enabled=True, api_key="tok") adapter = SmsAdapter(pc) assert adapter._from_number == "+15550001111" # ── Requirements check ───────────────────────────────────────────── class TestSmsRequirements: def test_check_sms_requirements_missing_sid(self): from gateway.platforms.sms import check_sms_requirements env = {"TWILIO_AUTH_TOKEN": "tok"} with patch.dict(os.environ, env, clear=True): assert check_sms_requirements() is False def test_check_sms_requirements_missing_token(self): from gateway.platforms.sms import check_sms_requirements env = {"TWILIO_ACCOUNT_SID": "ACtest"} with patch.dict(os.environ, env, clear=True): assert check_sms_requirements() is False def test_check_sms_requirements_both_set(self): from gateway.platforms.sms import check_sms_requirements env = { "TWILIO_ACCOUNT_SID": "ACtest", "TWILIO_AUTH_TOKEN": "tok", } with patch.dict(os.environ, env, clear=False): # Only returns True if aiohttp is also importable result = check_sms_requirements() try: import aiohttp # noqa: F401 assert result is True except ImportError: assert result is False # ── Toolset verification ─────────────────────────────────────────── class TestSmsToolset: def test_hermes_sms_toolset_exists(self): from toolsets import get_toolset ts = get_toolset("hermes-sms") assert ts is not None assert "tools" in ts def test_hermes_sms_in_gateway_includes(self): from toolsets import get_toolset gw = get_toolset("hermes-gateway") assert gw is not None assert "hermes-sms" in gw["includes"] def test_sms_platform_hint_exists(self): from agent.prompt_builder import PLATFORM_HINTS assert "sms" in PLATFORM_HINTS assert "concise" in PLATFORM_HINTS["sms"].lower() def test_sms_in_scheduler_platform_map(self): """Verify cron scheduler recognizes 'sms' as a valid platform.""" # Just check the Platform enum has SMS — the scheduler imports it dynamically assert Platform.SMS.value == "sms" def test_sms_in_send_message_platform_map(self): """Verify send_message_tool recognizes 'sms'.""" # The platform_map is built inside _handle_send; verify SMS enum exists assert hasattr(Platform, "SMS") def test_sms_in_cronjob_deliver_description(self): """Verify cronjob_tools mentions sms in deliver description.""" from tools.cronjob_tools import CRONJOB_SCHEMA deliver_desc = CRONJOB_SCHEMA["parameters"]["properties"]["deliver"]["description"] assert "sms" in deliver_desc.lower()