From 9b3bce1924fa5006774b7661a05adc9e4423f7eb Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 25 Apr 2026 17:53:19 -0700 Subject: [PATCH 1/3] fix(memory): jsonify metadata before Prisma writes on /v1/memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The POST/PUT memory endpoints handed bare dicts (and bare `None`) to prisma-client-python for the `Json?` `metadata` column, which the client rejects with `MissingRequiredValueError` / `DataError: metadata should be of any of the following types: NullableJsonNullValueInput, Json`. Both the create and upsert paths now route writes through the existing `jsonify_object` helper used elsewhere in the proxy for `Json?` columns (e.g. `LiteLLM_VerificationToken.budget_limits`), and omit metadata when None so the column defaults to SQL NULL via the schema. Explicit `metadata: null` on PUT is now a no-op for the column to match how the rest of the proxy handles nullable JSON fields (no `JsonNull`/`DbNull` sentinel exists in prisma-client-python — see RobertCraigie/prisma-client-py#714). A payload with only `metadata: null` returns 400 instead of a misleading 200. Made-with: Cursor --- litellm/proxy/memory/memory_endpoints.py | 34 ++++-- .../proxy/memory/test_memory_endpoints.py | 103 +++++++++++++++++- 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/litellm/proxy/memory/memory_endpoints.py b/litellm/proxy/memory/memory_endpoints.py index 0cb97f1dccdd..866a35ec2649 100644 --- a/litellm/proxy/memory/memory_endpoints.py +++ b/litellm/proxy/memory/memory_endpoints.py @@ -28,6 +28,7 @@ UserAPIKeyAuth, ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.utils import jsonify_object from litellm.types.memory_management import ( LiteLLM_MemoryRow, MemoryCreateRequest, @@ -273,9 +274,12 @@ async def create_memory( prisma_client = _require_prisma() user_id, team_id = _resolve_scope(user_api_key_dict, body.user_id, body.team_id) - # Prisma's Python client rejects `metadata=None` on a `Json?` field — - # the field must be omitted entirely to store SQL NULL. Build the data - # dict conditionally so we only include metadata when the caller sent it. + # `metadata` is a `Json?` column. Prisma's Python client rejects bare + # dicts/None for `Json?` fields, so we follow the same pattern used by + # `LiteLLM_VerificationToken.budget_limits` writes throughout the + # proxy: run the payload through `jsonify_object` (which `json.dumps` + # any nested dicts) and omit metadata when None so the column defaults + # to SQL NULL via the schema's nullable constraint. create_data: dict = { "key": body.key, "value": body.value, @@ -286,6 +290,7 @@ async def create_memory( } if body.metadata is not None: create_data["metadata"] = body.metadata + create_data = jsonify_object(create_data) try: row = await prisma_client.db.litellm_memorytable.create(data=create_data) @@ -415,19 +420,20 @@ async def upsert_memory( """ prisma_client = _require_prisma() - # Distinguish "metadata omitted from request" from "metadata: null". - # Omitted → don't touch the existing field. Explicit null → clear to SQL NULL. - # `model_fields_set` (Pydantic v2) only contains field names the caller - # actually sent in the payload. + # `metadata` is a `Json?` column. prisma-client-python rejects bare + # dicts/None on `Json?` fields, and there's no `JsonNull`/`DbNull` + # sentinel yet (RobertCraigie/prisma-client-py#714) — so we follow the + # established proxy pattern: only forward metadata when the caller sent + # a non-null value, and let `jsonify_object` stringify the dict for + # Prisma. An explicit `metadata: null` is a no-op for the column (same + # treatment the proxy gives nullable `Json?` fields elsewhere). fields_sent = body.model_fields_set - metadata_explicit = "metadata" in fields_sent + metadata_explicit_value = "metadata" in fields_sent and body.metadata is not None data: dict = {} if body.value is not None: data["value"] = body.value - if metadata_explicit: - # body.metadata may be None here — Prisma update accepts None on a - # nullable Json? field and sets the column to SQL NULL. + if metadata_explicit_value: data["metadata"] = body.metadata if not data: raise HTTPException( @@ -435,6 +441,7 @@ async def upsert_memory( detail="Request body must include at least one of: value, metadata.", ) data["updated_by"] = user_api_key_dict.user_id + data = jsonify_object(data) async def _find_existing() -> Any: """Return the caller-visible row for `key`, or None.""" @@ -467,7 +474,9 @@ async def _find_existing() -> Any: user_id, team_id = _resolve_scope( user_api_key_dict, body.user_id, body.team_id ) - # Omit `metadata` when None — Prisma rejects None on Json? fields. + # Omit `metadata` when None so the column defaults to SQL NULL, + # and run the rest through `jsonify_object` to stringify dicts + # for Prisma — same pattern as `create_memory` above. create_data: dict = { "key": key, "value": body.value, @@ -478,6 +487,7 @@ async def _find_existing() -> Any: } if body.metadata is not None: create_data["metadata"] = body.metadata + create_data = jsonify_object(create_data) try: row = await prisma_client.db.litellm_memorytable.create( data=create_data diff --git a/tests/test_litellm/proxy/memory/test_memory_endpoints.py b/tests/test_litellm/proxy/memory/test_memory_endpoints.py index 4a38837a23c1..aadfb0599c96 100644 --- a/tests/test_litellm/proxy/memory/test_memory_endpoints.py +++ b/tests/test_litellm/proxy/memory/test_memory_endpoints.py @@ -88,18 +88,30 @@ def _filter(self, where: Optional[Dict[str, Any]]) -> List[MagicMock]: return [r for r in self.rows if self._matches(r, where)] async def create(self, data: Dict[str, Any]) -> MagicMock: + import json as _json + # Key is globally unique. for r in self.rows: if r.key == data["key"]: raise Exception("UniqueViolation: duplicate key") self._counter += 1 + # Mirror real Prisma read-side behavior for `Json?` columns: writes + # come in as JSON strings (the endpoint pre-processes via + # `jsonify_object`), and Prisma deserializes them back to Python + # values on read. + metadata = data.get("metadata") + if isinstance(metadata, str): + try: + metadata = _json.loads(metadata) + except ValueError: + pass row = _make_row( memory_id=f"mem-{self._counter}", key=data["key"], value=data["value"], user_id=data.get("user_id"), team_id=data.get("team_id"), - metadata=data.get("metadata"), + metadata=metadata, ) row.created_by = data.get("created_by") row.updated_by = data.get("updated_by") @@ -125,9 +137,19 @@ async def find_many( return out async def update(self, where: Dict[str, Any], data: Dict[str, Any]) -> MagicMock: + import json as _json + for r in self.rows: if r.memory_id == where["memory_id"]: for k, v in data.items(): + # Mirror real Prisma's read behavior for `Json?` columns: + # the endpoint sends JSON strings via `jsonify_object`, + # and Prisma round-trips them back to Python values. + if k == "metadata" and isinstance(v, str): + try: + v = _json.loads(v) + except ValueError: + pass setattr(r, k, v) return r raise Exception("Not found") @@ -218,6 +240,42 @@ def test_create_memory_defaults_scope_to_caller(self): assert body["user_id"] == "user-a" assert body["team_id"] == "team-a" + def test_create_memory_with_metadata_jsonifies_for_prisma(self): + """ + Regression: prisma-client-python rejects bare dicts / None on `Json?` + columns with `DataError: metadata should be of any of the following + types: NullableJsonNullValueInput, Json`. The endpoint follows the + rest of the proxy's pattern (`jsonify_object`) and JSON-encodes dict + metadata to a string before handing it to Prisma. + """ + import json as _json + + table = self.prisma.db.litellm_memorytable + original_create = table.create + captured: Dict[str, Any] = {} + + async def spy_create(data: Dict[str, Any]): + captured["data"] = dict(data) + return await original_create(data) + + table.create = spy_create # type: ignore[assignment] + + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.post( + "/v1/memory", + json={ + "key": "agent_memory_id", + "value": "hello world", + "metadata": {"key": "value"}, + }, + ) + assert resp.status_code == 200, resp.text + sent_metadata = captured["data"]["metadata"] + assert isinstance(sent_metadata, str) + assert _json.loads(sent_metadata) == {"key": "value"} + assert resp.json()["metadata"] == {"key": "value"} + def test_create_memory_duplicate_key_returns_409(self): client = _make_client(_user_auth("user-a", "team-a")) with _patch_prisma(self.prisma): @@ -459,8 +517,16 @@ def test_put_memory_updates_existing(self): assert resp.json()["value"] == "new" assert len(table.rows) == 1 - def test_put_memory_explicit_null_metadata_clears_field(self): - """PUT with `metadata: null` should clear the metadata column (not silently drop the field).""" + def test_put_memory_explicit_null_metadata_is_noop(self): + """ + prisma-client-python can't write a true SQL NULL to a `Json?` column + (no `JsonNull`/`DbNull` sentinel — see + RobertCraigie/prisma-client-py#714). We mirror the rest of the proxy + and treat `metadata: null` as "leave the column alone" rather than + 500ing or silently writing a JSON `null`. The caller can still + update other fields in the same request; existing metadata is + preserved. + """ table = self.prisma.db.litellm_memorytable table.rows.append( _make_row( @@ -474,11 +540,36 @@ def test_put_memory_explicit_null_metadata_clears_field(self): ) client = _make_client(_user_auth("user-a", "team-a")) with _patch_prisma(self.prisma): - resp = client.put("/v1/memory/notes", json={"metadata": None}) + resp = client.put( + "/v1/memory/notes", json={"value": "new", "metadata": None} + ) assert resp.status_code == 200, resp.text body = resp.json() - assert body["metadata"] is None - assert table.rows[0].metadata is None + assert body["value"] == "new" + assert body["metadata"] == {"tag": "old"} + assert table.rows[0].metadata == {"tag": "old"} + + def test_put_memory_null_metadata_alone_returns_400(self): + """ + With explicit-null treated as a no-op, a payload that ONLY carries + `metadata: null` has no effective fields to write — surface 400 so + the caller doesn't get a misleading 200 with no state change. + """ + table = self.prisma.db.litellm_memorytable + table.rows.append( + _make_row( + memory_id="m1", + key="notes", + value="v", + user_id="user-a", + team_id="team-a", + metadata={"tag": "old"}, + ) + ) + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.put("/v1/memory/notes", json={"metadata": None}) + assert resp.status_code == 400 def test_put_memory_omitted_metadata_preserves_field(self): """PUT without a metadata field should NOT touch the stored metadata.""" From 84be9e8b1d148ef2e79f13fab417d9f702524162 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 25 Apr 2026 18:07:24 -0700 Subject: [PATCH 2/3] fix(memory): JSON-encode non-dict metadata before Prisma writes `jsonify_object` only stringifies dict values, so list-shaped metadata still hit Prisma as raw Python objects and triggered the same DataError this PR is meant to fix. `metadata` is typed `Optional[Any]` so list payloads are valid input. Replace `jsonify_object` with a local `_serialize_metadata_for_prisma` helper that always `json.dumps` non-string values, applied at all three write sites (POST create, PUT update, PUT-create). Adds regression tests for list metadata on each path. Co-Authored-By: Claude Opus 4.7 (1M context) --- litellm/proxy/memory/memory_endpoints.py | 59 ++++++----- .../proxy/memory/test_memory_endpoints.py | 98 +++++++++++++++++++ 2 files changed, 134 insertions(+), 23 deletions(-) diff --git a/litellm/proxy/memory/memory_endpoints.py b/litellm/proxy/memory/memory_endpoints.py index 866a35ec2649..9ae5b3bf24e4 100644 --- a/litellm/proxy/memory/memory_endpoints.py +++ b/litellm/proxy/memory/memory_endpoints.py @@ -17,6 +17,7 @@ the caller is a PROXY_ADMIN who explicitly supplies a different scope. """ +import json from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query @@ -28,7 +29,6 @@ UserAPIKeyAuth, ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.utils import jsonify_object from litellm.types.memory_management import ( LiteLLM_MemoryRow, MemoryCreateRequest, @@ -40,6 +40,21 @@ router = APIRouter() +def _serialize_metadata_for_prisma(metadata: Any) -> str: + """ + Encode a `metadata` payload for the `Json?` column. + + `metadata` is typed `Optional[Any]` — callers may send dicts, lists, or + JSON scalars. prisma-client-python rejects raw Python values on `Json?` + columns (`MissingRequiredValueError` / `DataError`), so we always + `json.dumps` here. Strings are passed through unchanged so callers that + already pre-serialize aren't double-encoded. + """ + if isinstance(metadata, str): + return metadata + return json.dumps(metadata) + + def _is_admin(user_api_key_dict: UserAPIKeyAuth) -> bool: return user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN @@ -274,12 +289,9 @@ async def create_memory( prisma_client = _require_prisma() user_id, team_id = _resolve_scope(user_api_key_dict, body.user_id, body.team_id) - # `metadata` is a `Json?` column. Prisma's Python client rejects bare - # dicts/None for `Json?` fields, so we follow the same pattern used by - # `LiteLLM_VerificationToken.budget_limits` writes throughout the - # proxy: run the payload through `jsonify_object` (which `json.dumps` - # any nested dicts) and omit metadata when None so the column defaults - # to SQL NULL via the schema's nullable constraint. + # `metadata` is a `Json?` column — prisma-client-python rejects raw + # Python values, so JSON-encode any non-null payload and omit the field + # entirely when None so the column defaults to SQL NULL. create_data: dict = { "key": body.key, "value": body.value, @@ -289,8 +301,7 @@ async def create_memory( "updated_by": user_api_key_dict.user_id, } if body.metadata is not None: - create_data["metadata"] = body.metadata - create_data = jsonify_object(create_data) + create_data["metadata"] = _serialize_metadata_for_prisma(body.metadata) try: row = await prisma_client.db.litellm_memorytable.create(data=create_data) @@ -420,13 +431,17 @@ async def upsert_memory( """ prisma_client = _require_prisma() - # `metadata` is a `Json?` column. prisma-client-python rejects bare - # dicts/None on `Json?` fields, and there's no `JsonNull`/`DbNull` - # sentinel yet (RobertCraigie/prisma-client-py#714) — so we follow the - # established proxy pattern: only forward metadata when the caller sent - # a non-null value, and let `jsonify_object` stringify the dict for - # Prisma. An explicit `metadata: null` is a no-op for the column (same - # treatment the proxy gives nullable `Json?` fields elsewhere). + # `metadata` is a `Json?` column. prisma-client-python rejects raw + # Python values on `Json?` fields, and there is no `JsonNull`/`DbNull` + # sentinel yet (RobertCraigie/prisma-client-py#714) so we have no way + # to write a true SQL NULL via the typed client. We mirror the rest of + # the proxy's handling of nullable `Json?` columns: forward metadata + # only when the caller sent a non-null value (always JSON-encoded), + # and treat explicit `metadata: null` as a no-op for the column. This + # matches the prior crashing behavior the PR fixes — there is no + # regression of a previously-working "clear metadata" path, and the + # rest of the proxy gives the same treatment to nullable `Json?` + # fields elsewhere. fields_sent = body.model_fields_set metadata_explicit_value = "metadata" in fields_sent and body.metadata is not None @@ -434,14 +449,13 @@ async def upsert_memory( if body.value is not None: data["value"] = body.value if metadata_explicit_value: - data["metadata"] = body.metadata + data["metadata"] = _serialize_metadata_for_prisma(body.metadata) if not data: raise HTTPException( status_code=400, detail="Request body must include at least one of: value, metadata.", ) data["updated_by"] = user_api_key_dict.user_id - data = jsonify_object(data) async def _find_existing() -> Any: """Return the caller-visible row for `key`, or None.""" @@ -474,9 +488,9 @@ async def _find_existing() -> Any: user_id, team_id = _resolve_scope( user_api_key_dict, body.user_id, body.team_id ) - # Omit `metadata` when None so the column defaults to SQL NULL, - # and run the rest through `jsonify_object` to stringify dicts - # for Prisma — same pattern as `create_memory` above. + # Omit `metadata` when None so the column defaults to SQL NULL; + # otherwise JSON-encode for Prisma — same pattern as + # `create_memory` above. create_data: dict = { "key": key, "value": body.value, @@ -486,8 +500,7 @@ async def _find_existing() -> Any: "updated_by": user_api_key_dict.user_id, } if body.metadata is not None: - create_data["metadata"] = body.metadata - create_data = jsonify_object(create_data) + create_data["metadata"] = _serialize_metadata_for_prisma(body.metadata) try: row = await prisma_client.db.litellm_memorytable.create( data=create_data diff --git a/tests/test_litellm/proxy/memory/test_memory_endpoints.py b/tests/test_litellm/proxy/memory/test_memory_endpoints.py index aadfb0599c96..28b7dd3718b9 100644 --- a/tests/test_litellm/proxy/memory/test_memory_endpoints.py +++ b/tests/test_litellm/proxy/memory/test_memory_endpoints.py @@ -276,6 +276,104 @@ async def spy_create(data: Dict[str, Any]): assert _json.loads(sent_metadata) == {"key": "value"} assert resp.json()["metadata"] == {"key": "value"} + def test_create_memory_with_list_metadata_jsonifies_for_prisma(self): + """ + `metadata` is typed `Optional[Any]` — callers may legitimately send + a JSON array (e.g. a list of tag objects). Lists must also be + JSON-stringified before reaching Prisma; otherwise prisma-client- + python raises the same `DataError` this PR is meant to fix. + """ + import json as _json + + table = self.prisma.db.litellm_memorytable + original_create = table.create + captured: Dict[str, Any] = {} + + async def spy_create(data: Dict[str, Any]): + captured["data"] = dict(data) + return await original_create(data) + + table.create = spy_create # type: ignore[assignment] + + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.post( + "/v1/memory", + json={ + "key": "agent_memory_id", + "value": "hello world", + "metadata": [{"tag": "work"}, {"tag": "shared"}], + }, + ) + assert resp.status_code == 200, resp.text + sent_metadata = captured["data"]["metadata"] + assert isinstance(sent_metadata, str) + assert _json.loads(sent_metadata) == [{"tag": "work"}, {"tag": "shared"}] + assert resp.json()["metadata"] == [{"tag": "work"}, {"tag": "shared"}] + + def test_put_memory_with_list_metadata_jsonifies_for_prisma(self): + """Same regression as the POST list-metadata test, but for PUT-create.""" + import json as _json + + table = self.prisma.db.litellm_memorytable + original_create = table.create + captured: Dict[str, Any] = {} + + async def spy_create(data: Dict[str, Any]): + captured["data"] = dict(data) + return await original_create(data) + + table.create = spy_create # type: ignore[assignment] + + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.put( + "/v1/memory/notes", + json={"value": "v", "metadata": [1, 2, 3]}, + ) + assert resp.status_code == 200, resp.text + sent_metadata = captured["data"]["metadata"] + assert isinstance(sent_metadata, str) + assert _json.loads(sent_metadata) == [1, 2, 3] + assert resp.json()["metadata"] == [1, 2, 3] + + def test_put_memory_update_with_list_metadata_jsonifies_for_prisma(self): + """Same regression as the POST list-metadata test, but for PUT-update.""" + import json as _json + + table = self.prisma.db.litellm_memorytable + table.rows.append( + _make_row( + memory_id="m1", + key="notes", + value="old", + user_id="user-a", + team_id="team-a", + metadata={"tag": "old"}, + ) + ) + + original_update = table.update + captured: Dict[str, Any] = {} + + async def spy_update(where, data): + captured["data"] = dict(data) + return await original_update(where, data) + + table.update = spy_update # type: ignore[assignment] + + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.put( + "/v1/memory/notes", + json={"metadata": [{"a": 1}]}, + ) + assert resp.status_code == 200, resp.text + sent_metadata = captured["data"]["metadata"] + assert isinstance(sent_metadata, str) + assert _json.loads(sent_metadata) == [{"a": 1}] + assert resp.json()["metadata"] == [{"a": 1}] + def test_create_memory_duplicate_key_returns_409(self): client = _make_client(_user_auth("user-a", "team-a")) with _patch_prisma(self.prisma): From 9ce2176b2621ec5330352930a29208df02f27ab8 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 25 Apr 2026 18:15:30 -0700 Subject: [PATCH 3/3] fix(memory): always json.dumps metadata, not just non-strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The str-passthrough in `_serialize_metadata_for_prisma` left plain Python strings (e.g. `metadata: "hello"`) unencoded — Postgres `jsonb` rejects bare-word strings as invalid JSON, reproducing the same DataError this PR is meant to fix. Always `json.dumps` regardless of input type so all `Optional[Any]` shapes (dict, list, scalar, str) become valid JSON. Adds a regression test for plain-string metadata. Co-Authored-By: Claude Opus 4.7 (1M context) --- litellm/proxy/memory/memory_endpoints.py | 14 ++++---- .../proxy/memory/test_memory_endpoints.py | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/memory/memory_endpoints.py b/litellm/proxy/memory/memory_endpoints.py index 9ae5b3bf24e4..39d5a03f2d36 100644 --- a/litellm/proxy/memory/memory_endpoints.py +++ b/litellm/proxy/memory/memory_endpoints.py @@ -44,14 +44,14 @@ def _serialize_metadata_for_prisma(metadata: Any) -> str: """ Encode a `metadata` payload for the `Json?` column. - `metadata` is typed `Optional[Any]` — callers may send dicts, lists, or - JSON scalars. prisma-client-python rejects raw Python values on `Json?` - columns (`MissingRequiredValueError` / `DataError`), so we always - `json.dumps` here. Strings are passed through unchanged so callers that - already pre-serialize aren't double-encoded. + `metadata` is typed `Optional[Any]`, so callers may send dicts, lists, + or JSON scalars (including plain Python strings like `"hello"`). + prisma-client-python rejects raw Python values on `Json?` columns + (`MissingRequiredValueError` / `DataError`), and Postgres `jsonb` + rejects bare-word strings as invalid JSON — so always `json.dumps`, + regardless of input type. Roundtrip on read deserializes back to the + original Python value. """ - if isinstance(metadata, str): - return metadata return json.dumps(metadata) diff --git a/tests/test_litellm/proxy/memory/test_memory_endpoints.py b/tests/test_litellm/proxy/memory/test_memory_endpoints.py index 28b7dd3718b9..2ddcbb370b0d 100644 --- a/tests/test_litellm/proxy/memory/test_memory_endpoints.py +++ b/tests/test_litellm/proxy/memory/test_memory_endpoints.py @@ -374,6 +374,38 @@ async def spy_update(where, data): assert _json.loads(sent_metadata) == [{"a": 1}] assert resp.json()["metadata"] == [{"a": 1}] + def test_create_memory_with_string_metadata_jsonifies_for_prisma(self): + """ + `metadata: Optional[Any]` permits JSON scalars too (string, number, + bool). A bare Python string like `"hello"` is NOT valid JSON for + Postgres `jsonb` — it must be JSON-encoded as `"\"hello\""`. + Without that encoding Prisma still raises `DataError`. + """ + import json as _json + + table = self.prisma.db.litellm_memorytable + original_create = table.create + captured: Dict[str, Any] = {} + + async def spy_create(data: Dict[str, Any]): + captured["data"] = dict(data) + return await original_create(data) + + table.create = spy_create # type: ignore[assignment] + + client = _make_client(_user_auth("user-a", "team-a")) + with _patch_prisma(self.prisma): + resp = client.post( + "/v1/memory", + json={"key": "k", "value": "v", "metadata": "hello"}, + ) + assert resp.status_code == 200, resp.text + sent_metadata = captured["data"]["metadata"] + assert isinstance(sent_metadata, str) + # Encoded as JSON string literal — `_json.loads` round-trips back. + assert _json.loads(sent_metadata) == "hello" + assert resp.json()["metadata"] == "hello" + def test_create_memory_duplicate_key_returns_409(self): client = _make_client(_user_auth("user-a", "team-a")) with _patch_prisma(self.prisma):