Skip to content

Commit 0496948

Browse files
groksrcclaude
andcommitted
fix: address PR review — fix broken test and add workspace coverage
- Fix fake_project_exists and fake_get_client signatures in upload routing test - Add config_manager fixture to upload routing test - Add tests for get_cloud_control_plane_client with workspace header - Add tests for get_client auto-resolving workspace from project config and default_workspace - Add tests for explicit workspace overriding config - Add tests for _workspace_headers helper and cloud_utils workspace passing - Add missing docstring for workspace param in fetch_cloud_projects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
1 parent e3323fd commit 0496948

4 files changed

Lines changed: 140 additions & 3 deletions

File tree

src/basic_memory/cli/commands/cloud/cloud_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ async def fetch_cloud_projects(
3030
) -> CloudProjectList:
3131
"""Fetch list of projects from cloud API.
3232
33+
Args:
34+
workspace: Cloud workspace tenant_id to list projects from
35+
3336
Returns:
3437
CloudProjectList with projects from cloud
3538
"""

tests/cli/cloud/test_cloud_api_client_and_utils.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
make_api_request,
1111
)
1212
from basic_memory.cli.commands.cloud.cloud_utils import (
13+
_workspace_headers,
1314
create_cloud_project,
1415
fetch_cloud_projects,
1516
project_exists,
@@ -165,6 +166,81 @@ async def api_request(**kwargs):
165166
assert seen["create_payload"]["path"] == "my-project"
166167

167168

169+
def test_workspace_headers_with_workspace():
170+
"""_workspace_headers returns X-Workspace-ID when workspace is provided."""
171+
assert _workspace_headers("tenant-123") == {"X-Workspace-ID": "tenant-123"}
172+
173+
174+
def test_workspace_headers_without_workspace():
175+
"""_workspace_headers returns empty dict when no workspace."""
176+
assert _workspace_headers(None) == {}
177+
assert _workspace_headers() == {}
178+
179+
180+
@pytest.mark.asyncio
181+
async def test_cloud_utils_pass_workspace_header(config_home, config_manager):
182+
"""fetch_cloud_projects, project_exists, and create_cloud_project pass workspace header."""
183+
config = config_manager.load_config()
184+
config.cloud_host = "https://cloud.example.test"
185+
config_manager.save_config(config)
186+
187+
auth = CLIAuth(client_id="cid", authkit_domain="https://auth.example.test")
188+
auth.token_file.parent.mkdir(parents=True, exist_ok=True)
189+
auth.token_file.write_text(
190+
'{"access_token":"token-123","refresh_token":null,"expires_at":9999999999,"token_type":"Bearer"}',
191+
encoding="utf-8",
192+
)
193+
194+
seen_headers: list[dict] = []
195+
196+
async def handler(request: httpx.Request) -> httpx.Response:
197+
seen_headers.append(dict(request.headers))
198+
if request.method == "GET":
199+
return httpx.Response(200, json={"projects": []})
200+
if request.method == "POST":
201+
payload = json.loads(request.content.decode("utf-8"))
202+
return httpx.Response(
203+
200,
204+
json={
205+
"message": "created",
206+
"status": "success",
207+
"default": False,
208+
"old_project": None,
209+
"new_project": {"name": payload["name"], "path": payload["path"]},
210+
},
211+
)
212+
raise AssertionError(f"Unexpected: {request.method}")
213+
214+
transport = httpx.MockTransport(handler)
215+
216+
@asynccontextmanager
217+
async def http_client_factory():
218+
async with httpx.AsyncClient(
219+
transport=transport, base_url="https://cloud.example.test"
220+
) as client:
221+
yield client
222+
223+
async def api_request(**kwargs):
224+
return await make_api_request(auth=auth, http_client_factory=http_client_factory, **kwargs)
225+
226+
# fetch with workspace
227+
await fetch_cloud_projects(workspace="tenant-abc", api_request=api_request)
228+
assert seen_headers[-1].get("x-workspace-id") == "tenant-abc"
229+
230+
# project_exists with workspace
231+
await project_exists("test", workspace="tenant-abc", api_request=api_request)
232+
assert seen_headers[-1].get("x-workspace-id") == "tenant-abc"
233+
234+
# create with workspace
235+
await create_cloud_project("new-proj", workspace="tenant-abc", api_request=api_request)
236+
assert seen_headers[-1].get("x-workspace-id") == "tenant-abc"
237+
238+
# Without workspace — header should not be present
239+
seen_headers.clear()
240+
await fetch_cloud_projects(api_request=api_request)
241+
assert "x-workspace-id" not in seen_headers[-1]
242+
243+
168244
@pytest.mark.asyncio
169245
async def test_make_api_request_prefers_api_key_over_oauth(config_home, config_manager):
170246
"""API key in config should be used without needing an OAuth token on disk."""

tests/cli/cloud/test_upload_command_routing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
runner = CliRunner()
1111

1212

13-
def test_cloud_upload_uses_control_plane_client(monkeypatch, tmp_path):
13+
def test_cloud_upload_uses_control_plane_client(monkeypatch, tmp_path, config_manager):
1414
"""Upload command should use control-plane cloud client for WebDAV PUT operations."""
1515
import basic_memory.cli.commands.cloud.upload_command as upload_command
1616

@@ -20,11 +20,11 @@ def test_cloud_upload_uses_control_plane_client(monkeypatch, tmp_path):
2020

2121
seen: dict[str, str] = {}
2222

23-
async def fake_project_exists(_project_name: str) -> bool:
23+
async def fake_project_exists(_project_name: str, workspace: str | None = None) -> bool:
2424
return True
2525

2626
@asynccontextmanager
27-
async def fake_get_client():
27+
async def fake_get_client(workspace=None):
2828
async with httpx.AsyncClient(base_url="https://cloud.example.test") as client:
2929
yield client
3030

tests/mcp/test_async_client_modes.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,64 @@ async def test_get_cloud_control_plane_client_uses_oauth_token(config_manager):
274274
assert client.headers.get("Authorization") == "Bearer oauth-control-123"
275275

276276

277+
@pytest.mark.asyncio
278+
async def test_get_cloud_control_plane_client_with_workspace(config_manager):
279+
"""Control plane client passes X-Workspace-ID header when workspace is provided."""
280+
cfg = config_manager.load_config()
281+
cfg.cloud_host = "https://cloud.example.test"
282+
cfg.cloud_api_key = "bmc_test_key_123"
283+
config_manager.save_config(cfg)
284+
285+
async with get_cloud_control_plane_client(workspace="tenant-abc") as client:
286+
assert client.headers.get("X-Workspace-ID") == "tenant-abc"
287+
288+
# Without workspace, header should not be present
289+
async with get_cloud_control_plane_client() as client:
290+
assert "X-Workspace-ID" not in client.headers
291+
292+
293+
@pytest.mark.asyncio
294+
async def test_get_client_auto_resolves_workspace_from_project_config(config_manager):
295+
"""get_client resolves workspace from project entry when not explicitly passed."""
296+
cfg = config_manager.load_config()
297+
cfg.cloud_host = "https://cloud.example.test"
298+
cfg.cloud_api_key = "bmc_test_key_123"
299+
cfg.set_project_mode("research", ProjectMode.CLOUD)
300+
cfg.projects["research"].workspace_id = "tenant-from-config"
301+
config_manager.save_config(cfg)
302+
303+
async with get_client(project_name="research") as client:
304+
assert client.headers.get("X-Workspace-ID") == "tenant-from-config"
305+
306+
307+
@pytest.mark.asyncio
308+
async def test_get_client_auto_resolves_workspace_from_default(config_manager):
309+
"""get_client falls back to default_workspace when project has no workspace_id."""
310+
cfg = config_manager.load_config()
311+
cfg.cloud_host = "https://cloud.example.test"
312+
cfg.cloud_api_key = "bmc_test_key_123"
313+
cfg.set_project_mode("research", ProjectMode.CLOUD)
314+
cfg.default_workspace = "default-tenant-456"
315+
config_manager.save_config(cfg)
316+
317+
async with get_client(project_name="research") as client:
318+
assert client.headers.get("X-Workspace-ID") == "default-tenant-456"
319+
320+
321+
@pytest.mark.asyncio
322+
async def test_get_client_explicit_workspace_overrides_config(config_manager):
323+
"""Explicit workspace param takes priority over project config."""
324+
cfg = config_manager.load_config()
325+
cfg.cloud_host = "https://cloud.example.test"
326+
cfg.cloud_api_key = "bmc_test_key_123"
327+
cfg.set_project_mode("research", ProjectMode.CLOUD)
328+
cfg.projects["research"].workspace_id = "tenant-from-config"
329+
config_manager.save_config(cfg)
330+
331+
async with get_client(project_name="research", workspace="explicit-tenant") as client:
332+
assert client.headers.get("X-Workspace-ID") == "explicit-tenant"
333+
334+
277335
@pytest.mark.asyncio
278336
async def test_get_cloud_control_plane_client_raises_without_credentials(config_manager):
279337
cfg = config_manager.load_config()

0 commit comments

Comments
 (0)