ForcePilot/backend/test/unit/channels/test_twitch_actions.py

357 lines
14 KiB
Python
Raw Normal View History

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.twitch.actions import (
_build_response,
describe_message_tool,
extract_target_from_args,
get_action_stats,
handle_action,
resolve_execution_mode,
supports_action,
)
from yuxi.channels.models import ChannelType, DeliveryResult
from yuxi.channels.protocols.actions import ActionContext
def make_adapter_mock(**kwargs):
adapter = MagicMock()
adapter.channel_type = ChannelType.TWITCH
adapter.config = kwargs.get("config", {})
adapter._helix = kwargs.get("_helix", None)
adapter._bot_user_id = kwargs.get("_bot_user_id", None)
adapter._outbound_cache = kwargs.get("_outbound_cache", MagicMock())
adapter._resolve_broadcaster_id = AsyncMock(return_value=kwargs.get("broadcaster_id", "123"))
adapter._send_via_helix = AsyncMock(return_value=DeliveryResult(success=True))
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
adapter.send_media = AsyncMock(return_value=DeliveryResult(success=True))
adapter.format_outbound = MagicMock(return_value={"target": "#test_channel"})
return adapter
def make_ctx(**kwargs):
action = kwargs.pop("action", "send")
chat_id = kwargs.pop("chat_id", "#test_channel")
msg_id = kwargs.pop("msg_id", "msg_test_123")
args = dict(kwargs)
if "chat_id" not in args:
args["chat_id"] = chat_id
if "msg_id" not in args:
args["msg_id"] = msg_id
return ActionContext(
action=action,
channel_id="twitch",
chat_id=chat_id,
msg_id=msg_id,
args=args,
)
class TestBuildResponse:
def test_builds_response(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", content="Hello")
response = _build_response(ctx, adapter)
assert response is not None
assert response.content == "Hello"
assert response.identity.channel_chat_id == "#test_channel"
def test_returns_none_when_no_chat_id(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", chat_id="", content="Hello")
response = _build_response(ctx, adapter)
assert response is None
def test_returns_none_when_no_content(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", chat_id="#test", content="")
response = _build_response(ctx, adapter)
assert response is None
def test_uses_media_url_for_send_media(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send_media", content_key="media_url", chat_id="#test", media_url="http://img.png")
response = _build_response(ctx, adapter, "media_url")
assert response is not None
assert response.content == "http://img.png"
class TestHandleActionSend:
@pytest.mark.asyncio
async def test_send_action(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", content="Hello world")
result = await handle_action(ctx, adapter)
assert result.success is True
@pytest.mark.asyncio
async def test_send_missing_chat_id(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", chat_id="", content="Hello")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "chat_id" in result.error
@pytest.mark.asyncio
async def test_send_missing_content(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send", chat_id="#test", content="")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_send_via_helix_preferred(self):
adapter = make_adapter_mock(config={"prefer_helix_send": True})
adapter._send_via_helix = AsyncMock(return_value=DeliveryResult(success=True))
ctx = make_ctx(action="send", content="Hello")
result = await handle_action(ctx, adapter)
assert result.success is True
adapter._send_via_helix.assert_called_once()
@pytest.mark.asyncio
async def test_send_via_helix_falls_back_to_irc(self):
adapter = make_adapter_mock(config={"prefer_helix_send": True})
adapter._send_via_helix = AsyncMock(return_value=DeliveryResult(success=False, error="fail"))
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
ctx = make_ctx(action="send", content="Hello")
result = await handle_action(ctx, adapter)
assert result.success is True
class TestHandleActionSendMedia:
@pytest.mark.asyncio
async def test_send_media_action(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send_media", media_url="http://img.png")
result = await handle_action(ctx, adapter)
assert result.success is True
@pytest.mark.asyncio
async def test_send_media_missing_chat_id(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send_media", chat_id="", media_url="http://img.png")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_send_media_missing_url(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send_media", chat_id="#test", media_url="")
result = await handle_action(ctx, adapter)
assert result.success is False
class TestHandleActionReply:
@pytest.mark.asyncio
async def test_reply_action(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="reply", content="Reply text", reply_to_msg_id="msg123", reply_to_username="user1")
result = await handle_action(ctx, adapter)
assert result.success is True
adapter._send_via_helix.assert_called_once()
@pytest.mark.asyncio
async def test_reply_missing_chat_id(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="reply", chat_id="", content="Reply", reply_to_msg_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_reply_missing_content(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="reply", chat_id="#test", content="", reply_to_msg_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_reply_missing_reply_to_msg_id(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="reply", chat_id="#test", content="Reply", reply_to_msg_id="")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_reply_falls_back_to_irc_with_prefix(self):
adapter = make_adapter_mock()
adapter._send_via_helix = AsyncMock(return_value=DeliveryResult(success=False, error="fail"))
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
ctx = make_ctx(action="reply", content="Reply text", reply_to_msg_id="msg123", reply_to_username="TestUser")
result = await handle_action(ctx, adapter)
assert result.success is True
assert "@TestUser" in adapter.send.call_args[0][0].content
class TestHandleActionUnsend:
@pytest.mark.asyncio
async def test_unsend_action(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id="bot123", broadcaster_id="12345")
adapter._helix.delete_chat_message = AsyncMock(return_value=True)
ctx = make_ctx(action="unsend", message_id="msg_delete_123")
result = await handle_action(ctx, adapter)
assert result.success is True
@pytest.mark.asyncio
async def test_unsend_missing_helix(self):
adapter = make_adapter_mock(_helix=None)
ctx = make_ctx(action="unsend", message_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_delete_message_action(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id="bot123", broadcaster_id="12345")
adapter._helix.delete_chat_message = AsyncMock(return_value=True)
ctx = make_ctx(action="delete_message", message_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is True
@pytest.mark.asyncio
async def test_unsend_no_broadcaster_id(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id="bot123", broadcaster_id=None)
adapter._resolve_broadcaster_id = AsyncMock(return_value=None)
adapter._helix.delete_chat_message = AsyncMock(return_value=True)
ctx = make_ctx(action="unsend", message_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "broadcaster" in result.error
@pytest.mark.asyncio
async def test_unsend_no_bot_user_id(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id=None, broadcaster_id="12345")
adapter._helix.delete_chat_message = AsyncMock(return_value=True)
ctx = make_ctx(action="unsend", message_id="msg123")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "bot_user_id" in result.error
class TestHandleActionAnnouncement:
@pytest.mark.asyncio
async def test_announcement_action(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id="bot123", broadcaster_id="12345")
adapter._helix.send_chat_announcement = AsyncMock(return_value=True)
ctx = make_ctx(action="announcement", content="Big news!", color="blue")
result = await handle_action(ctx, adapter)
assert result.success is True
@pytest.mark.asyncio
async def test_announcement_missing_helix(self):
adapter = make_adapter_mock(_helix=None)
ctx = make_ctx(action="announcement", content="News")
result = await handle_action(ctx, adapter)
assert result.success is False
@pytest.mark.asyncio
async def test_announcement_no_broadcaster_id(self):
adapter = make_adapter_mock(_helix=MagicMock(), _bot_user_id="bot123", broadcaster_id=None)
adapter._resolve_broadcaster_id = AsyncMock(return_value=None)
ctx = make_ctx(action="announcement", content="News")
result = await handle_action(ctx, adapter)
assert result.success is False
class TestHandleActionUnsupported:
@pytest.mark.asyncio
async def test_edit_action_unsupported(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="edit", content="new content")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "editing" in result.error
@pytest.mark.asyncio
async def test_reactions_unsupported(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="send_reaction")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "reactions" in result.error
@pytest.mark.asyncio
async def test_polls_unsupported(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="polls")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "polls" in result.error
@pytest.mark.asyncio
async def test_unknown_action(self):
adapter = make_adapter_mock()
ctx = make_ctx(action="some_random_action")
result = await handle_action(ctx, adapter)
assert result.success is False
assert "unknown action" in result.error
class TestSupportsAction:
def test_supported_actions(self):
for action in ("send", "send_media", "reply", "unsend", "delete_message", "announcement"):
assert supports_action(action) is True
def test_unsupported_actions(self):
for action in ("edit", "reactions", "polls", "pin"):
assert supports_action(action) is False
def test_unknown_action(self):
assert supports_action("random_action") is False
class TestDescribeMessageTool:
def test_returns_dict_with_all_actions(self):
result = describe_message_tool()
assert isinstance(result, dict)
assert "send" in result
assert "send_media" in result
assert "reply" in result
assert "unsend" in result
assert "delete_message" in result
assert "announcement" in result
def test_each_action_has_description(self):
result = describe_message_tool()
for action_info in result.values():
assert "description" in action_info
def test_each_action_has_parameters(self):
result = describe_message_tool()
for action_info in result.values():
assert "parameters" in action_info
class TestExtractTargetFromArgs:
def test_extracts_chat_id(self):
result = extract_target_from_args({"chat_id": "#test_channel"})
assert result == {"chat_id": "#test_channel"}
def test_returns_none_when_no_chat_id(self):
result = extract_target_from_args({"other_key": "value"})
assert result is None
def test_returns_none_for_empty_dict(self):
assert extract_target_from_args({}) is None
class TestResolveExecutionMode:
def test_always_returns_send(self):
assert resolve_execution_mode("send") == "send"
assert resolve_execution_mode("reply") == "send"
assert resolve_execution_mode("unknown") == "send"
class TestGetActionStats:
def test_returns_stats_dict(self):
result = get_action_stats()
assert "implemented" in result
assert "planned" in result
assert "unsupported" in result
def test_unimplemented_actions_in_unsupported(self):
result = get_action_stats()
assert "edit" in result["unsupported"]
assert "reactions" in result["unsupported"]
assert "polls" in result["unsupported"]