- 将 A_Memorix 从旧 submodule / 插件形态迁入主线源码,主体落到 src/A_memorix - 调整主程序接入方式,使 A_Memorix 作为源码内长期记忆子系统运行 - 回收父项目插件体系中针对 A_Memorix 的特判,减少对 plugin 通用层的侵入 - 将长期记忆配置、运行时、自检、导入、调优等能力收口到 memory 路由与主线服务层 - 重做长期记忆控制台与图谱页面,按 MaiBot 现有 dashboard 风格接入 - 补充实体关系图与证据视图双视图能力,支持查看节点、关系、段落及其证据链路 - 新增长期记忆配置编辑器与 memory-api,支持主线内配置管理 - 补齐删除管理能力:删除预览、混合删除、来源批量删除、删除操作恢复 - 优化删除预览与删除操作详情的前端展示,支持分页、检索,并以实体名/关系内容/段落摘要替代单纯 hash 展示 - 修复图谱与控制台相关前端问题,包括证据视图切换、查询触发时机、删除弹层空值保护等 - 新增或更新 A_Memorix 相关测试、WebUI 路由测试、前端 vitest 测试与辅助验证脚本 - 移除旧 plugins/A_memorix、.gitmodules 及相关历史维护文档
282 lines
8.1 KiB
Python
282 lines
8.1 KiB
Python
import pytest
|
|
|
|
from src.services.memory_service import MemorySearchResult, MemoryService
|
|
|
|
|
|
def test_coerce_write_result_treats_skipped_payload_as_success():
|
|
result = MemoryService._coerce_write_result({"skipped_ids": ["p1"], "detail": "chat_filtered"})
|
|
|
|
assert result.success is True
|
|
assert result.stored_ids == []
|
|
assert result.skipped_ids == ["p1"]
|
|
assert result.detail == "chat_filtered"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_admin_invokes_plugin(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args, kwargs))
|
|
return {"success": True, "nodes": [], "edges": []}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.graph_admin(action="get_graph", limit=12)
|
|
|
|
assert result["success"] is True
|
|
assert calls == [("memory_graph_admin", {"action": "get_graph", "limit": 12}, {"timeout_ms": 30000})]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_recycle_bin_uses_maintain_memory_tool(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args))
|
|
return {"success": True, "items": [{"hash": "abc"}], "count": 1}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.get_recycle_bin(limit=5)
|
|
|
|
assert result == {"success": True, "items": [{"hash": "abc"}], "count": 1}
|
|
assert calls == [("maintain_memory", {"action": "recycle_bin", "limit": 5})]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_respects_filter_by_default(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args))
|
|
return {"summary": "ok", "hits": [], "filtered": True}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.search(
|
|
"mai",
|
|
chat_id="stream-1",
|
|
person_id="person-1",
|
|
user_id="user-1",
|
|
group_id="",
|
|
)
|
|
|
|
assert isinstance(result, MemorySearchResult)
|
|
assert result.filtered is True
|
|
assert calls == [
|
|
(
|
|
"search_memory",
|
|
{
|
|
"query": "mai",
|
|
"limit": 5,
|
|
"mode": "search",
|
|
"chat_id": "stream-1",
|
|
"person_id": "person-1",
|
|
"time_start": None,
|
|
"time_end": None,
|
|
"respect_filter": True,
|
|
"user_id": "user-1",
|
|
"group_id": "",
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ingest_summary_can_bypass_filter(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args))
|
|
return {"success": True, "stored_ids": ["p1"], "detail": ""}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.ingest_summary(
|
|
external_id="chat_history:1",
|
|
chat_id="stream-1",
|
|
text="summary",
|
|
respect_filter=False,
|
|
user_id="user-1",
|
|
)
|
|
|
|
assert result.success is True
|
|
assert calls == [
|
|
(
|
|
"ingest_summary",
|
|
{
|
|
"external_id": "chat_history:1",
|
|
"chat_id": "stream-1",
|
|
"text": "summary",
|
|
"participants": [],
|
|
"time_start": None,
|
|
"time_end": None,
|
|
"tags": [],
|
|
"metadata": {},
|
|
"respect_filter": False,
|
|
"user_id": "user-1",
|
|
"group_id": "",
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_v5_admin_invokes_plugin(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args, kwargs))
|
|
return {"success": True, "count": 1}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.v5_admin(action="status", target="mai", limit=5)
|
|
|
|
assert result["success"] is True
|
|
assert calls == [("memory_v5_admin", {"action": "status", "target": "mai", "limit": 5}, {"timeout_ms": 30000})]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_admin_uses_long_timeout(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args, kwargs))
|
|
return {"success": True, "operation_id": "del-1"}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.delete_admin(action="execute", mode="relation", selector={"query": "mai"})
|
|
|
|
assert result["success"] is True
|
|
assert calls == [
|
|
(
|
|
"memory_delete_admin",
|
|
{"action": "execute", "mode": "relation", "selector": {"query": "mai"}},
|
|
{"timeout_ms": 120000},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_returns_empty_when_query_and_time_missing_async():
|
|
service = MemoryService()
|
|
|
|
result = await service.search("", time_start=None, time_end=None)
|
|
|
|
assert isinstance(result, MemorySearchResult)
|
|
assert result.summary == ""
|
|
assert result.hits == []
|
|
assert result.filtered is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_accepts_string_time_bounds(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args))
|
|
return {"summary": "ok", "hits": [], "filtered": False}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.search(
|
|
"广播站",
|
|
mode="time",
|
|
time_start="2026/03/18",
|
|
time_end="2026/03/18 09:30",
|
|
)
|
|
|
|
assert isinstance(result, MemorySearchResult)
|
|
assert calls == [
|
|
(
|
|
"search_memory",
|
|
{
|
|
"query": "广播站",
|
|
"limit": 5,
|
|
"mode": "time",
|
|
"chat_id": "",
|
|
"person_id": "",
|
|
"time_start": "2026/03/18",
|
|
"time_end": "2026/03/18 09:30",
|
|
"respect_filter": True,
|
|
"user_id": "",
|
|
"group_id": "",
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def test_coerce_search_result_preserves_aggregate_source_branches():
|
|
result = MemoryService._coerce_search_result(
|
|
{
|
|
"hits": [
|
|
{
|
|
"content": "广播站值夜班",
|
|
"type": "paragraph",
|
|
"metadata": {"event_time_start": 1.0},
|
|
"source_branches": ["search", "time"],
|
|
"rank": 1,
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
assert result.hits[0].metadata["source_branches"] == ["search", "time"]
|
|
assert result.hits[0].metadata["rank"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_admin_uses_long_timeout(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args, kwargs))
|
|
return {"success": True, "task_id": "import-1"}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.import_admin(action="create_lpmm_openie", alias="lpmm")
|
|
|
|
assert result["success"] is True
|
|
assert calls == [
|
|
(
|
|
"memory_import_admin",
|
|
{"action": "create_lpmm_openie", "alias": "lpmm"},
|
|
{"timeout_ms": 120000},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tuning_admin_uses_long_timeout(monkeypatch):
|
|
service = MemoryService()
|
|
calls = []
|
|
|
|
async def fake_invoke(component_name, args=None, **kwargs):
|
|
calls.append((component_name, args, kwargs))
|
|
return {"success": True, "task_id": "tuning-1"}
|
|
|
|
monkeypatch.setattr(service, "_invoke", fake_invoke)
|
|
|
|
result = await service.tuning_admin(action="create_task", payload={"query": "mai"})
|
|
|
|
assert result["success"] is True
|
|
assert calls == [
|
|
(
|
|
"memory_tuning_admin",
|
|
{"action": "create_task", "payload": {"query": "mai"}},
|
|
{"timeout_ms": 120000},
|
|
)
|
|
]
|