@@ -441,3 +441,239 @@ async def test_status(self, adapter):
441441 status = await adapter .get_status ()
442442 assert status .connected is False
443443 assert status .account_id == "bot"
444+
445+
446+ # ── IRC adapter edge cases ─────────────────────────────────────────────────
447+
448+ class TestIRCAdapterEdgeCases :
449+ def test_custom_channel_list (self ):
450+ from unified_channel .adapters .irc import IRCAdapter
451+ adapter = IRCAdapter (server = "irc.test.com" , nickname = "bot" , channels = ["#a" , "#b" ])
452+ assert adapter .channel_id == "irc"
453+
454+ def test_default_nickname (self ):
455+ from unified_channel .adapters .irc import IRCAdapter
456+ adapter = IRCAdapter (server = "irc.test.com" )
457+ assert adapter ._nickname is not None
458+
459+ @pytest .mark .asyncio
460+ async def test_process_empty_message (self ):
461+ from unified_channel .adapters .irc import IRCAdapter
462+ adapter = IRCAdapter (server = "irc.test.com" , nickname = "bot" )
463+ adapter ._connected = True
464+ # Non-PRIVMSG lines should be silently ignored
465+ await adapter ._process_line (":server 001 bot :Welcome" )
466+ assert adapter ._queue .empty ()
467+
468+ @pytest .mark .asyncio
469+ async def test_command_with_multiple_args (self ):
470+ from unified_channel .adapters .irc import IRCAdapter
471+ adapter = IRCAdapter (server = "irc.test.com" , command_prefix = "!" )
472+ adapter ._connected = True
473+
474+ await adapter ._process_line (":alice!a@h PRIVMSG #chan :!deploy prod --force --verbose" )
475+ msg = await asyncio .wait_for (adapter ._queue .get (), timeout = 1 )
476+ assert msg .content .type == ContentType .COMMAND
477+ assert msg .content .command == "deploy"
478+ assert "prod" in msg .content .args
479+
480+
481+ # ── WhatsApp adapter edge cases ─────────────────────────────────────────────
482+
483+ class TestWhatsAppEdgeCases :
484+ @pytest .fixture
485+ def adapter (self ):
486+ mock_httpx = MagicMock ()
487+ with patch .dict (sys .modules , {"httpx" : mock_httpx , "aiohttp" : MagicMock (), "aiohttp.web" : MagicMock ()}):
488+ from unified_channel .adapters .whatsapp import WhatsAppAdapter
489+ return WhatsAppAdapter (
490+ access_token = "test" ,
491+ phone_number_id = "123" ,
492+ verify_token = "verify" ,
493+ )
494+
495+ def test_channel_id (self , adapter ):
496+ assert adapter .channel_id == "whatsapp"
497+
498+ @pytest .mark .asyncio
499+ async def test_status_disconnected (self , adapter ):
500+ status = await adapter .get_status ()
501+ assert status .connected is False
502+ assert status .channel == "whatsapp"
503+
504+ @pytest .mark .asyncio
505+ async def test_process_command_with_args (self , adapter ):
506+ wa_msg = {
507+ "id" : "wamid.cmd" ,
508+ "from" : "+1234567890" ,
509+ "type" : "text" ,
510+ "timestamp" : "1700000000" ,
511+ "text" : {"body" : "/deploy staging --force" },
512+ }
513+ await adapter ._process_message (wa_msg , {})
514+ msg = await asyncio .wait_for (adapter ._queue .get (), timeout = 1 )
515+ assert msg .content .type == ContentType .COMMAND
516+ assert msg .content .command == "deploy"
517+ assert "staging" in msg .content .args
518+
519+
520+ # ── Mattermost adapter edge cases ──────────────────────────────────────────
521+
522+ class TestMattermostEdgeCases :
523+ @pytest .fixture
524+ def adapter (self ):
525+ with patch .dict (sys .modules , {"httpx" : MagicMock (), "websockets" : MagicMock ()}):
526+ from unified_channel .adapters .mattermost import MattermostAdapter
527+ a = MattermostAdapter (url = "https://mm.test" , token = "tok" )
528+ a ._bot_user_id = "bot123"
529+ a ._connected = True
530+ return a
531+
532+ def test_channel_id (self , adapter ):
533+ assert adapter .channel_id == "mattermost"
534+
535+ @pytest .mark .asyncio
536+ async def test_status_connected (self , adapter ):
537+ status = await adapter .get_status ()
538+ assert status .connected is True
539+ assert status .channel == "mattermost"
540+
541+ @pytest .mark .asyncio
542+ async def test_empty_post_data_produces_empty_message (self , adapter ):
543+ """Event with no post data still produces a message (adapter doesn't filter events)."""
544+ event = {"event" : "posted" , "data" : {}}
545+ await adapter ._process_post (event )
546+ msg = await asyncio .wait_for (adapter ._queue .get (), timeout = 1 )
547+ assert msg .channel == "mattermost"
548+ assert msg .content .text == ""
549+
550+
551+ # ── Twitch adapter edge cases ──────────────────────────────────────────────
552+
553+ class TestTwitchEdgeCases :
554+ @pytest .fixture
555+ def adapter (self ):
556+ with patch .dict (sys .modules , {"websockets" : MagicMock ()}):
557+ from unified_channel .adapters .twitch import TwitchAdapter
558+ a = TwitchAdapter (
559+ oauth_token = "oauth:test" , bot_username = "testbot" ,
560+ channels = ["#testchan" ], command_prefix = "!" ,
561+ )
562+ a ._connected = True
563+ return a
564+
565+ def test_channel_id (self , adapter ):
566+ assert adapter .channel_id == "twitch"
567+
568+ @pytest .mark .asyncio
569+ async def test_status_connected (self , adapter ):
570+ status = await adapter .get_status ()
571+ assert status .connected is True
572+ assert status .channel == "twitch"
573+
574+ @pytest .mark .asyncio
575+ async def test_process_multiword_message (self , adapter ):
576+ line = ":user1!user1@user1.tmi.twitch.tv PRIVMSG #testchan :this is a long message with many words"
577+ await adapter ._process_line (line )
578+ msg = await asyncio .wait_for (adapter ._queue .get (), timeout = 1 )
579+ assert msg .content .text == "this is a long message with many words"
580+ assert msg .content .type == ContentType .TEXT
581+
582+ @pytest .mark .asyncio
583+ async def test_non_privmsg_ignored (self , adapter ):
584+ line = ":tmi.twitch.tv 001 testbot :Welcome, GLHF!"
585+ await adapter ._process_line (line )
586+ assert adapter ._queue .empty ()
587+
588+
589+ # ── Zalo adapter edge cases ────────────────────────────────────────────────
590+
591+ class TestZaloEdgeCases :
592+ @pytest .fixture
593+ def adapter (self ):
594+ with patch .dict (sys .modules , {"httpx" : MagicMock (), "aiohttp" : MagicMock (), "aiohttp.web" : MagicMock ()}):
595+ from unified_channel .adapters .zalo import ZaloAdapter
596+ return ZaloAdapter (access_token = "tok" )
597+
598+ def test_channel_id (self , adapter ):
599+ assert adapter .channel_id == "zalo"
600+
601+ @pytest .mark .asyncio
602+ async def test_status_disconnected (self , adapter ):
603+ status = await adapter .get_status ()
604+ assert status .connected is False
605+ assert status .channel == "zalo"
606+
607+
608+ # ── BlueBubbles adapter edge cases ─────────────────────────────────────────
609+
610+ class TestBlueBubblesEdgeCases :
611+ @pytest .fixture
612+ def adapter (self ):
613+ with patch .dict (sys .modules , {"httpx" : MagicMock ()}):
614+ from unified_channel .adapters .bluebubbles import BlueBubblesAdapter
615+ return BlueBubblesAdapter (server_url = "http://localhost:1234" , password = "pw" )
616+
617+ def test_channel_id_default (self , adapter ):
618+ assert adapter .channel_id == "bluebubbles"
619+
620+ @pytest .mark .asyncio
621+ async def test_status_disconnected (self , adapter ):
622+ status = await adapter .get_status ()
623+ assert status .connected is False
624+ assert status .channel == "bluebubbles"
625+
626+
627+ # ── Synology Chat edge cases ──────────────────────────────────────────────
628+
629+ class TestSynologyChatEdgeCases :
630+ @pytest .fixture
631+ def adapter (self ):
632+ with patch .dict (sys .modules , {"httpx" : MagicMock (), "aiohttp" : MagicMock (), "aiohttp.web" : MagicMock ()}):
633+ from unified_channel .adapters .synology_chat import SynologyChatAdapter
634+ return SynologyChatAdapter (incoming_webhook_url = "https://nas/hook" )
635+
636+ @pytest .mark .asyncio
637+ async def test_status_has_correct_channel (self , adapter ):
638+ status = await adapter .get_status ()
639+ assert status .channel == "synology-chat"
640+ assert status .connected is False
641+
642+
643+ # ── Nextcloud Talk edge cases ──────────────────────────────────────────────
644+
645+ class TestNextcloudTalkEdgeCases :
646+ @pytest .fixture
647+ def adapter (self ):
648+ with patch .dict (sys .modules , {"httpx" : MagicMock ()}):
649+ from unified_channel .adapters .nextcloud_talk import NextcloudTalkAdapter
650+ return NextcloudTalkAdapter (
651+ server_url = "https://nc.test" ,
652+ username = "admin" , password = "secret" ,
653+ )
654+
655+ def test_custom_username_in_status (self , adapter ):
656+ # account_id should reflect the username
657+ assert adapter ._username == "admin"
658+
659+ @pytest .mark .asyncio
660+ async def test_status_account_id (self , adapter ):
661+ status = await adapter .get_status ()
662+ assert status .account_id == "admin"
663+
664+
665+ # ── iMessage adapter edge cases ───────────────────────────────────────────
666+
667+ @pytest .mark .skipif (sys .platform != "darwin" , reason = "macOS only" )
668+ class TestIMessageEdgeCases :
669+ def test_default_construction (self ):
670+ from unified_channel .adapters .imessage import IMessageAdapter
671+ adapter = IMessageAdapter ()
672+ assert adapter .channel_id == "imessage"
673+
674+ @pytest .mark .asyncio
675+ async def test_status_channel_field (self ):
676+ from unified_channel .adapters .imessage import IMessageAdapter
677+ adapter = IMessageAdapter ()
678+ status = await adapter .get_status ()
679+ assert status .channel == "imessage"
0 commit comments