From df4b88685b6d958d577f0247488f6ff3c880ec7f Mon Sep 17 00:00:00 2001 From: Saket Aryan Date: Thu, 17 Sep 2026 19:40:17 +0530 Subject: [PATCH] fix(client): repair the missed _bounded_stack call site, and test that a client constructs Reported as a blocker by an independent re-review, and correctly: the previous commit changed _bounded_stack to take the caller's entries and our own entry separately, updated _apply_client_headers, and missed _client_stack. That runs on every construction path, so every MemoryClient(...) raised TypeError. A total SDK outage, introduced by the fix for a cosmetic truncation bug. The whole suite stayed green because nothing constructed a client. That is the actual defect here, so the test file exists as much for the gap as for the bug: it builds a client, asserts the header reaches it, and covers the two bounding rules directly. Confirmed it fails against the broken call site and passes against the repaired one. AsyncMemoryClient is deliberately not constructed: its validation path makes a real request to /v1/ping/, and a unit test needing the network is worse than none. It shares _client_headers with the sync client, which is what the construction test guards. tests/test_client_surface_headers.py 5 passed, plugin suites 312 passed 8 skipped. Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb --- mem0/client/main.py | 4 +- tests/test_client_surface_headers.py | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/test_client_surface_headers.py diff --git a/mem0/client/main.py b/mem0/client/main.py index 65f2212b0..3a82d9e5a 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -164,10 +164,8 @@ def _client_headers(api_key: str, user_id: str) -> Dict[str, str]: def _client_stack() -> str: """This SDK appended to any stack an outer layer already declared.""" existing = os.getenv("MEM0_CLIENT_STACK", "").strip() - mine = f"mem0-python/{_sdk_version()}" entries = [part.strip() for part in existing.split(",") if part.strip()] if existing else [] - entries.append(mine) - return _bounded_stack(entries) + return _bounded_stack(entries, f"mem0-python/{_sdk_version()}") class MemoryClient: diff --git a/tests/test_client_surface_headers.py b/tests/test_client_surface_headers.py new file mode 100644 index 000000000..a2602a5bc --- /dev/null +++ b/tests/test_client_surface_headers.py @@ -0,0 +1,65 @@ +"""Surface-identity headers, and that a client can be constructed at all. + +The construction test exists because it was not there: a signature change to +_bounded_stack missed the _client_stack call site, every MemoryClient(...) raised +TypeError, and the whole suite stayed green because nothing built one. +""" + +import os +from unittest.mock import patch + +import pytest + +from mem0.client.main import _bounded_stack, _client_headers, _client_stack + + +def test_a_client_can_be_constructed(): + from mem0 import MemoryClient + + # _validate_api_key normally populates org/project from the API response; + # stubbing it leaves them None, which a later accessor rejects. Set them the + # way a real validation would. This test is about construction reaching the + # header stage at all. + def _stub(self): + self.org_id, self.project_id = "org", "proj" + + with patch.object(MemoryClient, "_validate_api_key", _stub): + client = MemoryClient(api_key="m0-test") + + assert client.client.headers["X-Mem0-Client"].startswith("mem0-python/") + + +# AsyncMemoryClient is deliberately not constructed here: its validation path +# makes a real request to /v1/ping/, and a unit test that needs the network is +# worse than none. It shares _client_headers with the sync client, which is the +# code the construction test above actually guards. +def test_headers_carry_this_sdk(): + headers = _client_headers("m0-test", "u1") + assert headers["X-Mem0-Client"].startswith("mem0-python/") + + +def test_our_entry_survives_a_caller_that_already_filled_the_stack(): + # Appending first and trimming to four dropped exactly the entry the + # function exists to add. + stack = _bounded_stack(["a/1", "b/2", "c/3", "d/4"], "mem0-python/9.9.9") + + assert "mem0-python/9.9.9" in stack + assert len(stack.split(",")) <= 4 + + +def test_the_character_cap_drops_whole_entries_not_characters(): + long_entries = [f"{'n' * 90}/1.0", f"{'m' * 90}/1.0", "c/3"] + stack = _bounded_stack(long_entries, "mem0-python/9.9.9") + + assert len(stack) <= 200 + assert stack.endswith("mem0-python/9.9.9") + for entry in stack.split(","): + assert entry.strip().count("/") == 1, f"severed entry: {entry!r}" + + +def test_an_outer_stack_is_appended_to_not_replaced(): + with patch.dict(os.environ, {"MEM0_CLIENT_STACK": "openclaw/2.1.0"}): + stack = _client_stack() + + assert stack.startswith("openclaw/2.1.0") + assert "mem0-python/" in stack