Download tests/plugins/memory/test_mem0_backend.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 26 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/plugins/memory/test_mem0_backend.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/tests/plugins/memory/test_mem0_backend.py
-
curl -L -o test_mem0_backend.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/plugins/memory/test_mem0_backend.py
26 kB
| """Tests for Mem0Backend abstraction — PlatformBackend, OSSBackend, SelfHostedBackend.""" | |
| import copy | |
| import importlib | |
| import json | |
| import os | |
| import sys | |
| import types | |
| from dataclasses import dataclass, field | |
| from types import SimpleNamespace | |
| import pytest | |
| from plugins.memory.mem0._backend import ( | |
| Mem0Backend, | |
| PlatformBackend, | |
| OSSBackend, | |
| SelfHostedBackend, | |
| ) | |
| class FakePlatformClient: | |
| """Fake MemoryClient for PlatformBackend tests.""" | |
| def __init__(self): | |
| self.calls = [] | |
| def search(self, query, **kwargs): | |
| self.calls.append(("search", query, kwargs)) | |
| return {"results": [{"id": "m1", "memory": "fact1", "score": 0.9}]} | |
| def get_all(self, **kwargs): | |
| self.calls.append(("get_all", kwargs)) | |
| return {"count": 1, "next": None, "results": [{"id": "m1", "memory": "fact1"}]} | |
| def add(self, messages, **kwargs): | |
| self.calls.append(("add", messages, kwargs)) | |
| return {"status": "PENDING", "event_id": "evt-1"} | |
| def update(self, **kwargs): | |
| self.calls.append(("update", kwargs)) | |
| return {"id": kwargs["memory_id"], "text": kwargs["text"]} | |
| def delete(self, **kwargs): | |
| self.calls.append(("delete", kwargs)) | |
| class TestPlatformBackend: | |
| def _make(self): | |
| client = FakePlatformClient() | |
| backend = PlatformBackend.__new__(PlatformBackend) | |
| backend._client = client | |
| return backend, client | |
| def test_search_forwards_params(self): | |
| backend, client = self._make() | |
| result = backend.search("test query", filters={"user_id": "u1"}, top_k=5) | |
| assert client.calls[0][0] == "search" | |
| assert client.calls[0][1] == "test query" | |
| assert client.calls[0][2]["filters"] == {"user_id": "u1"} | |
| assert client.calls[0][2]["top_k"] == 5 | |
| def test_add_forwards_kwargs(self): | |
| backend, client = self._make() | |
| msgs = [{"role": "user", "content": "hi"}] | |
| result = backend.add(msgs, user_id="u1", agent_id="hermes", infer=False) | |
| call = client.calls[0] | |
| assert call[2]["user_id"] == "u1" | |
| assert call[2]["infer"] is False | |
| # metadata kwarg should be omitted entirely when not provided so we | |
| # don't surprise older mem0 client versions with an unknown kwarg. | |
| assert "metadata" not in call[2] | |
| def test_update_forwards(self): | |
| backend, client = self._make() | |
| backend.update("m1", "new text") | |
| assert client.calls[0][1] == {"memory_id": "m1", "text": "new text"} | |
| def test_delete_forwards(self): | |
| backend, client = self._make() | |
| backend.delete("m1") | |
| assert client.calls[0][1] == {"memory_id": "m1"} | |
| class FakeOSSMemory: | |
| """Fake mem0.Memory for OSSBackend tests.""" | |
| def __init__(self): | |
| self.calls = [] | |
| def search(self, query, **kwargs): | |
| self.calls.append(("search", query, kwargs)) | |
| return {"results": [{"id": "m1", "memory": "fact1", "score": 0.8}]} | |
| def get_all(self, **kwargs): | |
| self.calls.append(("get_all", kwargs)) | |
| return {"results": [{"id": "m1", "memory": "fact1"}]} | |
| def add(self, messages, **kwargs): | |
| self.calls.append(("add", messages, kwargs)) | |
| return {"results": [{"id": "m1", "memory": "fact1", "event": "ADD"}]} | |
| def update(self, memory_id, **kwargs): | |
| self.calls.append(("update", memory_id, kwargs)) | |
| return {"message": "Memory updated successfully!"} | |
| def delete(self, memory_id): | |
| self.calls.append(("delete", memory_id)) | |
| return {"message": "Memory deleted successfully!"} | |
| class _FakeMem0State: | |
| factory_registrations: list = field(default_factory=list) | |
| from_config_calls: int = 0 | |
| clients: list = field(default_factory=list) | |
| requests: list = field(default_factory=list) | |
| def _install_fake_mem0(monkeypatch): | |
| """Install a small mem0 2.0.10-shaped surface for OSS backend tests.""" | |
| state = _FakeMem0State() | |
| class BaseLlmConfig: | |
| def __init__( | |
| self, | |
| model=None, | |
| temperature=0.1, | |
| api_key=None, | |
| max_tokens=2000, | |
| top_p=0.1, | |
| top_k=1, | |
| enable_vision=False, | |
| vision_details="auto", | |
| reasoning_effort=None, | |
| http_client_proxies=None, | |
| is_reasoning_model=None, | |
| **kwargs, | |
| ): | |
| self.model = model | |
| self.temperature = temperature | |
| self.api_key = api_key | |
| self.max_tokens = max_tokens | |
| self.top_p = top_p | |
| self.top_k = top_k | |
| self.enable_vision = enable_vision | |
| self.vision_details = vision_details | |
| self.reasoning_effort = reasoning_effort | |
| self.http_client_proxies = http_client_proxies | |
| self.is_reasoning_model = is_reasoning_model | |
| for name, value in kwargs.items(): | |
| setattr(self, name, value) | |
| class OpenAIConfig(BaseLlmConfig): | |
| def __init__( | |
| self, | |
| model=None, | |
| temperature=0.1, | |
| api_key=None, | |
| max_tokens=2000, | |
| top_p=0.1, | |
| top_k=1, | |
| enable_vision=False, | |
| vision_details="auto", | |
| reasoning_effort=None, | |
| http_client_proxies=None, | |
| is_reasoning_model=None, | |
| openai_base_url=None, | |
| models=None, | |
| route="fallback", | |
| openrouter_base_url=None, | |
| site_url=None, | |
| app_name=None, | |
| store=None, | |
| response_callback=None, | |
| ): | |
| super().__init__( | |
| model=model, | |
| temperature=temperature, | |
| api_key=api_key, | |
| max_tokens=max_tokens, | |
| top_p=top_p, | |
| top_k=top_k, | |
| enable_vision=enable_vision, | |
| vision_details=vision_details, | |
| reasoning_effort=reasoning_effort, | |
| http_client_proxies=http_client_proxies, | |
| is_reasoning_model=is_reasoning_model, | |
| ) | |
| self.openai_base_url = openai_base_url | |
| self.models = models | |
| self.route = route | |
| self.openrouter_base_url = openrouter_base_url | |
| self.site_url = site_url | |
| self.app_name = app_name | |
| self.store = store | |
| self.response_callback = response_callback | |
| class LLMBase: | |
| def __init__(self, config=None): | |
| self.config = config or BaseLlmConfig() | |
| if not hasattr(self.config, "model"): | |
| raise ValueError("Configuration must have a 'model' attribute") | |
| def _get_supported_params(self, **kwargs): | |
| if self.config.is_reasoning_model: | |
| return { | |
| name: kwargs[name] | |
| for name in ("messages", "response_format", "tools", "tool_choice") | |
| if name in kwargs | |
| } | |
| params = { | |
| "temperature": self.config.temperature, | |
| "top_p": self.config.top_p, | |
| "max_tokens": self.config.max_tokens, | |
| } | |
| params.update(kwargs) | |
| return params | |
| class OpenAILLM(LLMBase): | |
| def _parse_response(response, tools): | |
| if not tools: | |
| return response.choices[0].message.content | |
| parsed = { | |
| "content": response.choices[0].message.content, | |
| "tool_calls": [], | |
| } | |
| for tool_call in response.choices[0].message.tool_calls or []: | |
| parsed["tool_calls"].append( | |
| { | |
| "name": tool_call.function.name, | |
| "arguments": json.loads(tool_call.function.arguments), | |
| } | |
| ) | |
| return parsed | |
| class Factory: | |
| provider_to_class = { | |
| "openai": ("mem0.llms.openai.OpenAILLM", OpenAIConfig), | |
| "ollama": ("mem0.llms.openai.OpenAILLM", BaseLlmConfig), | |
| } | |
| def register_provider(cls, name, class_path, config_class=None): | |
| cls.provider_to_class[name] = ( | |
| class_path, | |
| config_class or BaseLlmConfig, | |
| ) | |
| state.factory_registrations.append((name, class_path, config_class)) | |
| def create(cls, provider_name, config=None, **kwargs): | |
| class_path, config_class = cls.provider_to_class[provider_name] | |
| if config is None: | |
| config = config_class(**kwargs) | |
| elif isinstance(config, dict): | |
| config = config_class(**config) | |
| module_name, class_name = class_path.rsplit(".", 1) | |
| llm_class = getattr(importlib.import_module(module_name), class_name) | |
| return llm_class(config) | |
| class MemoryConfig: | |
| def __init__(self, **config): | |
| llm = config["llm"] | |
| if llm["provider"] not in {"openai", "ollama"}: | |
| raise ValueError( | |
| f"Unsupported LLM provider: {llm['provider']}" | |
| ) | |
| self.llm = SimpleNamespace( | |
| provider=llm["provider"], | |
| config=copy.deepcopy(llm.get("config", {})), | |
| ) | |
| embedder = config["embedder"] | |
| self.embedder = SimpleNamespace( | |
| provider=embedder["provider"], | |
| config=copy.deepcopy(embedder.get("config", {})), | |
| ) | |
| vector_store = config["vector_store"] | |
| self.vector_store = SimpleNamespace( | |
| provider=vector_store["provider"], | |
| config=copy.deepcopy(vector_store.get("config", {})), | |
| ) | |
| self.version = config.get("version", "v1.1") | |
| class Memory: | |
| instances = [] | |
| def __init__(self, config): | |
| self.config = config | |
| self.llm = Factory.create(config.llm.provider, config.llm.config) | |
| self.embedding_model = SimpleNamespace( | |
| provider=config.embedder.provider, | |
| config=config.embedder.config, | |
| ) | |
| self.vector_store = SimpleNamespace( | |
| provider=config.vector_store.provider, | |
| config=config.vector_store.config, | |
| ) | |
| type(self).instances.append(self) | |
| def from_config(cls, config): | |
| # This mirrors mem0 2.0.10: validation rejects the private provider | |
| # before the factory gets a chance to resolve its registration. | |
| state.from_config_calls += 1 | |
| return cls(MemoryConfig(**config)) | |
| class FakeOpenAI: | |
| def __init__(self, *, api_key, base_url): | |
| self.api_key = api_key | |
| self.base_url = base_url | |
| state.clients.append(self) | |
| self.chat = SimpleNamespace( | |
| completions=SimpleNamespace(create=self._create) | |
| ) | |
| def _create(self, **params): | |
| state.requests.append(params) | |
| return SimpleNamespace( | |
| choices=[ | |
| SimpleNamespace( | |
| message=SimpleNamespace( | |
| content="direct answer", | |
| tool_calls=[ | |
| SimpleNamespace( | |
| function=SimpleNamespace( | |
| name="remember", | |
| arguments='{"fact": "tea"}', | |
| ) | |
| ) | |
| ], | |
| ) | |
| ) | |
| ] | |
| ) | |
| package_names = { | |
| "mem0": types.ModuleType("mem0"), | |
| "mem0.configs": types.ModuleType("mem0.configs"), | |
| "mem0.configs.llms": types.ModuleType("mem0.configs.llms"), | |
| "mem0.llms": types.ModuleType("mem0.llms"), | |
| "mem0.utils": types.ModuleType("mem0.utils"), | |
| "mem0.configs.base": types.ModuleType("mem0.configs.base"), | |
| "mem0.configs.llms.base": types.ModuleType("mem0.configs.llms.base"), | |
| "mem0.configs.llms.openai": types.ModuleType("mem0.configs.llms.openai"), | |
| "mem0.llms.base": types.ModuleType("mem0.llms.base"), | |
| "mem0.llms.openai": types.ModuleType("mem0.llms.openai"), | |
| "mem0.utils.factory": types.ModuleType("mem0.utils.factory"), | |
| "openai": types.ModuleType("openai"), | |
| } | |
| setattr(package_names["mem0"], "Memory", Memory) | |
| setattr(package_names["mem0.configs.base"], "MemoryConfig", MemoryConfig) | |
| setattr(package_names["mem0.configs.llms.base"], "BaseLlmConfig", BaseLlmConfig) | |
| setattr(package_names["mem0.configs.llms.openai"], "OpenAIConfig", OpenAIConfig) | |
| setattr(package_names["mem0.llms.base"], "LLMBase", LLMBase) | |
| setattr(package_names["mem0.llms.openai"], "OpenAILLM", OpenAILLM) | |
| setattr(package_names["mem0.utils.factory"], "LlmFactory", Factory) | |
| setattr(package_names["openai"], "OpenAI", FakeOpenAI) | |
| for name, module in package_names.items(): | |
| if name in {"mem0", "mem0.configs", "mem0.configs.llms", "mem0.llms", "mem0.utils"}: | |
| module.__path__ = [] | |
| monkeypatch.setitem(sys.modules, name, module) | |
| # The class-path registration imports this module after the fake mem0 | |
| # surface is installed, so it binds to the test doubles above. | |
| monkeypatch.delitem( | |
| sys.modules, "plugins.memory.mem0._openai_llm", raising=False | |
| ) | |
| return state, Memory, Factory | |
| class TestOSSBackend: | |
| def _make(self): | |
| memory = FakeOSSMemory() | |
| backend = OSSBackend.__new__(OSSBackend) | |
| backend._memory = memory | |
| return backend, memory | |
| def test_legacy_api_base_aliases_are_normalized_before_mem0_init(self, monkeypatch): | |
| state, Memory, factory = _install_fake_mem0(monkeypatch) | |
| raw = { | |
| "llm": { | |
| "provider": "openai", | |
| "config": { | |
| "model": "gpt-5-mini", | |
| "api_key": "openai-sentinel", | |
| "api_base": "https://llm.example/v1", | |
| }, | |
| }, | |
| "embedder": { | |
| "provider": "ollama", | |
| "config": {"model": "nomic-embed-text", "api_base": "http://ollama:11434"}, | |
| }, | |
| "vector_store": {"provider": "qdrant", "config": {}}, | |
| } | |
| before = copy.deepcopy(raw) | |
| environment = dict(os.environ) | |
| OSSBackend(raw) | |
| assert len(Memory.instances) == 1 | |
| captured = Memory.instances[0].config | |
| assert captured.llm.provider == "hermes_openai" | |
| assert captured.llm.config["openai_base_url"] == "https://llm.example/v1" | |
| assert captured.embedder.provider == "ollama" | |
| assert captured.embedder.config["ollama_base_url"] == "http://ollama:11434" | |
| assert "api_base" not in captured.llm.config | |
| assert "api_base" not in captured.embedder.config | |
| assert factory.provider_to_class["hermes_openai"][1].__name__ == "OpenAIConfig" | |
| assert len(state.factory_registrations) == 1 | |
| assert state.from_config_calls == 0 | |
| assert raw == before | |
| assert dict(os.environ) == environment | |
| def test_direct_openai_uses_openai_credentials_and_request_shape(self, monkeypatch): | |
| state, _, factory = _install_fake_mem0(monkeypatch) | |
| monkeypatch.setenv("OPENROUTER_API_KEY", "router-sentinel") | |
| monkeypatch.setenv("OPENAI_API_KEY", "env-openai-sentinel") | |
| module = importlib.import_module("plugins.memory.mem0._openai_llm") | |
| callback_calls = [] | |
| config = factory.provider_to_class["openai"][1]( | |
| model="gpt-5-mini", | |
| api_key="configured-openai-sentinel", | |
| openai_base_url="https://openai.example/v1", | |
| models=["router-model"], | |
| route="lowest-latency", | |
| site_url="https://hermes.example", | |
| app_name="Hermes", | |
| store=True, | |
| response_callback=lambda *args: callback_calls.append(args), | |
| ) | |
| adapter = module.DirectOpenAILLM(config) | |
| assert adapter.config.is_reasoning_model is True | |
| tools = [ | |
| { | |
| "type": "function", | |
| "function": {"name": "remember", "parameters": {}}, | |
| } | |
| ] | |
| result = adapter.generate_response( | |
| [{"role": "user", "content": "remember tea"}], | |
| response_format={"type": "json_object"}, | |
| tools=tools, | |
| tool_choice="required", | |
| ) | |
| assert len(state.clients) == 1 | |
| client = state.clients[0] | |
| assert client.api_key == "configured-openai-sentinel" | |
| assert client.base_url == "https://openai.example/v1" | |
| request = state.requests[0] | |
| assert request["model"] == "gpt-5-mini" | |
| assert request["tools"] == tools | |
| assert request["tool_choice"] == "required" | |
| assert request["response_format"] == {"type": "json_object"} | |
| assert request["store"] is True | |
| assert "models" not in request | |
| assert "route" not in request | |
| assert "extra_headers" not in request | |
| assert "temperature" not in request | |
| assert "top_p" not in request | |
| assert "max_tokens" not in request | |
| assert result == { | |
| "content": "direct answer", | |
| "tool_calls": [{"name": "remember", "arguments": {"fact": "tea"}}], | |
| } | |
| assert len(callback_calls) == 1 | |
| assert callback_calls[0][0] is adapter | |
| assert callback_calls[0][2] == request | |
| def test_direct_openai_preserves_explicit_non_reasoning_override(self, monkeypatch): | |
| state, _, factory = _install_fake_mem0(monkeypatch) | |
| config = factory.provider_to_class["openai"][1]( | |
| model="gpt-5-mini", | |
| api_key="configured-openai-sentinel", | |
| is_reasoning_model=False, | |
| ) | |
| module = importlib.import_module("plugins.memory.mem0._openai_llm") | |
| adapter = module.DirectOpenAILLM(config) | |
| adapter.generate_response([{"role": "user", "content": "remember tea"}]) | |
| assert adapter.config.is_reasoning_model is False | |
| request = state.requests[0] | |
| assert request["temperature"] == 0.1 | |
| assert request["top_p"] == 0.1 | |
| assert request["max_tokens"] == 2000 | |
| def test_direct_openai_defaults_missing_model_to_reasoning_safe_mini(self, monkeypatch): | |
| monkeypatch.setenv("OPENAI_API_KEY", "environment-openai-sentinel") | |
| _install_fake_mem0(monkeypatch) | |
| module = importlib.import_module("plugins.memory.mem0._openai_llm") | |
| adapter = module.DirectOpenAILLM() | |
| assert adapter.config.model == "gpt-5-mini" | |
| assert adapter.config.is_reasoning_model is True | |
| def test_direct_openai_uses_openai_environment_when_config_omits_values(self, monkeypatch): | |
| state, _, factory = _install_fake_mem0(monkeypatch) | |
| monkeypatch.setenv("OPENROUTER_API_KEY", "router-sentinel") | |
| monkeypatch.setenv("OPENAI_API_KEY", "env-openai-sentinel") | |
| monkeypatch.setenv("OPENAI_BASE_URL", "https://env-openai.example/v1") | |
| module = importlib.import_module("plugins.memory.mem0._openai_llm") | |
| config = factory.provider_to_class["openai"][1](model="gpt-5-mini") | |
| adapter = module.DirectOpenAILLM(config) | |
| assert len(state.clients) == 1 | |
| assert state.clients[0].api_key == "env-openai-sentinel" | |
| assert state.clients[0].base_url == "https://env-openai.example/v1" | |
| def test_missing_openai_key_fails_before_client_and_hides_router_secret(self, monkeypatch): | |
| state, _, factory = _install_fake_mem0(monkeypatch) | |
| router_secret = "router-secret-sentinel" | |
| monkeypatch.setenv("OPENROUTER_API_KEY", router_secret) | |
| monkeypatch.delenv("OPENAI_API_KEY", raising=False) | |
| module = importlib.import_module("plugins.memory.mem0._openai_llm") | |
| config = factory.provider_to_class["openai"][1]( | |
| model="gpt-5-mini", | |
| api_key=None, | |
| ) | |
| with pytest.raises(ValueError) as exc_info: | |
| module.DirectOpenAILLM(config) | |
| assert "OpenAI API key" in str(exc_info.value) | |
| assert router_secret not in str(exc_info.value) | |
| assert state.clients == [] | |
| assert state.requests == [] | |
| def test_registration_is_idempotent_and_clients_keep_instance_config(self, monkeypatch): | |
| state, Memory, factory = _install_fake_mem0(monkeypatch) | |
| first = { | |
| "llm": { | |
| "provider": "openai", | |
| "config": { | |
| "model": "gpt-5-mini", | |
| "api_key": "first-openai-sentinel", | |
| "openai_base_url": "https://first.example/v1", | |
| }, | |
| }, | |
| "embedder": {"provider": "ollama", "config": {}}, | |
| "vector_store": {"provider": "qdrant", "config": {}}, | |
| } | |
| second = { | |
| "llm": { | |
| "provider": "openai", | |
| "config": { | |
| "model": "gpt-5-mini", | |
| "api_key": "second-openai-sentinel", | |
| "openai_base_url": "https://second.example/v1", | |
| }, | |
| }, | |
| "embedder": {"provider": "ollama", "config": {}}, | |
| "vector_store": {"provider": "qdrant", "config": {}}, | |
| } | |
| first_before = copy.deepcopy(first) | |
| second_before = copy.deepcopy(second) | |
| OSSBackend(first) | |
| OSSBackend(second) | |
| assert len(state.factory_registrations) == 1 | |
| assert factory.provider_to_class["hermes_openai"][0].endswith( | |
| "_openai_llm.DirectOpenAILLM" | |
| ) | |
| assert [ | |
| (client.api_key, client.base_url) for client in state.clients | |
| ] == [ | |
| ("first-openai-sentinel", "https://first.example/v1"), | |
| ("second-openai-sentinel", "https://second.example/v1"), | |
| ] | |
| assert len(Memory.instances) == 2 | |
| assert state.from_config_calls == 0 | |
| assert first == first_before | |
| assert second == second_before | |
| def test_ollama_bypasses_direct_openai_adapter(self, monkeypatch): | |
| state, Memory, factory = _install_fake_mem0(monkeypatch) | |
| raw = { | |
| "llm": { | |
| "provider": "ollama", | |
| "config": { | |
| "model": "llama3.1:8b", | |
| "api_base": "http://ollama:11434", | |
| }, | |
| }, | |
| "embedder": { | |
| "provider": "ollama", | |
| "config": { | |
| "model": "nomic-embed-text", | |
| "api_base": "http://ollama:11434", | |
| }, | |
| }, | |
| "vector_store": {"provider": "qdrant", "config": {}}, | |
| } | |
| before = copy.deepcopy(raw) | |
| OSSBackend(raw) | |
| assert len(Memory.instances) == 1 | |
| assert state.from_config_calls == 1 | |
| assert Memory.instances[0].config.llm.provider == "ollama" | |
| assert Memory.instances[0].config.embedder.provider == "ollama" | |
| assert "hermes_openai" not in factory.provider_to_class | |
| assert state.clients == [] | |
| assert raw == before | |
| httpx = pytest.importorskip("httpx") | |
| class _StubServer: | |
| """Records requests and serves the real self-hosted server's response shapes.""" | |
| def __init__(self, rows=10): | |
| self.requests = [] | |
| self._rows = [{"id": f"m{i}", "memory": f"f{i}"} for i in range(rows)] | |
| def handler(self, request): | |
| self.requests.append(request) | |
| path, method = request.url.path, request.method | |
| if path == "/search" and method == "POST": | |
| return httpx.Response(200, json={"results": [{"id": "m1", "memory": "tea", "score": 0.9}]}) | |
| if path == "/memories" and method == "GET": | |
| top_k = int(request.url.params.get("top_k", len(self._rows))) | |
| return httpx.Response(200, json={"results": self._rows[:top_k]}) | |
| if path == "/memories" and method == "POST": | |
| return httpx.Response(200, json={"results": [{"id": "new", "memory": "stored", "event": "ADD"}]}) | |
| if path.startswith("/memories/") and method in ("PUT", "DELETE"): | |
| if path.endswith("/missing"): # server 404s unknown ids | |
| return httpx.Response(404, json={"detail": "Memory not found"}) | |
| verb = "updated" if method == "PUT" else "Memory deleted successfully" | |
| return httpx.Response(200, json={"message": verb}) | |
| return httpx.Response(404, json={"detail": "not found"}) | |
| def _backend(server, api_key="adminkey", host="http://sh:8888"): | |
| """Build a SelfHostedBackend routed through the stub transport. | |
| Uses the real __init__ (via the injectable ``transport`` kwarg) so the | |
| constructor's header/base_url setup is exercised by every test here. | |
| """ | |
| return SelfHostedBackend( | |
| api_key, host, transport=httpx.MockTransport(server.handler) | |
| ) | |
| class TestSelfHostedBackend: | |
| # --- constructor / auth setup (the crux of the bug) ------------------- | |
| def test_init_uses_x_api_key_not_token_auth(self): | |
| b = SelfHostedBackend("adminkey", "http://sh:8888") | |
| assert b._client.headers["x-api-key"] == "adminkey" | |
| assert "authorization" not in b._client.headers # NOT the cloud 'Token' scheme | |
| # --- search ---------------------------------------------------------- | |
| # --- add / update / delete ------------------------------------------ | |
| # --- error propagation (feeds the plugin's circuit breaker) ---------- | |
| def test_http_error_raises(self): | |
| s = _StubServer() | |
| with pytest.raises(httpx.HTTPStatusError): | |
| _backend(s).delete("missing") # 404 -> raise_for_status; 'not found' won't trip breaker | |