fix: skip workspace resolution when client factory is active (#630)

Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jope-bm
2026-02-27 13:40:04 -07:00
committed by GitHub
parent e97eafa55a
commit 4ea5396ddd
2 changed files with 87 additions and 0 deletions
+13
View File
@@ -419,6 +419,7 @@ async def get_project_client(
_explicit_routing,
_force_local_mode,
get_client,
is_factory_mode,
)
# Step 1: Resolve project name from config (no network call)
@@ -433,6 +434,18 @@ async def get_project_client(
f"Available projects: {project_names}"
)
# Step 1b: Factory injection (in-process cloud server)
# Trigger: set_client_factory() was called (e.g., by cloud MCP server)
# Why: the transport layer already resolved workspace and tenant context;
# attempting cloud workspace resolution here would call the production
# control-plane API with no valid credentials and fail with 401
# Outcome: use the factory client directly, skip workspace resolution
if is_factory_mode():
async with get_client() as client:
active_project = await get_active_project(client, resolved_project, context)
yield client, active_project
return
# Step 2: Check explicit routing BEFORE workspace resolution
# Trigger: CLI passed --local or --cloud
# Why: explicit flags must be deterministic — skip workspace entirely for --local
+74
View File
@@ -475,3 +475,77 @@ class TestGetProjectClientRoutingOrder:
assert "resolve_workspace_parameter should not be called" not in error_msg
# Should not get a local ASGI routing error
assert "no project found" not in error_msg
@pytest.mark.asyncio
async def test_factory_mode_skips_workspace_resolution(self, config_manager, monkeypatch):
"""When a client factory is set (in-process cloud server), skip workspace resolution.
The cloud MCP server calls set_client_factory() so that get_client() routes
requests through TenantASGITransport. In this mode, workspace and tenant context
are already resolved by the transport layer. Attempting cloud workspace resolution
would call the production control-plane API and fail with 401.
"""
from contextlib import asynccontextmanager
from basic_memory.mcp import async_client
from basic_memory.mcp.project_context import get_project_client
from basic_memory.config import ProjectEntry, ProjectMode
config = config_manager.load_config()
config.projects["cloud-proj"] = ProjectEntry(
path=str(config_manager.config_dir.parent / "cloud-proj"),
mode=ProjectMode.CLOUD,
)
config_manager.save_config(config)
# Set up a factory (simulates what cloud MCP server does)
@asynccontextmanager
async def fake_factory():
from httpx import ASGITransport, AsyncClient
from basic_memory.api.app import app as fastapi_app
async with AsyncClient(
transport=ASGITransport(app=fastapi_app),
base_url="http://test",
) as client:
yield client
original_factory = async_client._client_factory
async_client.set_client_factory(fake_factory)
# Patch workspace resolution to fail if called — factory mode should skip it
async def fail_if_called(**kwargs): # pragma: no cover
raise AssertionError(
"resolve_workspace_parameter must not be called in factory mode"
)
monkeypatch.setattr(
"basic_memory.mcp.project_context.resolve_workspace_parameter",
fail_if_called,
)
# Patch get_cloud_control_plane_client to fail if called
@asynccontextmanager
async def fail_control_plane(): # pragma: no cover
raise AssertionError(
"get_cloud_control_plane_client must not be called in factory mode"
)
monkeypatch.setattr(
"basic_memory.mcp.async_client.get_cloud_control_plane_client",
fail_control_plane,
)
try:
# Will fail at project validation (no real project in DB), but proves
# workspace resolution and control-plane calls were skipped
with pytest.raises(Exception) as exc_info:
async with get_project_client(project="cloud-proj"):
pass
error_msg = str(exc_info.value).lower()
assert "resolve_workspace_parameter must not be called" not in error_msg
assert "get_cloud_control_plane_client must not be called" not in error_msg
finally:
# Restore original factory to avoid polluting other tests
async_client._client_factory = original_factory