""" Fake platform factory for tests. Provides a fake platform adapter for tests that need inbound message injection and outbound message capture. """ from __future__ import annotations from unittest.mock import AsyncMock, Mock import typing import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.platform.entities as platform_entities class FakePlatform: """Fake platform adapter for unit and integration tests. Simulates platform behavior without real network calls: - Inbound text message construction - Group and private conversation identities - Mention-bot flag - Outbound text capture - Outbound file/image capture - Send failure simulation Does not start real platform adapters. Does not call IM platform SDKs. """ def __init__( self, *, bot_account_id: str = "test-bot", stream_output_supported: bool = False, raise_error: Exception = None, ): self.bot_account_id = bot_account_id self._stream_output_supported = stream_output_supported self._raise_error = raise_error # Captured outbound messages self._outbound_messages: list[dict] = [] self._outbound_chunks: list[dict] = [] # Registered listeners self._listeners: dict = {} def raises(self, error: Exception) -> "FakePlatform": """Configure platform to raise an error on send.""" self._raise_error = error return self def send_failure(self) -> "FakePlatform": """Configure platform to simulate send failure.""" return self.raises(Exception("Platform send failure")) def supports_streaming(self, supported: bool = True) -> "FakePlatform": """Configure whether streaming output is supported.""" self._stream_output_supported = supported return self def get_outbound_messages(self) -> list[dict]: """Get all captured outbound messages for assertions.""" return self._outbound_messages.copy() def get_outbound_chunks(self) -> list[dict]: """Get all captured outbound streaming chunks for assertions.""" return self._outbound_chunks.copy() def clear_outbound(self): """Clear captured outbound messages.""" self._outbound_messages.clear() self._outbound_chunks.clear() def last_message(self) -> dict | None: """Get the last captured outbound message.""" return self._outbound_messages[-1] if self._outbound_messages else None def last_chunk(self) -> dict | None: """Get the last captured streaming chunk.""" return self._outbound_chunks[-1] if self._outbound_chunks else None # ============== Inbound Message Construction ============== def create_friend_message( self, text: str, sender_id: typing.Union[int, str] = 12345, nickname: str = "TestUser", ) -> platform_events.FriendMessage: """Create an inbound friend (private) message event.""" sender = platform_entities.Friend( id=sender_id, nickname=nickname, remark=None, ) chain = platform_message.MessageChain([ platform_message.Plain(text=text), ]) return platform_events.FriendMessage( type="FriendMessage", sender=sender, message_chain=chain, time=1609459200, ) def create_group_message( self, text: str, sender_id: typing.Union[int, str] = 12345, sender_name: str = "TestUser", group_id: typing.Union[int, str] = 99999, group_name: str = "TestGroup", mention_bot: bool = False, ) -> platform_events.GroupMessage: """Create an inbound group message event. Args: text: Message text content sender_id: Sender user ID sender_name: Sender display name group_id: Group ID group_name: Group name mention_bot: If True, prepend @mention of bot account """ group = platform_entities.Group( id=group_id, name=group_name, permission=platform_entities.Permission.Member, ) sender = platform_entities.GroupMember( id=sender_id, member_name=sender_name, permission=platform_entities.Permission.Member, group=group, ) # Build message chain with optional mention components = [] if mention_bot: components.append(platform_message.At(target=self.bot_account_id)) components.append(platform_message.Plain(text=" ")) components.append(platform_message.Plain(text=text)) chain = platform_message.MessageChain(components) return platform_events.GroupMessage( type="GroupMessage", sender=sender, message_chain=chain, time=1609459200, ) def create_image_message( self, url: str = "https://example.com/image.png", text: str = "", sender_id: typing.Union[int, str] = 12345, is_group: bool = False, group_id: typing.Union[int, str] = 99999, ) -> platform_events.MessageEvent: """Create an inbound image message event.""" components = [] if text: components.append(platform_message.Plain(text=text)) components.append(platform_message.Image(url=url)) chain = platform_message.MessageChain(components) if is_group: return self.create_group_message("", sender_id, group_id=group_id) # Replace chain else: sender = platform_entities.Friend(id=sender_id, nickname="TestUser", remark=None) return platform_events.FriendMessage( type="FriendMessage", sender=sender, message_chain=chain, time=1609459200, ) # ============== Adapter Methods (Simulated) ============== async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain, ): """Simulate sending a message (captures for assertions).""" if self._raise_error: raise self._raise_error self._outbound_messages.append({ "type": "send", "target_type": target_type, "target_id": target_id, "message": message, }) async def reply_message( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, quote_origin: bool = False, ): """Simulate replying to a message (captures for assertions).""" if self._raise_error: raise self._raise_error self._outbound_messages.append({ "type": "reply", "source_type": message_source.type, "source": message_source, "message": message, "quote_origin": quote_origin, }) async def reply_message_chunk( self, message_source: platform_events.MessageEvent, bot_message: dict, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, ): """Simulate streaming reply (captures for assertions).""" if self._raise_error: raise self._raise_error self._outbound_chunks.append({ "type": "reply_chunk", "source_type": message_source.type, "source": message_source, "bot_message": bot_message, "message": message, "quote_origin": quote_origin, "is_final": is_final, }) async def is_stream_output_supported(self) -> bool: """Return whether streaming output is supported.""" return self._stream_output_supported def register_listener( self, event_type: typing.Type[platform_events.Event], callback: typing.Callable, ): """Register an event listener (stores for simulation).""" if event_type not in self._listeners: self._listeners[event_type] = [] self._listeners[event_type].append(callback) def unregister_listener( self, event_type: typing.Type[platform_events.Event], callback: typing.Callable, ): """Unregister an event listener.""" if event_type in self._listeners: self._listeners[event_type].remove(callback) async def run_async(self): """Simulate running the adapter (does nothing).""" pass async def kill(self) -> bool: """Simulate killing the adapter.""" return True async def is_muted(self, group_id: int) -> bool: """Simulate checking mute status.""" return False async def create_message_card( self, message_id: typing.Type[str, int], event: platform_events.MessageEvent, ) -> bool: """Simulate creating a message card.""" return False # ============== Simulation Helpers ============== async def simulate_inbound_event( self, event: platform_events.Event, ): """Simulate receiving an inbound event by calling registered listeners.""" listeners = self._listeners.get(type(event), []) for callback in listeners: await callback(event, self) def fake_platform( bot_account_id: str = "test-bot", stream_output_supported: bool = False, ) -> FakePlatform: """Create a FakePlatform instance.""" return FakePlatform( bot_account_id=bot_account_id, stream_output_supported=stream_output_supported, ) def fake_platform_with_streaming() -> FakePlatform: """Create a FakePlatform that supports streaming output.""" return FakePlatform(stream_output_supported=True) def fake_platform_with_failure() -> FakePlatform: """Create a FakePlatform that simulates send failure.""" return FakePlatform().send_failure() # ============== Mock Adapter (for Query) ============== def mock_platform_adapter(platform: FakePlatform = None) -> Mock: """Create a mock platform adapter using FakePlatform or a simple mock.""" if platform is None: platform = FakePlatform() adapter = Mock() adapter.bot_account_id = platform.bot_account_id adapter.reply_message = AsyncMock(side_effect=platform.reply_message) adapter.reply_message_chunk = AsyncMock(side_effect=platform.reply_message_chunk) adapter.send_message = AsyncMock(side_effect=platform.send_message) adapter.is_stream_output_supported = AsyncMock( return_value=platform._stream_output_supported ) adapter._fake_platform = platform # Store for assertions return adapter