Skip to content

Commit b3403e9

Browse files
groksrcclaudephernandez
authored
fix: add workspace routing to cloud upload and API client (#704)
Signed-off-by: Drew Cain <groksrc@gmail.com> Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: phernandez <paul@basicmachines.co>
1 parent fe04a0b commit b3403e9

File tree

5 files changed

+67
-1
lines changed

5 files changed

+67
-1
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ async def fetch_cloud_projects(
4141
) -> CloudProjectList:
4242
"""Fetch list of projects from cloud API.
4343
44+
Args:
45+
project_name: Optional project name for workspace resolution
46+
workspace: Cloud workspace tenant_id to list projects from
47+
4448
Returns:
4549
CloudProjectList with projects from cloud
4650
"""

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ async def _upload():
106106
raise typer.Exit(1)
107107

108108
# Perform upload (or dry run)
109+
if resolved_workspace:
110+
console.print(f"[dim]Using workspace: {resolved_workspace}[/dim]")
109111
if dry_run:
110112
console.print(
111113
f"[yellow]DRY RUN: Showing what would be uploaded to '{project}'[/yellow]"

tests/cli/cloud/test_cloud_api_client_and_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ async def api_request(**_kwargs):
270270
await project_exists("alpha", api_request=api_request)
271271

272272

273+
274+
273275
@pytest.mark.asyncio
274276
async def test_make_api_request_prefers_api_key_over_oauth(config_home, config_manager):
275277
"""API key in config should be used without needing an OAuth token on disk."""

tests/cli/cloud/test_upload_command_routing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
runner = CliRunner()
1313

1414

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

tests/mcp/test_async_client_modes.py

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

318318

319+
@pytest.mark.asyncio
320+
async def test_get_cloud_control_plane_client_with_workspace(config_manager):
321+
"""Control plane client passes X-Workspace-ID header when workspace is provided."""
322+
cfg = config_manager.load_config()
323+
cfg.cloud_host = "https://cloud.example.test"
324+
cfg.cloud_api_key = "bmc_test_key_123"
325+
config_manager.save_config(cfg)
326+
327+
async with get_cloud_control_plane_client(workspace="tenant-abc") as client:
328+
assert client.headers.get("X-Workspace-ID") == "tenant-abc"
329+
330+
# Without workspace, header should not be present
331+
async with get_cloud_control_plane_client() as client:
332+
assert "X-Workspace-ID" not in client.headers
333+
334+
335+
@pytest.mark.asyncio
336+
async def test_get_client_auto_resolves_workspace_from_project_config(config_manager):
337+
"""get_client resolves workspace from project entry when not explicitly passed."""
338+
cfg = config_manager.load_config()
339+
cfg.cloud_host = "https://cloud.example.test"
340+
cfg.cloud_api_key = "bmc_test_key_123"
341+
cfg.set_project_mode("research", ProjectMode.CLOUD)
342+
cfg.projects["research"].workspace_id = "tenant-from-config"
343+
config_manager.save_config(cfg)
344+
345+
async with get_client(project_name="research") as client:
346+
assert client.headers.get("X-Workspace-ID") == "tenant-from-config"
347+
348+
349+
@pytest.mark.asyncio
350+
async def test_get_client_auto_resolves_workspace_from_default(config_manager):
351+
"""get_client falls back to default_workspace when project has no workspace_id."""
352+
cfg = config_manager.load_config()
353+
cfg.cloud_host = "https://cloud.example.test"
354+
cfg.cloud_api_key = "bmc_test_key_123"
355+
cfg.set_project_mode("research", ProjectMode.CLOUD)
356+
cfg.default_workspace = "default-tenant-456"
357+
config_manager.save_config(cfg)
358+
359+
async with get_client(project_name="research") as client:
360+
assert client.headers.get("X-Workspace-ID") == "default-tenant-456"
361+
362+
363+
@pytest.mark.asyncio
364+
async def test_get_client_explicit_workspace_overrides_config(config_manager):
365+
"""Explicit workspace param takes priority over project config."""
366+
cfg = config_manager.load_config()
367+
cfg.cloud_host = "https://cloud.example.test"
368+
cfg.cloud_api_key = "bmc_test_key_123"
369+
cfg.set_project_mode("research", ProjectMode.CLOUD)
370+
cfg.projects["research"].workspace_id = "tenant-from-config"
371+
config_manager.save_config(cfg)
372+
373+
async with get_client(project_name="research", workspace="explicit-tenant") as client:
374+
assert client.headers.get("X-Workspace-ID") == "explicit-tenant"
375+
376+
319377
@pytest.mark.asyncio
320378
async def test_get_cloud_control_plane_client_raises_without_credentials(config_manager):
321379
cfg = config_manager.load_config()

0 commit comments

Comments
 (0)