from __future__ import annotations import pytest from yuxi.channel.channels.hooks.adapter import HooksAdapter, HOOKS_CAPABILITIES from yuxi.channel.channels.hooks.config import HookMapping, HooksConfig from yuxi.channel.channels.hooks.translator import HooksTranslator from yuxi.channel.domain.model.shared.channel_type import ChannelType class TestHookMapping: def test_defaults(self) -> None: m = HookMapping(match_path="/test") assert m.match_path == "/test" assert m.match_source == "*" assert m.default_agent_id == 1 assert m.allowed_agent_ids == [] assert m.default_session_key == "main" assert m.allow_request_session_key is False assert m.allowed_session_key_prefixes == [] assert m.session_key_strategy == "main" assert m.deliver is True assert m.max_body_bytes == 256 * 1024 assert m.secret is None def test_with_values(self) -> None: m = HookMapping( match_path="/webhook", match_source="github", default_agent_id=5, secret="s3cret", max_body_bytes=1024, ) assert m.match_source == "github" assert m.default_agent_id == 5 assert m.secret == "s3cret" assert m.max_body_bytes == 1024 class TestHooksConfig: def test_defaults(self) -> None: config = HooksConfig() assert config.mappings == [] def test_with_mappings(self) -> None: m = HookMapping(match_path="/h1") config = HooksConfig(mappings=[m]) assert len(config.mappings) == 1 class TestHooksTranslator: def test_translate_basic(self) -> None: mapping = HookMapping(match_path="/test", default_agent_id=3) raw = {"id": "evt-1", "content": "hello"} msg = HooksTranslator.translate(raw, mapping) assert msg.message_id == "evt-1" assert msg.content == "hello" assert msg.channel_type == ChannelType.HOOKS assert msg.agent_config_id == 3 assert msg.metadata["source"] == "hooks" assert msg.metadata["match_path"] == "/test" def test_translate_uses_text_fallback(self) -> None: mapping = HookMapping(match_path="/test") raw = {"text": "fallback text"} msg = HooksTranslator.translate(raw, mapping) assert msg.content == "fallback text" def test_translate_agent_id_from_raw(self) -> None: mapping = HookMapping(match_path="/test", default_agent_id=1, allowed_agent_ids=[1, 2, 3]) raw = {"id": "e1", "content": "hi", "agent_id": 2} msg = HooksTranslator.translate(raw, mapping) assert msg.agent_config_id == 2 def test_translate_agent_id_not_allowed_falls_back(self) -> None: mapping = HookMapping(match_path="/test", default_agent_id=1, allowed_agent_ids=[1, 2]) raw = {"id": "e1", "content": "hi", "agent_id": 99} msg = HooksTranslator.translate(raw, mapping) assert msg.agent_config_id == 1 def test_translate_session_key_from_request(self) -> None: mapping = HookMapping( match_path="/test", allow_request_session_key=True, allowed_session_key_prefixes=["sess-"], ) raw = {"id": "e1", "content": "hi", "session_key": "sess-abc"} msg = HooksTranslator.translate(raw, mapping) assert msg.metadata["session_key"] == "sess-abc" def test_translate_session_key_prefix_not_allowed(self) -> None: mapping = HookMapping( match_path="/test", allow_request_session_key=True, allowed_session_key_prefixes=["valid-"], default_session_key="default", ) raw = {"id": "e1", "content": "hi", "session_key": "invalid-abc"} msg = HooksTranslator.translate(raw, mapping) assert msg.metadata["session_key"] == "default" class TestHooksAdapter: def test_channel_type(self) -> None: adapter = HooksAdapter() assert adapter.channel_type == ChannelType.HOOKS.value def test_capabilities(self) -> None: assert HOOKS_CAPABILITIES.media is False assert HOOKS_CAPABILITIES.dm is True assert HOOKS_CAPABILITIES.max_text_length == 32768 def test_get_default_config(self) -> None: config = HooksAdapter.get_default_config() assert "mappings" in config def test_match_hook_exact(self) -> None: adapter = HooksAdapter( mappings=[ {"match_path": "/webhook", "match_source": "github"}, ] ) result = adapter.match_hook("/webhook", "github") assert result is not None assert result.match_path == "/webhook" def test_match_hook_wildcard(self) -> None: adapter = HooksAdapter( mappings=[ {"match_path": "/hook1"}, ] ) result = adapter.match_hook("/hook1", "any-source") assert result is not None def test_match_hook_not_found(self) -> None: adapter = HooksAdapter( mappings=[ {"match_path": "/hook1"}, ] ) result = adapter.match_hook("/nonexistent") assert result is None @pytest.mark.asyncio async def test_is_healthy_no_mappings(self) -> None: adapter = HooksAdapter() await adapter.open() assert await adapter.is_healthy() is False @pytest.mark.asyncio async def test_is_healthy_with_mappings(self) -> None: adapter = HooksAdapter(mappings=[{"match_path": "/h1"}]) await adapter.open() assert await adapter.is_healthy() is True @pytest.mark.asyncio async def test_close(self) -> None: adapter = HooksAdapter(mappings=[{"match_path": "/h1"}]) await adapter.open() await adapter.close() assert await adapter.is_healthy() is False @pytest.mark.asyncio async def test_receive_message(self) -> None: adapter = HooksAdapter(mappings=[{"match_path": "/test"}]) raw = {"id": "e1", "content": "hi", "match_path": "/test"} msg = await adapter.receive_message(raw) assert msg.message_id == "e1" @pytest.mark.asyncio async def test_receive_message_no_mapping_raises(self) -> None: adapter = HooksAdapter(mappings=[{"match_path": "/other"}]) raw = {"id": "e1", "content": "hi", "match_path": "/nonexistent"} with pytest.raises(ValueError, match="no hook mapping"): await adapter.receive_message(raw) @pytest.mark.asyncio async def test_send_message_always_succeeds(self) -> None: adapter = HooksAdapter() result = await adapter.send_message("sess-1", "hello", channel_type="hooks", metadata={}) assert result.success is True @pytest.mark.asyncio async def test_send_media_returns_true(self) -> None: adapter = HooksAdapter() result = await adapter.send_media("sess-1", url="http://x", media_type="image", metadata={}) assert result is True def test_ws_connection_is_none(self) -> None: adapter = HooksAdapter() assert adapter.ws_connection is None def test_route_contributor_has_router(self) -> None: adapter = HooksAdapter() contributor = adapter.route_contributor assert contributor.router is not None