fix: encode dynamic URL path segments (#5963)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
VectorPeak
2026-07-07 18:41:54 +08:00
committed by GitHub
parent 87276ef968
commit cc52f0e367
9 changed files with 374 additions and 39 deletions
+30 -14
View File
@@ -17,6 +17,10 @@ import {
type SearchOptions,
} from "./base.js";
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
@@ -218,9 +222,13 @@ export class PlatformBackend implements Backend {
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
return (await this._request(
"GET",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
}
async listMemories(
@@ -277,9 +285,13 @@ export class PlatformBackend implements Backend {
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
payload.source = "CLI";
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
return (await this._request(
"PUT",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
json: payload,
},
)) as Record<string, unknown>;
}
async delete(
@@ -297,9 +309,13 @@ export class PlatformBackend implements Backend {
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
return (await this._request(
"DELETE",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
@@ -323,7 +339,7 @@ export class PlatformBackend implements Backend {
for (const [entityType, entityId] of entities) {
results[entityType] = (await this._request(
"DELETE",
`/v2/entities/${entityType}/${entityId}/`,
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
@@ -386,9 +402,9 @@ export class PlatformBackend implements Backend {
}
async getEvent(eventId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
string,
unknown
>;
return (await this._request(
"GET",
`/v1/event/${encodePathSegment(eventId)}/`,
)) as Record<string, unknown>;
}
}
+49 -1
View File
@@ -2,7 +2,7 @@
* Tests for the Platform backend (mem0 Platform API client).
*/
import { describe, it, expect, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PlatformBackend } from "../src/backend/platform.js";
import { createDefaultConfig } from "../src/config.js";
@@ -12,6 +12,22 @@ function makeBackend(): PlatformBackend {
return new PlatformBackend(createDefaultConfig().platform);
}
function mockFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: vi.fn().mockReturnValue(null) },
json: vi.fn().mockResolvedValue({ message: "ok" }),
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("deleteEntities", () => {
it("returns all results keyed by entity type for a multi-entity delete", async () => {
const backend = makeBackend();
@@ -50,3 +66,35 @@ describe("deleteEntities", () => {
);
});
});
describe("PlatformBackend path encoding", () => {
it("encodes memory IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.get("mem/a?b#c");
await backend.update("mem/a?b#c", "updated");
await backend.delete("mem/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
]);
});
it("encodes entity and event IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.deleteEntities({ userId: "org/team?active#frag" });
await backend.getEvent("evt/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
]);
});
});
+24 -5
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Any
from urllib.parse import quote
import httpx
@@ -11,6 +12,10 @@ from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
def _encode_path_segment(value: Any) -> str:
return quote(str(value), safe="")
class PlatformBackend(Backend):
"""Backend that talks to the mem0 Platform API."""
@@ -196,7 +201,11 @@ class PlatformBackend(Backend):
)
def get(self, memory_id: str) -> dict:
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
return self._request(
"GET",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
def list_memories(
self,
@@ -250,7 +259,11 @@ class PlatformBackend(Backend):
if metadata:
payload["metadata"] = metadata
payload["source"] = "CLI"
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
return self._request(
"PUT",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
json=payload,
)
def delete(
self,
@@ -274,7 +287,11 @@ class PlatformBackend(Backend):
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
return self._request(
"DELETE",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
else:
raise ValueError("Either memory_id or --all is required")
@@ -302,7 +319,9 @@ class PlatformBackend(Backend):
results: dict = {}
for entity_type, entity_id in entities.items():
results[entity_type] = self._request(
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
"DELETE",
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
params={"source": "CLI"},
)
return results
@@ -348,7 +367,7 @@ class PlatformBackend(Backend):
return result if isinstance(result, list) else result.get("results", [])
def get_event(self, event_id: str) -> dict:
return self._request("GET", f"/v1/event/{event_id}/")
return self._request("GET", f"/v1/event/{_encode_path_segment(event_id)}/")
class AuthError(Exception):
@@ -0,0 +1,43 @@
from unittest.mock import MagicMock
from mem0_cli.backend.platform import PlatformBackend
def _backend(sample_config):
backend = PlatformBackend(sample_config.platform)
backend._client = MagicMock()
backend._client.request.return_value = MagicMock(
status_code=200,
json=lambda: {"message": "ok"},
headers={},
raise_for_status=lambda: None,
)
return backend
def test_memory_id_path_segments_are_encoded(sample_config):
backend = _backend(sample_config)
backend.get("mem/a?b#c")
backend.update("mem/a?b#c", content="updated")
backend.delete("mem/a?b#c")
paths = [call.args[1] for call in backend._client.request.call_args_list]
assert paths == [
"/v1/memories/mem%2Fa%3Fb%23c/",
"/v1/memories/mem%2Fa%3Fb%23c/",
"/v1/memories/mem%2Fa%3Fb%23c/",
]
def test_entity_and_event_path_segments_are_encoded(sample_config):
backend = _backend(sample_config)
backend.delete_entities(user_id="org/team?active#frag")
backend.get_event("evt/a?b#c")
paths = [call.args[1] for call in backend._client.request.call_args_list]
assert paths == [
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
"/v1/event/evt%2Fa%3Fb%23c/",
]
+12 -6
View File
@@ -67,6 +67,10 @@ function rejectTopLevelEntityParams(
}
}
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
class APIError extends Error {
constructor(message: string) {
super(message);
@@ -311,7 +315,7 @@ export default class MemoryClient {
this._captureEvent("update", [payloadKeys]);
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
{
method: "PUT",
headers: this.headers,
@@ -325,7 +329,7 @@ export default class MemoryClient {
if (this.telemetryId === "") await this.ping();
this._captureEvent("get", []);
return this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
{
headers: this.headers,
},
@@ -397,7 +401,7 @@ export default class MemoryClient {
// @ts-ignore
const query = new URLSearchParams(snakeOptions).toString();
return this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/${query ? `?${query}` : ""}`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/${query ? `?${query}` : ""}`,
{
method: "DELETE",
headers: this.headers,
@@ -428,7 +432,7 @@ export default class MemoryClient {
if (this.telemetryId === "") await this.ping();
this._captureEvent("history", []);
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/history/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/history/`,
{
headers: this.headers,
},
@@ -466,7 +470,7 @@ export default class MemoryClient {
data.entity_type = "user";
}
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/entities/${data.entity_type}/${data.entity_id}/`,
`${this.host}/v1/entities/${encodePathSegment(data.entity_type)}/${encodePathSegment(data.entity_id)}/`,
{
method: "DELETE",
headers: this.headers,
@@ -510,7 +514,9 @@ export default class MemoryClient {
for (const entity of to_delete) {
try {
await this.client.delete(`/v2/entities/${entity.type}/${entity.name}/`);
await this.client.delete(
`/v2/entities/${encodePathSegment(entity.type)}/${encodePathSegment(entity.name)}/`,
);
} catch (error: any) {
throw new APIError(
`Failed to delete ${entity.type} ${entity.name}: ${error.message}`,
@@ -118,6 +118,20 @@ describe("MemoryClient - get()", () => {
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await expect(client.get("nonexistent")).rejects.toThrow("Memory not found");
});
test("URL-encodes memory ID path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: createMockMemory({ id: "mem/a?b#c" }),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.get("mem/a?b#c");
expect(findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/")).toBeDefined();
});
});
// ─── update() ────────────────────────────────────────────
@@ -213,6 +227,22 @@ describe("MemoryClient - update()", () => {
"At least one of text, metadata, timestamp, or expirationDate must be provided",
);
});
test("URL-encodes memory ID path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: createMockMemory({ id: "mem/a?b#c" }),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.update("mem/a?b#c", { text: "Updated text" });
expect(
findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/", "PUT"),
).toBeDefined();
});
});
// ─── delete() ────────────────────────────────────────────
@@ -250,6 +280,22 @@ describe("MemoryClient - delete()", () => {
expect(call).toBeDefined();
expect(call![0]).toContain("delete_linked=true");
});
test("URL-encodes memory ID path segments before adding query params", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: { message: "Memory deleted successfully" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.delete("mem/a?b#c", { deleteLinked: true });
const call = findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/", "DELETE");
expect(call).toBeDefined();
expect(call![0]).toContain("delete_linked=true");
});
});
// ─── deleteAll() ─────────────────────────────────────────
@@ -326,4 +372,23 @@ describe("MemoryClient - history()", () => {
const result: MemoryHistory[] = await client.history("mem_123");
expect(result).toEqual([]);
});
test("URL-encodes memory ID path segments", async () => {
const historyEntries = [
createMockMemoryHistory({ memory_id: "mem/a?b#c" }),
];
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/history/", {
status: 200,
body: historyEntries,
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.history("mem/a?b#c");
expect(
findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/history/"),
).toBeDefined();
});
});
@@ -71,4 +71,43 @@ describe("MemoryClient - deleteUser() (deprecated)", () => {
findFetchCall(mock, "/v1/entities/user/456/", "DELETE"),
).toBeDefined();
});
test("URL-encodes entity path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/entities/team%2Ftype/org%2Fteam%3Factive%23frag/", {
status: 200,
body: { message: "Entity deleted successfully!" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.deleteUser({
entity_id: "org/team?active#frag" as never,
entity_type: "team/type",
});
expect(
findFetchCall(
mock,
"/v1/entities/team%2Ftype/org%2Fteam%3Factive%23frag/",
"DELETE",
),
).toBeDefined();
});
});
describe("MemoryClient - deleteUsers()", () => {
test("URL-encodes entity path segments", async () => {
setupMockFetch();
const client = new MemoryClient({ apiKey: TEST_API_KEY });
client.telemetryId = "test@example.com";
const deleteMock = jest.fn().mockResolvedValue({});
client.client = { delete: deleteMock };
await client.deleteUsers({ userId: "org/team?active#frag" });
expect(deleteMock).toHaveBeenCalledWith(
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
);
});
});
+32 -12
View File
@@ -3,6 +3,7 @@ import logging
import os
import warnings
from typing import Any, Dict, List, Optional
from urllib.parse import quote
import httpx
import requests
@@ -42,6 +43,10 @@ def _validate_and_trim_search_query(query: str) -> str:
return trimmed
def _encode_path_segment(value: Any) -> str:
return quote(str(value), safe="")
def _maybe_alias_anon_to_email(user_email):
"""Fire $identify per prior anon ID so PostHog merges them into email.
@@ -229,7 +234,7 @@ class MemoryClient:
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
"""
params = self._prepare_params()
response = self.client.get(f"/v1/memories/{memory_id}/", params=params)
response = self.client.get(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
response.raise_for_status()
capture_client_event("client.get", self, {"memory_id": memory_id, "sync_type": "sync"})
return response.json()
@@ -367,7 +372,7 @@ class MemoryClient:
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
params = self._prepare_params()
response = self.client.put(f"/v1/memories/{memory_id}/", json=payload, params=params)
response = self.client.put(f"/v1/memories/{_encode_path_segment(memory_id)}/", json=payload, params=params)
response.raise_for_status()
return response.json()
@@ -395,7 +400,7 @@ class MemoryClient:
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
"""
params = self._prepare_params({"delete_linked": delete_linked or None})
response = self.client.delete(f"/v1/memories/{memory_id}/", params=params)
response = self.client.delete(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
response.raise_for_status()
capture_client_event(
"client.delete", self, {"memory_id": memory_id, "delete_linked": delete_linked, "sync_type": "sync"}
@@ -452,7 +457,7 @@ class MemoryClient:
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
"""
params = self._prepare_params()
response = self.client.get(f"/v1/memories/{memory_id}/history/", params=params)
response = self.client.get(f"/v1/memories/{_encode_path_segment(memory_id)}/history/", params=params)
response.raise_for_status()
capture_client_event("client.history", self, {"memory_id": memory_id, "sync_type": "sync"})
return response.json()
@@ -513,7 +518,10 @@ class MemoryClient:
# Delete entities and check response immediately
for entity in to_delete:
response = self.client.delete(f"/v2/entities/{entity['type']}/{entity['name']}/", params=params)
response = self.client.delete(
f"/v2/entities/{_encode_path_segment(entity['type'])}/{_encode_path_segment(entity['name'])}/",
params=params,
)
response.raise_for_status()
capture_client_event(
@@ -691,7 +699,9 @@ class MemoryClient:
ValueError: If org_id or project_id are not set.
"""
logger.warning(
"get_project() method is going to be deprecated in version v1.0 of the package. Please use the client.project.get() method instead."
"get_project() method is going to be deprecated in version v1.0 "
"of the package. Please use the client.project.get() method "
"instead."
)
if not (self.org_id and self.project_id):
raise ValueError("org_id and project_id must be set to access instructions or categories")
@@ -1147,7 +1157,7 @@ class AsyncMemoryClient:
@api_error_handler
async def get(self, memory_id: str) -> Dict[str, Any]:
params = self._prepare_params()
response = await self.async_client.get(f"/v1/memories/{memory_id}/", params=params)
response = await self.async_client.get(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
response.raise_for_status()
capture_client_event("client.get", self, {"memory_id": memory_id, "sync_type": "async"})
return response.json()
@@ -1285,7 +1295,9 @@ class AsyncMemoryClient:
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
params = self._prepare_params()
response = await self.async_client.put(f"/v1/memories/{memory_id}/", json=payload, params=params)
response = await self.async_client.put(
f"/v1/memories/{_encode_path_segment(memory_id)}/", json=payload, params=params
)
response.raise_for_status()
return response.json()
@@ -1313,7 +1325,7 @@ class AsyncMemoryClient:
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
"""
params = self._prepare_params({"delete_linked": delete_linked or None})
response = await self.async_client.delete(f"/v1/memories/{memory_id}/", params=params)
response = await self.async_client.delete(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
response.raise_for_status()
capture_client_event(
"client.delete", self, {"memory_id": memory_id, "delete_linked": delete_linked, "sync_type": "async"}
@@ -1365,7 +1377,10 @@ class AsyncMemoryClient:
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
"""
params = self._prepare_params()
response = await self.async_client.get(f"/v1/memories/{memory_id}/history/", params=params)
response = await self.async_client.get(
f"/v1/memories/{_encode_path_segment(memory_id)}/history/",
params=params,
)
response.raise_for_status()
capture_client_event("client.history", self, {"memory_id": memory_id, "sync_type": "async"})
return response.json()
@@ -1426,7 +1441,10 @@ class AsyncMemoryClient:
# Delete entities and check response immediately
for entity in to_delete:
response = await self.async_client.delete(f"/v2/entities/{entity['type']}/{entity['name']}/", params=params)
response = await self.async_client.delete(
f"/v2/entities/{_encode_path_segment(entity['type'])}/{_encode_path_segment(entity['name'])}/",
params=params,
)
response.raise_for_status()
capture_client_event(
@@ -1590,7 +1608,9 @@ class AsyncMemoryClient:
ValueError: If org_id or project_id are not set.
"""
logger.warning(
"get_project() method is going to be deprecated in version v1.0 of the package. Please use the client.project.get() method instead."
"get_project() method is going to be deprecated in version v1.0 "
"of the package. Please use the client.project.get() method "
"instead."
)
if not (self.org_id and self.project_id):
raise ValueError("org_id and project_id must be set to access instructions or categories")
+80 -1
View File
@@ -1,7 +1,8 @@
"""Tests for MemoryClient entity parameter rejection."""
import asyncio
import json
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
@@ -259,6 +260,84 @@ class TestDeleteLinked:
assert "delete_linked" not in call_args.kwargs.get("params", {})
class TestPathSegmentEncoding:
"""Path params should remain one URL segment even when IDs contain URL syntax."""
def _setup_response(self):
return MagicMock(json=lambda: {"message": "ok"}, raise_for_status=lambda: None)
def _called_paths(self, mock_method):
return [call.args[0] for call in mock_method.call_args_list]
def test_sync_memory_id_path_segments_are_encoded(self, mock_memory_client):
mock_memory_client.client.get.return_value = self._setup_response()
mock_memory_client.client.put.return_value = self._setup_response()
mock_memory_client.client.delete.return_value = self._setup_response()
memory_id = "mem/a?b#c"
mock_memory_client.get(memory_id)
mock_memory_client.update(memory_id, text="updated")
mock_memory_client.delete(memory_id)
mock_memory_client.history(memory_id)
assert "/v1/memories/mem%2Fa%3Fb%23c/" in self._called_paths(mock_memory_client.client.get)
assert mock_memory_client.client.put.call_args.args[0] == "/v1/memories/mem%2Fa%3Fb%23c/"
assert mock_memory_client.client.delete.call_args.args[0] == "/v1/memories/mem%2Fa%3Fb%23c/"
assert "/v1/memories/mem%2Fa%3Fb%23c/history/" in self._called_paths(mock_memory_client.client.get)
def test_sync_entity_path_segments_are_encoded(self, mock_memory_client):
mock_memory_client.client.delete.return_value = self._setup_response()
mock_memory_client.delete_users(user_id="org/team?active#frag")
assert mock_memory_client.client.delete.call_args.args[0] == (
"/v2/entities/user/org%2Fteam%3Factive%23frag/"
)
def test_async_memory_id_path_segments_are_encoded(self):
asyncio.run(self._assert_async_memory_id_path_segments_are_encoded())
async def _assert_async_memory_id_path_segments_are_encoded(self):
from mem0.client.main import AsyncMemoryClient
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
client.async_client.get = AsyncMock(return_value=self._setup_response())
client.async_client.put = AsyncMock(return_value=self._setup_response())
client.async_client.delete = AsyncMock(return_value=self._setup_response())
memory_id = "mem/a?b#c"
with patch("mem0.client.main.capture_client_event"):
await client.get(memory_id)
await client.update(memory_id, text="updated")
await client.delete(memory_id)
await client.history(memory_id)
assert client.async_client.get.call_args_list[0].args[0] == "/v1/memories/mem%2Fa%3Fb%23c/"
assert client.async_client.put.call_args.args[0] == "/v1/memories/mem%2Fa%3Fb%23c/"
assert client.async_client.delete.call_args.args[0] == "/v1/memories/mem%2Fa%3Fb%23c/"
assert client.async_client.get.call_args_list[1].args[0] == "/v1/memories/mem%2Fa%3Fb%23c/history/"
def test_async_entity_path_segments_are_encoded(self):
asyncio.run(self._assert_async_entity_path_segments_are_encoded())
async def _assert_async_entity_path_segments_are_encoded(self):
from mem0.client.main import AsyncMemoryClient
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
client.async_client.delete = AsyncMock(return_value=self._setup_response())
with patch("mem0.client.main.capture_client_event"):
await client.delete_users(user_id="org/team?active#frag")
assert client.async_client.delete.call_args.args[0] == (
"/v2/entities/user/org%2Fteam%3Factive%23frag/"
)
class TestValidateApiKeyHttpError:
"""_validate_api_key should surface a clear ValueError on a non-JSON HTTP error.