Files
mem0/tests/test_client_profiles.py
T
Pratik 34894e2cea fix(profiles): follow the jobs API
Four POST routes became one collection with the operation in the body, so the
three route pages collapse into one. samples became sample. Creates send an
Idempotency-Key and return a status_url to poll, which the client follows
rather than building the path.

Full rebuild is closed: regenerate_profiles answers 501 not_yet_available and
the docs say so instead of teaching a daily cadence that cannot run.
2026-09-16 17:02:10 -07:00

234 lines
9.0 KiB
Python

"""Tests for the MemoryClient profile methods.
These assert request construction — path, verb, body — rather than echoing a
mocked response back. The profile payload itself is the customer's own JSON
Schema shape, so the tests also pin that the SDK passes it through untouched.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@pytest.fixture
def mock_memory_client():
"""A MemoryClient whose transport is mocked."""
with patch("mem0.client.main.httpx.Client") as mock_httpx:
mock_http_client = MagicMock()
mock_http_client.get.return_value = MagicMock(
json=lambda: {"org_id": "org1", "project_id": "proj1", "user_email": "test@test.com"},
raise_for_status=lambda: None,
)
mock_httpx.return_value = mock_http_client
with patch("mem0.client.main.capture_client_event"):
from mem0.client.main import MemoryClient
client = MemoryClient(api_key="test-api-key")
# The constructor pings through this same mock; drop that call.
mock_http_client.get.reset_mock()
yield client
def _assert_job_call(post_mock, expected_json):
"""One jobs collection, operation in the body, idempotency key per attempt."""
post_mock.assert_called_once()
args, kwargs = post_mock.call_args
assert args[0] == "/v2/profiles/jobs/"
assert kwargs["json"] == expected_json
assert kwargs["headers"]["Idempotency-Key"]
def _mock_response(payload):
response = MagicMock()
response.json.return_value = payload
response.raise_for_status.return_value = None
return response
class TestGetProfile:
def test_reads_the_v2_entity_route(self, mock_memory_client):
mock_memory_client.client.get.return_value = _mock_response(
{"profile": {}, "status": "pending", "entity_type": "user", "entity_id": "alice"}
)
mock_memory_client.get_profile("alice")
mock_memory_client.client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
def test_supports_agents(self, mock_memory_client):
mock_memory_client.client.get.return_value = _mock_response({"profile": {}, "status": "pending"})
mock_memory_client.get_profile("support-bot", entity_type="agent")
mock_memory_client.client.get.assert_called_once_with("/v2/entities/agent/support-bot/profile/")
def test_encodes_path_segments(self, mock_memory_client):
"""An id with a slash must not open a new path segment."""
mock_memory_client.client.get.return_value = _mock_response({"profile": {}, "status": "pending"})
mock_memory_client.get_profile("tenant/alice")
mock_memory_client.client.get.assert_called_once_with("/v2/entities/user/tenant%2Falice/profile/")
def test_returns_the_envelope_verbatim(self, mock_memory_client):
"""The customer's schema keys reach the caller exactly as stored."""
payload = {
"profile": {"favorite_topics": ["hiking"], "work_style": {"preferred_hours": "mornings"}},
"status": "succeeded",
"entity_type": "user",
"entity_id": "alice",
"updated_at": "2026-02-08T00:00:00Z",
"generation_count": 3,
}
mock_memory_client.client.get.return_value = _mock_response(payload)
assert mock_memory_client.get_profile("alice") == payload
class TestGenerateProfile:
def test_posts_entity_type_and_id(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"profile_id": "p_1", "status": "PENDING"})
mock_memory_client.generate_profile("alice")
_assert_job_call(
mock_memory_client.client.post,
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
)
def test_agent_entity_type(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"profile_id": "p_2", "status": "PENDING"})
mock_memory_client.generate_profile("support-bot", entity_type="agent")
_assert_job_call(
mock_memory_client.client.post,
{"operation": "trigger", "entity_type": "agent", "entity_id": "support-bot"},
)
class TestProfileSettings:
def test_get_reads_v2(self, mock_memory_client):
mock_memory_client.client.get.return_value = _mock_response(
{"enabled": True, "schema": None, "custom_instructions": None}
)
mock_memory_client.get_profile_settings()
mock_memory_client.client.get.assert_called_once_with("/v2/profiles/settings/")
def test_update_sends_only_supplied_fields(self, mock_memory_client):
"""A partial update must not blank the fields it never mentions."""
mock_memory_client.client.post.return_value = _mock_response({"enabled": False})
mock_memory_client.update_profile_settings(enabled=False)
mock_memory_client.client.post.assert_called_once_with(
"/v2/profiles/settings/",
json={"enabled": False},
)
def test_update_passes_schema_verbatim(self, mock_memory_client):
schema = {
"type": "object",
"properties": {
"favorite_topics": {
"type": "array",
"description": "Topics the user returns to",
"items": {"type": "string"},
}
},
}
mock_memory_client.client.post.return_value = _mock_response({"enabled": True, "schema": schema})
mock_memory_client.update_profile_settings(enabled=True, schema=schema, custom_instructions="Keep it durable")
mock_memory_client.client.post.assert_called_once_with(
"/v2/profiles/settings/",
json={"enabled": True, "schema": schema, "custom_instructions": "Keep it durable"},
)
class TestSampleAndRegenerate:
def test_sample_without_limit(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"sampled": 5, "results": []})
mock_memory_client.sample_profiles()
_assert_job_call(mock_memory_client.client.post, {"operation": "sample"})
def test_sample_with_limit(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"sampled": 3, "results": []})
mock_memory_client.sample_profiles(limit=3)
_assert_job_call(mock_memory_client.client.post, {"operation": "sample", "limit": 3})
def test_regenerate(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response(
{"status": "accepted", "project_id": "proj_abc", "existing_profile_count": 12}
)
mock_memory_client.regenerate_profiles()
_assert_job_call(mock_memory_client.client.post, {"operation": "regenerate"})
class TestAsyncClientParity:
"""The async client must speak the same wire protocol as the sync one."""
@pytest.fixture
def async_client(self):
# AsyncMemoryClient validates the key synchronously, through requests.
validation = MagicMock()
validation.json.return_value = {
"org_id": "org1",
"project_id": "proj1",
"user_email": "test@test.com",
}
validation.raise_for_status.return_value = None
with patch("mem0.client.main.httpx.AsyncClient") as mock_httpx:
mock_httpx.return_value = MagicMock()
with patch("mem0.client.main.requests.get", return_value=validation):
with patch("mem0.client.main.capture_client_event"):
from mem0.client.main import AsyncMemoryClient
yield AsyncMemoryClient(api_key="test-api-key")
def test_get_profile(self, async_client):
async_client.async_client.get = AsyncMock(return_value=_mock_response({"profile": {}, "status": "pending"}))
asyncio.run(async_client.get_profile("alice"))
async_client.async_client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
def test_generate_profile(self, async_client):
async_client.async_client.post = AsyncMock(return_value=_mock_response({"profile_id": "p_1"}))
asyncio.run(async_client.generate_profile("alice", entity_type="agent"))
_assert_job_call(
async_client.async_client.post,
{"operation": "trigger", "entity_type": "agent", "entity_id": "alice"},
)
def test_update_settings_partial(self, async_client):
async_client.async_client.post = AsyncMock(return_value=_mock_response({"enabled": True}))
asyncio.run(async_client.update_profile_settings(enabled=True))
async_client.async_client.post.assert_called_once_with(
"/v2/profiles/settings/",
json={"enabled": True},
)
def test_regenerate(self, async_client):
async_client.async_client.post = AsyncMock(return_value=_mock_response({"status": "accepted"}))
asyncio.run(async_client.regenerate_profiles())
_assert_job_call(async_client.async_client.post, {"operation": "regenerate"})