555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
import inspect
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from mem0 import Memory, MemoryClient
|
|
from mem0.proxy.main import Chat, Completions, Mem0
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_memory_client():
|
|
mock_client = Mock(spec=MemoryClient)
|
|
mock_client.user_email = None
|
|
return mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_openai_embedding_client():
|
|
with patch("mem0.embeddings.openai.OpenAI") as mock_openai:
|
|
mock_client = Mock()
|
|
mock_openai.return_value = mock_client
|
|
yield mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_openai_llm_client():
|
|
with patch("mem0.llms.openai.OpenAI") as mock_openai:
|
|
mock_client = Mock()
|
|
mock_openai.return_value = mock_client
|
|
yield mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_litellm():
|
|
with patch("mem0.proxy.main.litellm") as mock:
|
|
yield mock
|
|
|
|
|
|
def test_mem0_initialization_with_api_key(mock_openai_embedding_client, mock_openai_llm_client):
|
|
mem0 = Mem0()
|
|
assert isinstance(mem0.mem0_client, Memory)
|
|
assert isinstance(mem0.chat, Chat)
|
|
|
|
|
|
def test_mem0_initialization_with_config():
|
|
config = {"some_config": "value"}
|
|
with patch("mem0.Memory.from_config") as mock_from_config:
|
|
mem0 = Mem0(config=config)
|
|
mock_from_config.assert_called_once_with(config)
|
|
assert isinstance(mem0.chat, Chat)
|
|
|
|
|
|
def test_mem0_initialization_without_params(mock_openai_embedding_client, mock_openai_llm_client):
|
|
mem0 = Mem0()
|
|
assert isinstance(mem0.mem0_client, Memory)
|
|
assert isinstance(mem0.chat, Chat)
|
|
|
|
|
|
def test_chat_initialization(mock_memory_client):
|
|
chat = Chat(mock_memory_client)
|
|
assert isinstance(chat.completions, Completions)
|
|
|
|
|
|
def test_completions_create(mock_memory_client, mock_litellm):
|
|
completions = Completions(mock_memory_client)
|
|
|
|
messages = [{"role": "user", "content": "Hello, how are you?"}]
|
|
mock_memory_client.search.return_value = [{"memory": "Some relevant memory"}]
|
|
mock_litellm.completion.return_value = {"choices": [{"message": {"content": "I'm doing well, thank you!"}}]}
|
|
mock_litellm.supports_function_calling.return_value = True
|
|
|
|
response = completions.create(model="gpt-4.1-nano-2025-04-14", messages=messages, user_id="test_user", temperature=0.7)
|
|
|
|
mock_memory_client.add.assert_called_once()
|
|
mock_memory_client.search.assert_called_once()
|
|
|
|
mock_litellm.completion.assert_called_once()
|
|
call_args = mock_litellm.completion.call_args[1]
|
|
assert call_args["model"] == "gpt-4.1-nano-2025-04-14"
|
|
assert len(call_args["messages"]) == 2
|
|
assert call_args["temperature"] == 0.7
|
|
|
|
assert response == {"choices": [{"message": {"content": "I'm doing well, thank you!"}}]}
|
|
|
|
|
|
def test_completions_create_with_system_message(mock_memory_client, mock_litellm):
|
|
completions = Completions(mock_memory_client)
|
|
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant."},
|
|
{"role": "user", "content": "Hello, how are you?"},
|
|
]
|
|
mock_memory_client.search.return_value = [{"memory": "Some relevant memory"}]
|
|
mock_litellm.completion.return_value = {"choices": [{"message": {"content": "I'm doing well, thank you!"}}]}
|
|
mock_litellm.supports_function_calling.return_value = True
|
|
|
|
completions.create(model="gpt-4.1-nano-2025-04-14", messages=messages, user_id="test_user")
|
|
|
|
call_args = mock_litellm.completion.call_args[1]
|
|
assert call_args["messages"][0]["role"] == "system"
|
|
assert call_args["messages"][0]["content"] == "You are a helpful assistant."
|
|
|
|
|
|
def test_completions_create_messages_default_does_not_leak_between_calls(mock_memory_client, mock_litellm):
|
|
"""Regression test for the B006 mutable-default bug in Completions.create.
|
|
|
|
Before the fix, `messages: List = []` made every call that didn't pass
|
|
`messages` share the same module-level list. A previous call could mutate
|
|
that list (e.g. via `_prepare_messages`) and subsequent calls would observe
|
|
the leaked state instead of an empty list.
|
|
|
|
After the fix, `messages` defaults to `None` and is normalized to a fresh
|
|
`[]` inside the function on each call, isolating call N from call N-1.
|
|
"""
|
|
completions = Completions(mock_memory_client)
|
|
mock_litellm.supports_function_calling.return_value = True
|
|
mock_litellm.completion.return_value = {"choices": [{"message": {"content": "ok"}}]}
|
|
mock_memory_client.search.return_value = []
|
|
|
|
# Each call passes a fresh list — confirms the public happy path stays green.
|
|
completions.create(
|
|
model="gpt-4.1-nano-2025-04-14",
|
|
messages=[{"role": "user", "content": "first"}],
|
|
user_id="user_a",
|
|
)
|
|
completions.create(
|
|
model="gpt-4.1-nano-2025-04-14",
|
|
messages=[{"role": "user", "content": "second"}],
|
|
user_id="user_b",
|
|
)
|
|
|
|
# The Completions.create signature must not bind a mutable container as
|
|
# the default for `messages`. This is what B006 lints against and what the
|
|
# historical default `messages: List = []` violated.
|
|
sig = inspect.signature(Completions.create)
|
|
messages_default = sig.parameters["messages"].default
|
|
assert messages_default is None, (
|
|
f"Completions.create(messages=...) must default to None to avoid the "
|
|
f"B006 shared-default-list bug; got {messages_default!r}."
|
|
)
|