Skip to content

Commit 9d09c5c

Browse files
gambletanclaude
andcommitted
test: strengthen test suite to 208 tests
Added edge cases and real-world scenario tests across all modules: - Adapter tests: constructor variants, parsing edge cases, status checks - Manager tests: broadcast failures, concurrent messages, error recovery - Bridge tests: exception handling, flag parsing, custom prefix, sequences - Memory tests: max_turns=1, long messages, middleware chains, SQLite ops - Rich tests: empty tables, URL buttons, code blocks, cross-platform rendering - Streaming tests: empty iterators, sequential streams, error cancellation - Integration tests: full pipeline with access+memory+commands, bridge E2E Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 91254a5 commit 9d09c5c

File tree

7 files changed

+1487
-0
lines changed

7 files changed

+1487
-0
lines changed

tests/test_adapters_unit.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)