test_memory.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import pytest
  2. from unittest.mock import AsyncMock, MagicMock, patch
  3. class FakeEmbeddings:
  4. """最小 Embeddings 桩(O-14 终裁 2026-11-08 09:1x)。
  5. MemoryManager.__init__(embeddings) 已将 embeddings 改为必传
  6. (向量版记忆,langchain_chroma.Chroma 替换已废弃 langchain.memory)。
  7. 本桩实现 langchain Embeddings 兼容接口,返回固定 4 维向量,
  8. 仅用于单测构造 MemoryManager,不触发真实嵌入。
  9. """
  10. def embed_documents(self, texts):
  11. return [[0.0, 0.1, 0.2, 0.3] for _ in texts]
  12. def embed_query(self, text):
  13. return [0.0, 0.1, 0.2, 0.3]
  14. @pytest.fixture
  15. def fake_chroma():
  16. """用 MagicMock 替换 langchain_chroma.Chroma(隔离 chromadb 原生依赖)。
  17. 单测验证的是 MemoryManager 的契约,而非向量库实现;因此不应被
  18. `chromadb_rust_bindings` 原生 DLL 缺失这类宿主机环境问题拖红(B-54)。
  19. """
  20. with patch("langchain_chroma.Chroma") as chroma_cls:
  21. chroma_cls.return_value = MagicMock()
  22. yield chroma_cls
  23. @pytest.mark.asyncio
  24. async def test_memory_manager_init(fake_chroma):
  25. """B-64 对齐:构造契约 —— embeddings 必传,集合名 user_memory。"""
  26. from app.memory.store import MemoryManager
  27. embeddings = FakeEmbeddings()
  28. mgr = MemoryManager(embeddings=embeddings)
  29. assert mgr is not None
  30. assert mgr.memory_vectorstore is not None
  31. assert mgr.embeddings is embeddings
  32. fake_chroma.assert_called_once()
  33. kwargs = fake_chroma.call_args.kwargs
  34. assert kwargs["collection_name"] == "user_memory"
  35. assert kwargs["embedding_function"] is embeddings
  36. @pytest.mark.asyncio
  37. async def test_save_conversation_writes_vectorstore(fake_chroma):
  38. """B-64 对齐:save_conversation 写入向量库并携带 user/conversation 元数据。"""
  39. from app.memory.store import MemoryManager
  40. mgr = MemoryManager(embeddings=FakeEmbeddings())
  41. mgr.memory_vectorstore.aadd_texts = AsyncMock()
  42. await mgr.save_conversation(
  43. user_id=1,
  44. conversation_id="conv-1",
  45. messages=[{"role": "user", "content": "你好"},
  46. {"role": "assistant", "content": "你好呀"}],
  47. )
  48. mgr.memory_vectorstore.aadd_texts.assert_awaited_once()
  49. kwargs = mgr.memory_vectorstore.aadd_texts.call_args.kwargs
  50. assert len(kwargs["texts"]) == 1
  51. assert "[user] 你好" in kwargs["texts"][0]
  52. assert "[assistant] 你好呀" in kwargs["texts"][0]
  53. meta = kwargs["metadatas"][0]
  54. assert meta["user_id"] == 1
  55. assert meta["conversation_id"] == "conv-1"
  56. assert "timestamp" in meta
  57. @pytest.mark.asyncio
  58. async def test_recall_returns_contents(fake_chroma):
  59. """B-64 对齐:recall 按 user_id 过滤做相似度召回并返回纯文本列表。"""
  60. from app.memory.store import MemoryManager
  61. mgr = MemoryManager(embeddings=FakeEmbeddings())
  62. doc1, doc2 = MagicMock(), MagicMock()
  63. doc1.page_content = "历史记忆一"
  64. doc2.page_content = "历史记忆二"
  65. mgr.memory_vectorstore.similarity_search = MagicMock(return_value=[doc1, doc2])
  66. out = await mgr.recall(user_id=7, query="钙", k=2)
  67. assert out == ["历史记忆一", "历史记忆二"]
  68. kwargs = mgr.memory_vectorstore.similarity_search.call_args.kwargs
  69. assert kwargs["k"] == 2
  70. assert kwargs["filter"] == {"user_id": 7}
  71. def test_legacy_memory_api_removed():
  72. """B-64 收口:langchain.memory 版旧接口已随迁移移除,防止回潮。"""
  73. from app.memory.store import MemoryManager
  74. assert not hasattr(MemoryManager, "get_working_memory")
  75. assert not hasattr(MemoryManager, "get_longterm_memory")