fix(cli): limit team workspace guard to bisync

Signed-off-by: Drew Cain <groksrc@gmail.com>
This commit is contained in:
Drew Cain
2026-05-27 19:12:55 -05:00
committed by Paul Hernandez
parent d0ae373f45
commit 8bf7bdbc0d
2 changed files with 60 additions and 20 deletions
@@ -33,12 +33,9 @@ from basic_memory.utils import generate_permalink, normalize_project_path
console = Console()
TEAM_WORKSPACE_SYNC_UNSUPPORTED = (
"Mirror-style rclone sync/bisync is supported only for Personal workspaces.\n"
"Team workspaces should not use local mirror workflows because they can "
"overwrite or delete shared cloud files.\n"
"Use cloud API/MCP routing for Team workspace edits, or inspect projects with "
"`bm project list --workspace <workspace>`."
TEAM_WORKSPACE_BISYNC_UNSUPPORTED = (
"The bisync operation is only supported on Personal workspaces.\n"
"Use `bm cloud sync --name {name}` instead."
)
@@ -96,7 +93,7 @@ async def _get_workspace_for_project(name: str, config: BasicMemoryConfig) -> Wo
def _require_personal_workspace(name: str, config: BasicMemoryConfig) -> WorkspaceInfo:
"""Exit before rclone work when the target workspace is not personal."""
"""Exit before bisync work when the target workspace is not personal."""
try:
workspace = run_with_cleanup(_get_workspace_for_project(name, config))
except Exception as exc:
@@ -104,7 +101,7 @@ def _require_personal_workspace(name: str, config: BasicMemoryConfig) -> Workspa
raise typer.Exit(1)
if workspace.workspace_type != "personal":
console.print(f"[red]{TEAM_WORKSPACE_SYNC_UNSUPPORTED}[/red]")
console.print(f"[red]{TEAM_WORKSPACE_BISYNC_UNSUPPORTED.format(name=name)}[/red]")
raise typer.Exit(1)
return workspace
@@ -161,7 +158,6 @@ def sync_project_command(
"""
config = ConfigManager().config
_require_cloud_credentials(config)
_require_personal_workspace(name, config)
try:
# Get tenant info for bucket name
@@ -270,7 +266,6 @@ def check_project_command(
"""
config = ConfigManager().config
_require_cloud_credentials(config)
_require_personal_workspace(name, config)
try:
# Get tenant info for bucket name
@@ -353,7 +348,6 @@ def setup_project_sync(
config_manager = ConfigManager()
config = config_manager.config
_require_cloud_credentials(config)
_require_personal_workspace(name, config)
async def _verify_project_exists():
"""Verify the project exists on cloud by listing all projects."""
@@ -402,8 +396,8 @@ def setup_project_sync(
console.print(f"[green]Sync configured for project '{name}'[/green]")
console.print(f"\nLocal sync path: {resolved_path}")
console.print("\nNext steps:")
console.print(f" 1. Preview: bm cloud bisync --name {name} --resync --dry-run")
console.print(f" 2. Sync: bm cloud bisync --name {name} --resync")
console.print(f" 1. Preview: bm cloud sync --name {name} --dry-run")
console.print(f" 2. Sync: bm cloud sync --name {name}")
except Exception as e:
console.print(f"[red]Error configuring sync: {str(e)}[/red]")
raise typer.Exit(1)
+53 -7
View File
@@ -99,15 +99,12 @@ def test_cloud_bisync_fails_fast_when_sync_entry_disappears(monkeypatch, config_
@pytest.mark.parametrize(
"argv",
[
["cloud", "sync", "--name", "research"],
["cloud", "bisync", "--name", "research"],
["cloud", "check", "--name", "research"],
["cloud", "bisync-reset", "research"],
["cloud", "sync-setup", "research", "/tmp/research"],
],
)
def test_cloud_sync_commands_block_organization_workspace(monkeypatch, argv, config_manager):
"""Rclone sync commands should fail before setup/execution for Team workspaces."""
def test_cloud_bisync_commands_block_organization_workspace(monkeypatch, argv, config_manager):
"""Bisync commands should fail before setup/execution for Team workspaces."""
project_sync_command = importlib.import_module("basic_memory.cli.commands.cloud.project_sync")
config = config_manager.load_config()
@@ -130,13 +127,62 @@ def test_cloud_sync_commands_block_organization_workspace(monkeypatch, argv, con
"get_mount_info",
lambda: pytest.fail("workspace guard should run before mount lookup"),
)
monkeypatch.setattr(
project_sync_command,
"get_project_bisync_state",
lambda _name: pytest.fail("workspace guard should run before bisync state lookup"),
)
result = runner.invoke(app, argv)
assert result.exit_code == 1, result.output
output = " ".join(result.output.split())
assert "Mirror-style rclone sync/bisync is supported only for Personal workspaces" in output
assert "overwrite or delete shared cloud files" in output
assert "The bisync operation is only supported on Personal workspaces" in output
assert "Use `bm cloud sync --name research` instead" in output
def test_cloud_sync_allows_organization_workspace(monkeypatch, config_manager):
"""Team workspaces can still use the one-way sync command."""
project_sync_command = importlib.import_module("basic_memory.cli.commands.cloud.project_sync")
config = config_manager.load_config()
config.cloud_api_key = "bmc_test"
config.projects["research"] = ProjectEntry(
path="/tmp/research",
mode=ProjectMode.CLOUD,
workspace_id="team-tenant",
local_sync_path="/tmp/research",
)
config_manager.save_config(config)
monkeypatch.setattr(
project_sync_command,
"get_available_workspaces",
lambda: pytest.fail("sync should not require a personal workspace"),
)
monkeypatch.setattr(
project_sync_command,
"get_mount_info",
lambda: _async_value(SimpleNamespace(bucket_name="tenant-bucket")),
)
monkeypatch.setattr(
project_sync_command,
"_get_cloud_project",
lambda _name: _async_value(
SimpleNamespace(name="research", external_id="external-project-id", path="research")
),
)
monkeypatch.setattr(
project_sync_command,
"_get_sync_project",
lambda _name, _config, _project_data: (SimpleNamespace(name="research"), "/tmp/research"),
)
monkeypatch.setattr(project_sync_command, "project_sync", lambda *args, **kwargs: True)
result = runner.invoke(app, ["cloud", "sync", "--name", "research"])
assert result.exit_code == 0, result.output
assert "research synced successfully" in result.output
def test_require_personal_workspace_allows_personal_workspace(monkeypatch, config_manager):