mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Implement a bridge for connecting channel bot conversations (Slack, Discord, Telegram) to the Chat UI. This feature includes message broadcasting, history persistence, and dedicated session handling for each channel. Documentation has been added to describe the architecture and performance characteristics.
196 lines
8.1 KiB
Python
196 lines
8.1 KiB
Python
"""End-to-end test for the channel-to-chat bridge.
|
|
|
|
Tests:
|
|
1. Server starts and API endpoints respond
|
|
2. Bridge attaches to a mock bot correctly
|
|
3. Channel messages broadcast via ChatManager and persist to datastore
|
|
4. Performance: bridge adds zero blocking overhead to the bot flow
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import time
|
|
import sys
|
|
|
|
async def run_tests():
|
|
results = []
|
|
|
|
# ── Test 1: Module imports ─────────────────────────────────────
|
|
print("\n═══ Test 1: Module Imports ═══")
|
|
try:
|
|
from praisonaiui.features.channels import PraisonAIChannels, _channels, _live_bots
|
|
from praisonaiui.features.chat import ChatManager, get_chat_manager, ChatMessage
|
|
results.append(("Module imports", True, ""))
|
|
print(" ✅ channels.py and chat.py imported OK")
|
|
except Exception as e:
|
|
results.append(("Module imports", False, str(e)))
|
|
print(f" ❌ Import failed: {e}")
|
|
return results
|
|
|
|
# ── Test 2: ChatManager broadcast ──────────────────────────────
|
|
print("\n═══ Test 2: ChatManager Broadcast ═══")
|
|
try:
|
|
mgr = get_chat_manager()
|
|
# Broadcast with no clients connected should be a no-op
|
|
await mgr.broadcast("test-session", {"type": "test", "content": "hello"})
|
|
results.append(("Broadcast no clients", True, "No-op with 0 clients"))
|
|
print(" ✅ Broadcast with 0 clients: no-op, no error")
|
|
except Exception as e:
|
|
results.append(("Broadcast no clients", False, str(e)))
|
|
print(f" ❌ Broadcast failed: {e}")
|
|
|
|
# ── Test 3: Mock bot + bridge attachment ────────────────────────
|
|
print("\n═══ Test 3: Bridge Attachment to Mock Bot ═══")
|
|
try:
|
|
# Create a mock bot with on_message support
|
|
class MockSession:
|
|
async def chat(self, agent, user_id, text):
|
|
return f"Echo: {text}"
|
|
|
|
class MockBot:
|
|
def __init__(self):
|
|
self._session = MockSession()
|
|
self._message_handlers = []
|
|
self.is_running = True
|
|
|
|
def on_message(self, fn):
|
|
self._message_handlers.append(fn)
|
|
return fn
|
|
|
|
bot = MockBot()
|
|
channels = PraisonAIChannels()
|
|
|
|
# Attach the bridge
|
|
channels._attach_chat_bridge("test-ch-001", bot, "slack")
|
|
|
|
# Verify on_message handler was registered
|
|
assert len(bot._message_handlers) == 1, f"Expected 1 handler, got {len(bot._message_handlers)}"
|
|
results.append(("Bridge attachment", True, "on_message handler registered"))
|
|
print(" ✅ on_message handler registered")
|
|
|
|
# Verify _session.chat was wrapped
|
|
assert bot._session.chat != MockSession.chat, "chat() should be wrapped"
|
|
results.append(("Session chat wrapped", True, "chat() wrapped for response broadcast"))
|
|
print(" ✅ _session.chat() wrapped for response broadcast")
|
|
|
|
except Exception as e:
|
|
results.append(("Bridge attachment", False, str(e)))
|
|
print(f" ❌ Bridge attachment failed: {e}")
|
|
return results
|
|
|
|
# ── Test 4: on_message fires without blocking ──────────────────
|
|
print("\n═══ Test 4: Fire-and-Forget Performance ═══")
|
|
try:
|
|
class MockMessage:
|
|
content = "Hello from Slack!"
|
|
class sender:
|
|
display_name = "test_user"
|
|
username = "test_user"
|
|
user_id = "U123"
|
|
|
|
# Time the on_message handler call (should be near-instant since broadcast is create_task)
|
|
handler = bot._message_handlers[0]
|
|
|
|
t_start = time.perf_counter()
|
|
await handler(MockMessage())
|
|
t_on_message = time.perf_counter() - t_start
|
|
|
|
# Should be < 1ms since the broadcast is fire-and-forget
|
|
assert t_on_message < 0.01, f"on_message took {t_on_message*1000:.2f}ms (expected < 10ms)"
|
|
results.append(("on_message perf", True, f"{t_on_message*1000:.3f}ms"))
|
|
print(f" ✅ on_message handler: {t_on_message*1000:.3f}ms (< 10ms threshold)")
|
|
|
|
except Exception as e:
|
|
results.append(("on_message perf", False, str(e)))
|
|
print(f" ❌ on_message perf test failed: {e}")
|
|
|
|
# ── Test 5: Wrapped chat() returns original response untouched ──
|
|
print("\n═══ Test 5: Wrapped chat() Returns Original Response ═══")
|
|
try:
|
|
class MockAgent:
|
|
name = "test-agent"
|
|
|
|
t_start = time.perf_counter()
|
|
response = await bot._session.chat(MockAgent(), "user123", "What is 2+2?")
|
|
t_chat = time.perf_counter() - t_start
|
|
|
|
assert response == "Echo: What is 2+2?", f"Expected 'Echo: What is 2+2?', got '{response}'"
|
|
results.append(("chat() response intact", True, f"Response correct, {t_chat*1000:.3f}ms"))
|
|
print(f" ✅ Response untouched: '{response}'")
|
|
print(f" ✅ chat() wrapper overhead: {t_chat*1000:.3f}ms")
|
|
|
|
except Exception as e:
|
|
results.append(("chat() response intact", False, str(e)))
|
|
print(f" ❌ chat() test failed: {e}")
|
|
|
|
# ── Test 6: Performance comparison (wrapped vs direct) ─────────
|
|
print("\n═══ Test 6: Performance Comparison (100 iterations) ═══")
|
|
try:
|
|
# Benchmark direct session chat
|
|
direct_session = MockSession()
|
|
agent = MockAgent()
|
|
|
|
t_start = time.perf_counter()
|
|
for _ in range(100):
|
|
await direct_session.chat(agent, "user", "test")
|
|
t_direct = time.perf_counter() - t_start
|
|
|
|
# Benchmark wrapped session chat
|
|
t_start = time.perf_counter()
|
|
for _ in range(100):
|
|
await bot._session.chat(agent, "user", "test")
|
|
t_wrapped = time.perf_counter() - t_start
|
|
|
|
overhead_pct = ((t_wrapped - t_direct) / t_direct * 100) if t_direct > 0 else 0
|
|
# Allow tasks to complete
|
|
await asyncio.sleep(0.1)
|
|
|
|
results.append(("Performance comparison", True,
|
|
f"Direct: {t_direct*1000:.2f}ms, Wrapped: {t_wrapped*1000:.2f}ms, Overhead: {overhead_pct:.1f}%"))
|
|
print(f" Direct (100 calls): {t_direct*1000:.2f}ms")
|
|
print(f" Wrapped (100 calls): {t_wrapped*1000:.2f}ms")
|
|
print(f" Overhead: {overhead_pct:.1f}%")
|
|
if overhead_pct < 50:
|
|
print(f" ✅ Overhead acceptable (< 50%)")
|
|
else:
|
|
print(f" ⚠️ Overhead higher than expected but calls are fire-and-forget")
|
|
|
|
except Exception as e:
|
|
results.append(("Performance comparison", False, str(e)))
|
|
print(f" ❌ Performance comparison failed: {e}")
|
|
|
|
# ── Test 7: Bot without on_message (graceful skip) ─────────────
|
|
print("\n═══ Test 7: Bot Without on_message (Graceful Skip) ═══")
|
|
try:
|
|
class MinimalBot:
|
|
is_running = True
|
|
|
|
minimal_bot = MinimalBot()
|
|
channels._attach_chat_bridge("test-ch-002", minimal_bot, "discord")
|
|
# Should not crash — just skip the hook
|
|
results.append(("Graceful skip", True, "No crash when bot lacks on_message"))
|
|
print(" ✅ No crash when bot lacks on_message")
|
|
|
|
except Exception as e:
|
|
results.append(("Graceful skip", False, str(e)))
|
|
print(f" ❌ Failed: {e}")
|
|
|
|
# ── Summary ────────────────────────────────────────────────────
|
|
print("\n" + "═" * 60)
|
|
print("RESULTS SUMMARY")
|
|
print("═" * 60)
|
|
passed = sum(1 for _, ok, _ in results if ok)
|
|
total = len(results)
|
|
for name, ok, detail in results:
|
|
icon = "✅" if ok else "❌"
|
|
print(f" {icon} {name}: {detail}")
|
|
print(f"\n {passed}/{total} tests passed")
|
|
print("═" * 60)
|
|
|
|
return results
|
|
|
|
if __name__ == "__main__":
|
|
results = asyncio.run(run_tests())
|
|
failed = any(not ok for _, ok, _ in results)
|
|
sys.exit(1 if failed else 0)
|