mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
216 lines
7.6 KiB
Python
216 lines
7.6 KiB
Python
"""
|
|
Unit tests for F30 Chat LLM Assistant.
|
|
Tests config retrieval from OpenClaw backend.
|
|
"""
|
|
|
|
import asyncio
|
|
import time
|
|
import unittest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from connector.config import ConnectorConfig
|
|
from connector.contract import CommandRequest, CommandResponse
|
|
from connector.router import CommandRouter
|
|
|
|
|
|
class MockOpenClawClient:
|
|
"""Mock OpenClaw client for testing."""
|
|
|
|
def __init__(self, config_response=None, key_configured=True):
|
|
self._config_response = config_response or {
|
|
"provider": "openai",
|
|
"model": "gpt-4o-mini",
|
|
"api_key_configured": key_configured,
|
|
}
|
|
|
|
async def get_openclaw_config(self):
|
|
return {"ok": True, "data": self._config_response}
|
|
|
|
async def get_health(self):
|
|
return {"ok": True, "data": {"status": "healthy"}}
|
|
|
|
async def get_jobs(self):
|
|
return {"ok": True, "data": {"running": 1, "pending": 2}}
|
|
|
|
async def get_prompt_queue(self):
|
|
return {"ok": True, "data": {"exec_info": {"queue_remaining": 3}}}
|
|
|
|
|
|
def make_request(
|
|
sender_id: str, text: str, platform: str = "telegram"
|
|
) -> CommandRequest:
|
|
"""Helper to create CommandRequest with all required fields."""
|
|
return CommandRequest(
|
|
platform=platform,
|
|
channel_id="123",
|
|
sender_id=sender_id,
|
|
username="testuser",
|
|
message_id="msg-001",
|
|
text=text,
|
|
timestamp=time.time(),
|
|
)
|
|
|
|
|
|
class TestChatCommand(unittest.IsolatedAsyncioTestCase):
|
|
"""Test /chat command."""
|
|
|
|
def setUp(self):
|
|
self.config = ConnectorConfig()
|
|
self.config.admin_users = ["admin123"]
|
|
self.config.telegram_allowed_users = [123]
|
|
|
|
async def test_chat_not_configured(self):
|
|
"""Should return error if LLM not configured in OpenClaw settings."""
|
|
# Mock client returning no API key configured
|
|
client = MockOpenClawClient(key_configured=False)
|
|
client._config_response = {"provider": None, "api_key_configured": False}
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("456", "/chat hello")
|
|
resp = await router.handle(req)
|
|
self.assertIn("not configured", resp.text.lower())
|
|
|
|
@patch("connector.router.LLMClient")
|
|
async def test_chat_general(self, mock_llm_cls):
|
|
"""Should handle general chat."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.is_configured = AsyncMock(return_value=True)
|
|
mock_llm.chat = AsyncMock(return_value="Hello! I'm OpenClaw Assistant.")
|
|
mock_llm_cls.return_value = mock_llm
|
|
|
|
client = MockOpenClawClient()
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("456", "/chat hello there")
|
|
resp = await router.handle(req)
|
|
self.assertEqual(resp.text, "Hello! I'm OpenClaw Assistant.")
|
|
mock_llm.chat.assert_called_once()
|
|
|
|
@patch("connector.router.LLMClient")
|
|
async def test_chat_run_untrusted(self, mock_llm_cls):
|
|
"""Should suggest /run with --approval for untrusted users."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.is_configured = AsyncMock(return_value=True)
|
|
mock_llm.chat = AsyncMock(
|
|
return_value="```\n/run txt2img --input prompt='cat' --approval\n```"
|
|
)
|
|
mock_llm_cls.return_value = mock_llm
|
|
|
|
client = MockOpenClawClient()
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("999", "/chat run make a cat image") # Not in allowed users
|
|
resp = await router.handle(req)
|
|
self.assertIn("--approval", resp.text)
|
|
|
|
@patch("connector.router.LLMClient")
|
|
async def test_chat_run_trusted(self, mock_llm_cls):
|
|
"""Should suggest /run without --approval for trusted users."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.is_configured = AsyncMock(return_value=True)
|
|
mock_llm.chat = AsyncMock(
|
|
return_value="```\n/run txt2img --input prompt='cat'\n```"
|
|
)
|
|
mock_llm_cls.return_value = mock_llm
|
|
|
|
self.config.telegram_allowed_users = [123]
|
|
client = MockOpenClawClient()
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("123", "/chat run make a cat image")
|
|
resp = await router.handle(req)
|
|
mock_llm.chat.assert_called_once()
|
|
call_args = mock_llm.chat.call_args
|
|
self.assertIn("no --approval", call_args[0][1])
|
|
|
|
@patch("connector.router.LLMClient")
|
|
async def test_chat_status(self, mock_llm_cls):
|
|
"""Should summarize status."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.is_configured = AsyncMock(return_value=True)
|
|
mock_llm.chat = AsyncMock(
|
|
return_value="System healthy. 1 running, 2 pending jobs."
|
|
)
|
|
mock_llm_cls.return_value = mock_llm
|
|
|
|
client = MockOpenClawClient()
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("456", "/chat status")
|
|
resp = await router.handle(req)
|
|
self.assertIn("healthy", resp.text.lower())
|
|
|
|
async def test_chat_usage(self):
|
|
"""Should show usage when no args."""
|
|
with patch("connector.router.LLMClient") as mock_llm_cls:
|
|
mock_llm = MagicMock()
|
|
mock_llm.is_configured = AsyncMock(return_value=True)
|
|
mock_llm_cls.return_value = mock_llm
|
|
|
|
client = MockOpenClawClient()
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("456", "/chat")
|
|
resp = await router.handle(req)
|
|
self.assertIn("usage", resp.text.lower())
|
|
|
|
async def test_config_retrieval_failure(self):
|
|
"""Should handle OpenClaw config retrieval failure gracefully."""
|
|
client = MagicMock()
|
|
client.get_openclaw_config = AsyncMock(
|
|
return_value={"ok": False, "error": "unreachable"}
|
|
)
|
|
client.get_health = AsyncMock(return_value={"ok": False})
|
|
client.get_jobs = AsyncMock(return_value={"ok": False})
|
|
client.get_prompt_queue = AsyncMock(return_value={"ok": False})
|
|
|
|
router = CommandRouter(self.config, client)
|
|
|
|
req = make_request("456", "/chat hello")
|
|
resp = await router.handle(req)
|
|
# Should indicate LLM not configured
|
|
self.assertIn("not configured", resp.text.lower())
|
|
|
|
|
|
class TestLLMClient(unittest.IsolatedAsyncioTestCase):
|
|
"""Test LLMClient config fetching and error handling."""
|
|
|
|
async def test_llm_call_failure_fallback(self):
|
|
"""Should handle LLM call failure gracefully."""
|
|
from connector.llm_client import LLMClient
|
|
|
|
client = MagicMock()
|
|
client.get_openclaw_config = AsyncMock(
|
|
return_value={
|
|
"ok": True,
|
|
"data": {"config": {"provider": "openai", "model": "gpt-4o"}},
|
|
}
|
|
)
|
|
client.chat_llm = AsyncMock(
|
|
return_value={"ok": False, "error": "llm_request_failed"}
|
|
)
|
|
|
|
llm = LLMClient(client)
|
|
result = await llm.chat("system", "user message")
|
|
self.assertIn("LLM Error", result)
|
|
|
|
async def test_no_prompt_logging(self):
|
|
"""Verify user prompts are not logged (by design, not by level)."""
|
|
from connector.llm_client import LLMClient
|
|
|
|
client = MagicMock()
|
|
client.get_openclaw_config = AsyncMock(
|
|
return_value={"ok": True, "data": {"config": {"provider": "openai"}}}
|
|
)
|
|
client.chat_llm = AsyncMock(return_value={"ok": True, "text": "response"})
|
|
|
|
llm = LLMClient(client)
|
|
|
|
# Verify the LLM client docstring mentions no prompt logging
|
|
# This is a design verification, not a runtime check
|
|
self.assertIn("Never logs user prompt content", LLMClient.__doc__ or "")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|