diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index dd638bbc6..ff259f2fe 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -404,10 +404,10 @@ export default class MemoryClient { ...(filters && { filters }), }; - let url = `${this.host}/v3/memories/`; - if (page && pageSize) { - url += `?page=${page}&page_size=${pageSize}`; - } + const queryParams: string[] = []; + if (page !== undefined) queryParams.push(`page=${page}`); + if (pageSize !== undefined) queryParams.push(`page_size=${pageSize}`); + const url = `${this.host}/v3/memories/${queryParams.length ? `?${queryParams.join("&")}` : ""}`; const response = await this._fetchWithErrorHandling(url, { method: "POST", diff --git a/mem0-ts/src/client/tests/memoryClient.search.test.ts b/mem0-ts/src/client/tests/memoryClient.search.test.ts index 021187867..be85df9f5 100644 --- a/mem0-ts/src/client/tests/memoryClient.search.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.search.test.ts @@ -373,3 +373,56 @@ describe("MemoryClient - getAll() entity param rejection", () => { expect(getFetchBody(call!).show_expired).toBe(true); }); }); + +describe("MemoryClient - getAll() page/pageSize query params", () => { + test("sends no query params when neither page nor pageSize is provided", async () => { + const extra = new Map(); + extra.set("/v3/memories/", { status: 200, body: { results: [] } }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.getAll({ filters: { user_id: "u1" } }); + + const call = findFetchCall(mock, "/v3/memories/", "POST"); + expect(call![0]).not.toContain("?"); + }); + + test("sends page alone as a query param", async () => { + const extra = new Map(); + extra.set("/v3/memories/", { status: 200, body: { results: [] } }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.getAll({ filters: { user_id: "u1" }, page: 2 }); + + const call = findFetchCall(mock, "/v3/memories/", "POST"); + expect(call![0]).toContain("?page=2"); + expect(call![0]).not.toContain("page_size"); + }); + + test("sends pageSize alone as a query param", async () => { + const extra = new Map(); + extra.set("/v3/memories/", { status: 200, body: { results: [] } }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.getAll({ filters: { user_id: "u1" }, pageSize: 50 }); + + const call = findFetchCall(mock, "/v3/memories/", "POST"); + expect(call![0]).toContain("?page_size=50"); + expect(call![0]).not.toContain("?page=50"); + }); + + test("sends both page and pageSize when provided, including falsy 0 values", async () => { + const extra = new Map(); + extra.set("/v3/memories/", { status: 200, body: { results: [] } }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.getAll({ filters: { user_id: "u1" }, page: 0, pageSize: 0 }); + + const call = findFetchCall(mock, "/v3/memories/", "POST"); + expect(call![0]).toContain("page=0"); + expect(call![0]).toContain("page_size=0"); + }); +}); diff --git a/mem0/client/main.py b/mem0/client/main.py index a1b68d73b..a52986dcd 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -275,11 +275,8 @@ class MemoryClient: kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs} params = self._prepare_params(kwargs) - if "page" in params and "page_size" in params: - query_params = { - "page": params.pop("page"), - "page_size": params.pop("page_size"), - } + query_params = {key: params.pop(key) for key in ("page", "page_size") if key in params} + if query_params: response = self.client.post("/v3/memories/", json=params, params=query_params) else: response = self.client.post("/v3/memories/", json=params) @@ -1198,11 +1195,8 @@ class AsyncMemoryClient: kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs} params = self._prepare_params(kwargs) - if "page" in params and "page_size" in params: - query_params = { - "page": params.pop("page"), - "page_size": params.pop("page_size"), - } + query_params = {key: params.pop(key) for key in ("page", "page_size") if key in params} + if query_params: response = await self.async_client.post("/v3/memories/", json=params, params=query_params) else: response = await self.async_client.post("/v3/memories/", json=params) diff --git a/tests/test_client.py b/tests/test_client.py index d7d11ca86..07fc2052b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,6 +8,7 @@ import httpx import pytest import requests +from mem0.client.main import AsyncMemoryClient from mem0.client.types import GetAllMemoryOptions, SearchMemoryOptions @@ -181,6 +182,150 @@ class TestGetAllTypedOptionsParity: ) +class TestGetAllPageSizeQueryParams: + """MEM-6227: page and page_size must reach the API as query params independently.""" + + def test_get_all_page_size_alone_lands_in_query_params(self, mock_memory_client): + """get_all(page_size=...) without page should send page_size as a query param.""" + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + mock_memory_client.client.post.return_value = mock_response + + mock_memory_client.get_all(filters={"user_id": "u1"}, page_size=50) + + mock_memory_client.client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page_size": 50}, + ) + + def test_get_all_page_alone_lands_in_query_params(self, mock_memory_client): + """get_all(page=...) without page_size should send page as a query param.""" + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + mock_memory_client.client.post.return_value = mock_response + + mock_memory_client.get_all(filters={"user_id": "u1"}, page=2) + + mock_memory_client.client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page": 2}, + ) + + def test_get_all_page_and_page_size_together(self, mock_memory_client): + """get_all(page=..., page_size=...) should send both as query params.""" + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + mock_memory_client.client.post.return_value = mock_response + + mock_memory_client.get_all(filters={"user_id": "u1"}, page=2, page_size=50) + + mock_memory_client.client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page": 2, "page_size": 50}, + ) + + def test_get_all_neither_page_nor_page_size_sends_no_query_params(self, mock_memory_client): + """get_all() without page or page_size should not pass a params kwarg.""" + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + mock_memory_client.client.post.return_value = mock_response + + mock_memory_client.get_all(filters={"user_id": "u1"}) + + mock_memory_client.client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + ) + + def test_async_get_all_page_size_alone_lands_in_query_params(self): + asyncio.run(self._assert_async_get_all_page_size_alone()) + + async def _assert_async_get_all_page_size_alone(self): + client = AsyncMemoryClient.__new__(AsyncMemoryClient) + client.async_client = MagicMock() + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + client.async_client.post = AsyncMock(return_value=mock_response) + + with patch("mem0.client.main.capture_client_event"): + await client.get_all(filters={"user_id": "u1"}, page_size=50) + + client.async_client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page_size": 50}, + ) + + def test_async_get_all_page_alone_lands_in_query_params(self): + asyncio.run(self._assert_async_get_all_page_alone()) + + async def _assert_async_get_all_page_alone(self): + """get_all(page=0) without page_size should still send page=0 as a query param.""" + client = AsyncMemoryClient.__new__(AsyncMemoryClient) + client.async_client = MagicMock() + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + client.async_client.post = AsyncMock(return_value=mock_response) + + with patch("mem0.client.main.capture_client_event"): + await client.get_all(filters={"user_id": "u1"}, page=0) + + client.async_client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page": 0}, + ) + + def test_async_get_all_page_and_page_size_together(self): + asyncio.run(self._assert_async_get_all_page_and_page_size_together()) + + async def _assert_async_get_all_page_and_page_size_together(self): + """get_all(page=0, page_size=0) should send both as query params despite being falsy.""" + client = AsyncMemoryClient.__new__(AsyncMemoryClient) + client.async_client = MagicMock() + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + client.async_client.post = AsyncMock(return_value=mock_response) + + with patch("mem0.client.main.capture_client_event"): + await client.get_all(filters={"user_id": "u1"}, page=0, page_size=0) + + client.async_client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + params={"page": 0, "page_size": 0}, + ) + + def test_async_get_all_neither_page_nor_page_size_sends_no_query_params(self): + asyncio.run(self._assert_async_get_all_neither_page_nor_page_size()) + + async def _assert_async_get_all_neither_page_nor_page_size(self): + client = AsyncMemoryClient.__new__(AsyncMemoryClient) + client.async_client = MagicMock() + mock_response = MagicMock() + mock_response.json.return_value = {"results": []} + mock_response.raise_for_status.return_value = None + client.async_client.post = AsyncMock(return_value=mock_response) + + with patch("mem0.client.main.capture_client_event"): + await client.get_all(filters={"user_id": "u1"}) + + client.async_client.post.assert_called_once_with( + "/v3/memories/", + json={"filters": {"user_id": "u1"}}, + ) + + class TestUpdateExpirationDate: """Tests for update expiration_date payload handling."""