from __future__ import annotations import tempfile import unittest from email.message import EmailMessage from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from opc.channels.manager import ChannelManager from opc.channels.session import ChannelSessionMapping from opc.core.config import OPCConfig from opc.core.models import SystemMessage, UserMessage from opc.engine import OPCEngine from opc.layer0_interaction.message_bus import MessageBus def _stub_bus(): return SimpleNamespace(publish_inbound=None, publish_outbound=None) class ChannelRuntimeTests(unittest.IsolatedAsyncioTestCase): async def test_channel_manager_initializes_enabled_channels_and_dispatches(self) -> None: config = OPCConfig() config.channels.slack.enabled = True config.channels.slack.allow_from = ["*"] bus = MessageBus() sent: list[SystemMessage] = [] class _Channel: def __init__(self, cfg, bus): self.cfg = cfg self.bus = bus self._running = False async def start(self): self._running = True async def stop(self): self._running = False async def send(self, message: SystemMessage): sent.append(message) real_import = __import__ with patch("builtins.__import__") as mocked_import: def fake_import(name, globals=None, locals=None, fromlist=(), level=0): if name == "opc.channels.slack": return SimpleNamespace(SlackChannel=_Channel) return real_import(name, globals, locals, fromlist, level) mocked_import.side_effect = fake_import manager = ChannelManager(config, bus) self.assertEqual(manager.enabled_channels, ["slack"]) await manager.start_all() await manager.dispatch_outbound(SystemMessage(channel="slack", user_id="u", session_id="s", content="hello", metadata={"chat_id": "c1"})) await manager.stop_all() self.assertEqual(sent[0].content, "hello") self.assertEqual(sent[0].metadata["chat_id"], "c1") def test_config_load_save_roundtrip_includes_channels(self) -> None: config = OPCConfig() config.channels.feishu.enabled = True config.channels.feishu.app_id = "app" config.channels.feishu.allow_from = ["*"] with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) config.save(path) reloaded = OPCConfig.load(path) self.assertTrue(reloaded.channels.feishu.enabled) self.assertEqual(reloaded.channels.feishu.app_id, "app") async def test_user_message_metadata_is_preserved_in_response(self) -> None: bus = MessageBus() payload = UserMessage(channel="feishu", user_id="user1", content="hi", session_id="sess", metadata={"chat_id": "chat-1", "thread_id": "th-1"}) async def handler(message: UserMessage): return SystemMessage(channel=message.channel, user_id=message.user_id, session_id=message.session_id, content="ok", metadata={"chat_id": message.metadata.get("chat_id", "")}) bus.set_handler(handler) response = await bus.process_single(payload) assert response is not None self.assertEqual(response.metadata["chat_id"], "chat-1") async def test_base_channel_publish_inbound_normalizes_metadata_and_thread_session(self) -> None: from opc.channels.slack import SlackChannel config = OPCConfig() config.channels.slack.allow_from = ["*"] bus = MessageBus() channel = SlackChannel(config.channels.slack, bus) await channel.publish_inbound( sender_id="u-1", chat_id="c-1", content="hello", attachments=["a.png"], metadata={"thread_id": "th-9", "reply_to": "ts-1", "raw": {"x": 1}}, ) message = await bus._inbound_queue.get() self.assertEqual(message.session_id, "slack:c-1:th-9") self.assertEqual(message.metadata["sender_id"], "u-1") self.assertEqual(message.metadata["reply_to"], "ts-1") self.assertEqual(message.metadata["attachments"], ["a.png"]) def test_channel_session_mapping_prefers_thread_scope(self) -> None: route = ChannelSessionMapping.derive(channel="feishu", chat_id="chat-1", thread_id="thread-1") self.assertEqual(route.session_id, "feishu:chat-1:thread-1") self.assertEqual(route.thread_id, "thread-1") class ProviderSmokeTests(unittest.TestCase): def test_provider_registry_shape(self) -> None: config = OPCConfig() expected = ["telegram", "whatsapp", "discord", "feishu", "mochat", "dingtalk", "email", "slack", "qq", "matrix"] for name in expected: self.assertTrue(hasattr(config.channels, name)) def test_telegram_normalize_update(self) -> None: from opc.channels.telegram import TelegramChannel channel = TelegramChannel(OPCConfig().channels.telegram, _stub_bus()) normalized = channel.normalize_update({"message": {"text": "hi", "chat": {"id": 7}, "from": {"id": 9}, "message_thread_id": 88}}) self.assertEqual(normalized["chat_id"], "7") self.assertEqual(normalized["thread_id"], "88") def test_whatsapp_normalize_event(self) -> None: from opc.channels.whatsapp import WhatsAppChannel channel = WhatsAppChannel(OPCConfig().channels.whatsapp, _stub_bus()) normalized = channel.normalize_event({"message": {"from": "u1", "chat_id": "c1", "text": "hi"}}) self.assertEqual(normalized["sender_id"], "u1") self.assertEqual(normalized["chat_id"], "c1") def test_discord_normalize_event(self) -> None: from opc.channels.discord import DiscordChannel channel = DiscordChannel(OPCConfig().channels.discord, _stub_bus()) normalized = channel.normalize_event({"author": {"id": "u1"}, "channel_id": "c1", "content": "hi", "is_thread": True, "thread_id": "t1"}) self.assertEqual(normalized["thread_id"], "t1") def test_feishu_normalize_event(self) -> None: from opc.channels.feishu import FeishuChannel channel = FeishuChannel(OPCConfig().channels.feishu, _stub_bus()) normalized = channel.normalize_event({"event": {"sender": {"sender_id": {"open_id": "u1"}}, "message": {"chat_id": "c1", "content": "hi", "thread_id": "t1", "parent_id": "p1"}}}) self.assertEqual(normalized["sender_id"], "u1") self.assertEqual(normalized["reply_to"], "p1") def test_mochat_normalize_event(self) -> None: from opc.channels.mochat import MochatChannel channel = MochatChannel(OPCConfig().channels.mochat, _stub_bus()) normalized = channel.normalize_event({"message": {"from": "u1", "conversation_id": "c1", "text": "hi"}}) self.assertEqual(normalized["chat_id"], "c1") def test_dingtalk_normalize_event(self) -> None: from opc.channels.dingtalk import DingTalkChannel channel = DingTalkChannel(OPCConfig().channels.dingtalk, _stub_bus()) normalized = channel.normalize_event({"senderStaffId": "u1", "conversationId": "c1", "text": {"content": "hi"}, "msgId": "m1"}) self.assertEqual(normalized["reply_to"], "m1") def test_email_normalize_email(self) -> None: from opc.channels.email import EmailChannel channel = EmailChannel(OPCConfig().channels.email, _stub_bus()) normalized = channel.normalize_email({"from": "a@example.com", "thread_id": "th1", "body": "hi", "message_id": "m1"}) self.assertEqual(normalized["chat_id"], "th1") def test_email_parse_long_body_returns_marked_preview_metadata(self) -> None: from opc.channels.email import EmailChannel config = OPCConfig().channels.email config.max_body_chars = 20 channel = EmailChannel(config, _stub_bus()) message = EmailMessage() message["From"] = "a@example.com" message["Subject"] = "Long body" message["Message-ID"] = "" message.set_content("hello " * 20) with patch("opc.channels.email.persist_tool_result", return_value={"full_output_path": "/tmp/email-body.txt"}): parsed = channel._parse_message_bytes(message.as_bytes()) self.assertIsNotNone(parsed) assert parsed is not None self.assertTrue(parsed["body_truncated"]) self.assertGreater(parsed["body_omitted_chars"], 0) self.assertIn("email body preview truncated", parsed["body"]) self.assertTrue(parsed["full_body_path"]) def test_slack_normalize_event(self) -> None: from opc.channels.slack import SlackChannel channel = SlackChannel(OPCConfig().channels.slack, _stub_bus()) normalized = channel.normalize_event({"event": {"user": "u1", "channel": "c1", "text": "hi", "thread_ts": "t1", "ts": "m1"}}) self.assertEqual(normalized["thread_id"], "t1") def test_qq_normalize_event(self) -> None: from opc.channels.qq import QQChannel channel = QQChannel(OPCConfig().channels.qq, _stub_bus()) normalized = channel.normalize_event({"author": {"id": "u1"}, "group_openid": "g1", "content": "hi", "id": "m1"}) self.assertEqual(normalized["chat_id"], "g1") def test_matrix_normalize_event(self) -> None: from opc.channels.matrix import MatrixChannel channel = MatrixChannel(OPCConfig().channels.matrix, _stub_bus()) normalized = channel.normalize_event({"sender": "@u:server", "room_id": "!r:server", "event_id": "$e", "content": {"body": "hi", "m.relates_to": {"event_id": "$thread"}}}) self.assertEqual(normalized["chat_id"], "!r:server") self.assertEqual(normalized["thread_id"], "$thread") def test_socket_and_polling_channels_capture_outbound(self) -> None: from opc.channels.email import EmailChannel from opc.channels.slack import SlackChannel slack = SlackChannel(OPCConfig().channels.slack, _stub_bus()) email = EmailChannel(OPCConfig().channels.email, _stub_bus()) import asyncio asyncio.run(slack.send(SystemMessage(channel="slack", user_id="u", session_id="s", content="hello", metadata={"chat_id": "c1", "thread_id": "t1"}))) asyncio.run(email.send(SystemMessage(channel="email", user_id="u", session_id="s", content="hello", metadata={"chat_id": "mailbox"}))) self.assertEqual(slack.last_outbound["thread_id"], "t1") self.assertEqual(email.last_outbound["chat_id"], "mailbox") if __name__ == "__main__": unittest.main()