Files
AstrBot/tests_v4/test_memory_client.py
whatevertogo 94514fb19b feat: Enhance handler and capability dispatchers with improved error handling
- Updated HandlerDispatcher to raise TypeError for uninjectable required parameters, logging errors appropriately.
- Refactored CapabilityDispatcher to raise TypeError for missing required parameters during capability execution.
- Renamed _load_plugin_config to load_plugin_config for clarity and consistency.
- Introduced _sync_plugin_registry method in SupervisorRuntime to manage plugin capabilities more effectively.
- Enhanced capability registration logic to handle naming conflicts with better logging and automatic renaming.
- Added tests for handler and capability dispatchers to ensure proper error handling and functionality.
- Implemented new HTTP and metadata capabilities with corresponding tests for registration and retrieval.
- Improved MemoryClient methods with additional tests for save_with_ttl, get_many, delete_many, and stats.
- Added tests for the testing module to ensure proper import and functionality of PluginHarness.
2026-03-14 22:07:10 +08:00

382 lines
13 KiB
Python

"""
Tests for clients/memory.py - MemoryClient implementation.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from astrbot_sdk.clients.memory import MemoryClient
from astrbot_sdk.clients._proxy import CapabilityProxy
class TestMemoryClientInit:
"""Tests for MemoryClient initialization."""
def test_init_with_proxy(self):
"""MemoryClient should store proxy reference."""
proxy = MagicMock(spec=CapabilityProxy)
client = MemoryClient(proxy)
assert client._proxy is proxy
class TestMemoryClientSearch:
"""Tests for MemoryClient.search() method."""
@pytest.mark.asyncio
async def test_search_returns_items(self):
"""search() should return list of items."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(
return_value={
"items": [
{"id": "1", "content": "first"},
{"id": "2", "content": "second"},
]
}
)
client = MemoryClient(proxy)
result = await client.search("test query")
proxy.call.assert_called_once_with(
"memory.search",
{"query": "test query"},
)
assert len(result) == 2
assert result[0]["content"] == "first"
@pytest.mark.asyncio
async def test_search_returns_empty_list_for_no_results(self):
"""search() should return empty list when no items found."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
result = await client.search("nonexistent")
assert result == []
@pytest.mark.asyncio
async def test_search_with_empty_query(self):
"""search() should work with empty query."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"items": []})
client = MemoryClient(proxy)
result = await client.search("")
proxy.call.assert_called_once_with("memory.search", {"query": ""})
assert result == []
@pytest.mark.asyncio
async def test_search_returns_empty_list_for_malformed_items(self):
"""search() should ignore malformed non-list item payloads."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"items": "bad"})
client = MemoryClient(proxy)
result = await client.search("test")
assert result == []
class TestMemoryClientSave:
"""Tests for MemoryClient.save() method."""
@pytest.mark.asyncio
async def test_save_with_key_and_value(self):
"""save() should call proxy with key and value."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save("my_key", {"data": "value"})
proxy.call.assert_called_once_with(
"memory.save",
{"key": "my_key", "value": {"data": "value"}},
)
@pytest.mark.asyncio
async def test_save_with_extra_kwargs(self):
"""save() should merge extra kwargs into value."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save("key", {"base": 1}, extra="added", another=2)
call_args = proxy.call.call_args[0][1]
assert call_args["value"] == {"base": 1, "extra": "added", "another": 2}
@pytest.mark.asyncio
async def test_save_with_only_kwargs(self):
"""save() should work with only kwargs (no value)."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save("key", name="test", count=5)
call_args = proxy.call.call_args[0][1]
assert call_args["value"] == {"name": "test", "count": 5}
@pytest.mark.asyncio
async def test_save_with_none_value(self):
"""save() should handle None value with kwargs."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save("key", None, field="value")
call_args = proxy.call.call_args[0][1]
assert call_args["value"] == {"field": "value"}
@pytest.mark.asyncio
async def test_save_with_empty_value_and_no_kwargs(self):
"""save() should work with empty dict."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save("key", {})
proxy.call.assert_called_once_with(
"memory.save",
{"key": "key", "value": {}},
)
@pytest.mark.asyncio
async def test_save_raises_type_error_for_non_dict_value(self):
"""save() should raise TypeError for non-dict value."""
proxy = AsyncMock(spec=CapabilityProxy)
client = MemoryClient(proxy)
with pytest.raises(TypeError, match="memory.save 的 value 必须是 dict"):
await client.save("key", "not a dict")
@pytest.mark.asyncio
async def test_save_raises_type_error_for_list_value(self):
"""save() should raise TypeError for list value."""
proxy = AsyncMock(spec=CapabilityProxy)
client = MemoryClient(proxy)
with pytest.raises(TypeError, match="memory.save 的 value 必须是 dict"):
await client.save("key", [1, 2, 3])
class TestMemoryClientGet:
"""Tests for MemoryClient.get() method."""
@pytest.mark.asyncio
async def test_get_returns_dict_value(self):
"""get() should return dict value from proxy response."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"value": {"theme": "dark"}})
client = MemoryClient(proxy)
result = await client.get("user_pref")
proxy.call.assert_called_once_with("memory.get", {"key": "user_pref"})
assert result == {"theme": "dark"}
@pytest.mark.asyncio
async def test_get_returns_none_for_missing_value(self):
"""get() should return None when memory is absent."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"value": None})
client = MemoryClient(proxy)
result = await client.get("missing")
assert result is None
@pytest.mark.asyncio
async def test_get_returns_none_for_non_dict_value(self):
"""get() should ignore malformed non-dict payloads."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"value": "bad"})
client = MemoryClient(proxy)
result = await client.get("bad")
assert result is None
class TestMemoryClientDelete:
"""Tests for MemoryClient.delete() method."""
@pytest.mark.asyncio
async def test_delete_calls_proxy_with_key(self):
"""delete() should call proxy with correct key."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.delete("to_delete")
proxy.call.assert_called_once_with("memory.delete", {"key": "to_delete"})
@pytest.mark.asyncio
async def test_delete_with_empty_key(self):
"""delete() should work with empty string key."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.delete("")
proxy.call.assert_called_once_with("memory.delete", {"key": ""})
class TestMemoryClientSaveWithTTL:
"""Tests for MemoryClient.save_with_ttl() method."""
@pytest.mark.asyncio
async def test_save_with_ttl_calls_proxy(self):
"""save_with_ttl() should call proxy with key, value, and ttl_seconds."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
await client.save_with_ttl("temp_key", {"data": "value"}, ttl_seconds=3600)
proxy.call.assert_called_once_with(
"memory.save_with_ttl",
{"key": "temp_key", "value": {"data": "value"}, "ttl_seconds": 3600},
)
@pytest.mark.asyncio
async def test_save_with_ttl_raises_type_error_for_non_dict(self):
"""save_with_ttl() should raise TypeError for non-dict value."""
proxy = AsyncMock(spec=CapabilityProxy)
client = MemoryClient(proxy)
with pytest.raises(
TypeError, match="memory.save_with_ttl 的 value 必须是 dict"
):
await client.save_with_ttl("key", "not a dict", ttl_seconds=60)
@pytest.mark.asyncio
async def test_save_with_ttl_raises_value_error_for_invalid_ttl(self):
"""save_with_ttl() should raise ValueError for ttl_seconds < 1."""
proxy = AsyncMock(spec=CapabilityProxy)
client = MemoryClient(proxy)
with pytest.raises(ValueError, match="ttl_seconds 必须大于 0"):
await client.save_with_ttl("key", {"data": 1}, ttl_seconds=0)
with pytest.raises(ValueError, match="ttl_seconds 必须大于 0"):
await client.save_with_ttl("key", {"data": 1}, ttl_seconds=-1)
class TestMemoryClientGetMany:
"""Tests for MemoryClient.get_many() method."""
@pytest.mark.asyncio
async def test_get_many_returns_items(self):
"""get_many() should return list of items with key and value."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(
return_value={
"items": [
{"key": "k1", "value": {"a": 1}},
{"key": "k2", "value": {"b": 2}},
]
}
)
client = MemoryClient(proxy)
result = await client.get_many(["k1", "k2"])
proxy.call.assert_called_once_with("memory.get_many", {"keys": ["k1", "k2"]})
assert len(result) == 2
assert result[0]["key"] == "k1"
assert result[0]["value"] == {"a": 1}
@pytest.mark.asyncio
async def test_get_many_returns_empty_list_for_malformed_response(self):
"""get_many() should return empty list for malformed response."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"items": "not a list"})
client = MemoryClient(proxy)
result = await client.get_many(["k1", "k2"])
assert result == []
@pytest.mark.asyncio
async def test_get_many_returns_empty_list_for_missing_items(self):
"""get_many() should return empty list when items key missing."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
result = await client.get_many(["k1"])
assert result == []
class TestMemoryClientDeleteMany:
"""Tests for MemoryClient.delete_many() method."""
@pytest.mark.asyncio
async def test_delete_many_returns_count(self):
"""delete_many() should return number of deleted items."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"deleted_count": 3})
client = MemoryClient(proxy)
result = await client.delete_many(["k1", "k2", "k3"])
proxy.call.assert_called_once_with(
"memory.delete_many", {"keys": ["k1", "k2", "k3"]}
)
assert result == 3
@pytest.mark.asyncio
async def test_delete_many_returns_zero_for_missing_count(self):
"""delete_many() should return 0 when deleted_count missing."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
result = await client.delete_many(["k1"])
assert result == 0
class TestMemoryClientStats:
"""Tests for MemoryClient.stats() method."""
@pytest.mark.asyncio
async def test_stats_returns_total_items(self):
"""stats() should return total_items count."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={"total_items": 42, "total_bytes": 1024})
client = MemoryClient(proxy)
result = await client.stats()
proxy.call.assert_called_once_with("memory.stats", {})
assert result["total_items"] == 42
assert result["total_bytes"] == 1024
@pytest.mark.asyncio
async def test_stats_defaults_to_zero(self):
"""stats() should default total_items to 0 if missing."""
proxy = AsyncMock(spec=CapabilityProxy)
proxy.call = AsyncMock(return_value={})
client = MemoryClient(proxy)
result = await client.stats()
assert result["total_items"] == 0
assert result["total_bytes"] is None