mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
db60942267
Signed-off-by: Drew Cain <groksrc@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""Tests for bm project set-cloud and bm project set-local commands."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from basic_memory.cli.app import app
|
|
|
|
# Importing the commands module registers the project subcommands with the app
|
|
import basic_memory.cli.commands.project # noqa: F401
|
|
|
|
|
|
@pytest.fixture
|
|
def runner():
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config(tmp_path, monkeypatch):
|
|
"""Create a mock config with projects for testing set-cloud/set-local."""
|
|
from basic_memory import config as config_module
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
config_dir = tmp_path / ".basic-memory"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
config_file = config_dir / "config.json"
|
|
|
|
config_data = {
|
|
"env": "dev",
|
|
"projects": {
|
|
"main": {"path": str(tmp_path / "main")},
|
|
"research": {"path": str(tmp_path / "research")},
|
|
},
|
|
"default_project": "main",
|
|
"cloud_api_key": "bmc_test_key_123",
|
|
}
|
|
|
|
config_file.write_text(json.dumps(config_data, indent=2))
|
|
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
|
|
yield config_file
|
|
|
|
|
|
class TestSetCloud:
|
|
"""Tests for bm project set-cloud command."""
|
|
|
|
def test_set_cloud_success(self, runner, mock_config):
|
|
"""Test setting a project to cloud mode."""
|
|
result = runner.invoke(app, ["project", "set-cloud", "research"])
|
|
assert result.exit_code == 0
|
|
assert "cloud mode" in result.stdout.lower()
|
|
|
|
# Verify config was updated
|
|
config_data = json.loads(mock_config.read_text())
|
|
assert config_data["projects"]["research"]["mode"] == "cloud"
|
|
|
|
def test_set_cloud_nonexistent_project(self, runner, mock_config):
|
|
"""Test set-cloud with a project that doesn't exist in config."""
|
|
result = runner.invoke(app, ["project", "set-cloud", "nonexistent"])
|
|
assert result.exit_code == 1
|
|
assert "not found" in result.stdout.lower()
|
|
|
|
def test_set_cloud_no_credentials(self, runner, tmp_path, monkeypatch):
|
|
"""Test set-cloud when neither API key nor OAuth session is available."""
|
|
from basic_memory import config as config_module
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
config_dir = tmp_path / ".basic-memory"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
config_file = config_dir / "config.json"
|
|
|
|
# Config without cloud_api_key
|
|
config_data = {
|
|
"env": "dev",
|
|
"projects": {"research": {"path": str(tmp_path / "research")}},
|
|
"default_project": "research",
|
|
}
|
|
config_file.write_text(json.dumps(config_data, indent=2))
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
|
|
result = runner.invoke(app, ["project", "set-cloud", "research"])
|
|
assert result.exit_code == 1
|
|
assert "no cloud credentials" in result.stdout.lower()
|
|
|
|
def test_set_cloud_with_oauth_session(self, runner, tmp_path, monkeypatch):
|
|
"""Test set-cloud succeeds with OAuth token but no API key."""
|
|
from basic_memory import config as config_module
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
config_dir = tmp_path / ".basic-memory"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
config_file = config_dir / "config.json"
|
|
|
|
# Config without cloud_api_key but with a project
|
|
config_data = {
|
|
"env": "dev",
|
|
"projects": {"research": {"path": str(tmp_path / "research")}},
|
|
"default_project": "research",
|
|
}
|
|
config_file.write_text(json.dumps(config_data, indent=2))
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
|
|
# Write OAuth token file so CLIAuth.load_tokens() returns something
|
|
token_file = config_dir / "basic-memory-cloud.json"
|
|
token_data = {
|
|
"access_token": "oauth-token-789",
|
|
"refresh_token": None,
|
|
"expires_at": 9999999999,
|
|
"token_type": "Bearer",
|
|
}
|
|
token_file.write_text(json.dumps(token_data, indent=2))
|
|
|
|
result = runner.invoke(app, ["project", "set-cloud", "research"])
|
|
assert result.exit_code == 0
|
|
assert "cloud mode" in result.stdout.lower()
|
|
|
|
# Verify config was updated
|
|
config_data = json.loads(config_file.read_text())
|
|
assert config_data["projects"]["research"]["mode"] == "cloud"
|
|
|
|
|
|
class TestSetLocal:
|
|
"""Tests for bm project set-local command."""
|
|
|
|
def test_set_local_success(self, runner, mock_config):
|
|
"""Test reverting a project to local mode."""
|
|
# First set to cloud
|
|
runner.invoke(app, ["project", "set-cloud", "research"])
|
|
config_data = json.loads(mock_config.read_text())
|
|
assert config_data["projects"]["research"]["mode"] == "cloud"
|
|
|
|
# Now set back to local
|
|
result = runner.invoke(app, ["project", "set-local", "research"])
|
|
assert result.exit_code == 0
|
|
assert "local mode" in result.stdout.lower()
|
|
|
|
# Verify config was updated — mode reset to local
|
|
config_data = json.loads(mock_config.read_text())
|
|
assert config_data["projects"]["research"]["mode"] == "local"
|
|
|
|
def test_set_local_nonexistent_project(self, runner, mock_config):
|
|
"""Test set-local with a project that doesn't exist in config."""
|
|
result = runner.invoke(app, ["project", "set-local", "nonexistent"])
|
|
assert result.exit_code == 1
|
|
assert "not found" in result.stdout.lower()
|
|
|
|
def test_set_local_already_local(self, runner, mock_config):
|
|
"""Test set-local on a project that's already local (no-op, should succeed)."""
|
|
result = runner.invoke(app, ["project", "set-local", "main"])
|
|
assert result.exit_code == 0
|
|
assert "local mode" in result.stdout.lower()
|
|
|
|
def test_set_local_clears_workspace_id(self, runner, mock_config):
|
|
"""Test that set-local clears workspace_id from the project entry."""
|
|
from basic_memory import config as config_module
|
|
|
|
# Manually set workspace_id on the project
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
config_data = json.loads(mock_config.read_text())
|
|
config_data["projects"]["research"]["mode"] = "cloud"
|
|
config_data["projects"]["research"]["workspace_id"] = "11111111-1111-1111-1111-111111111111"
|
|
mock_config.write_text(json.dumps(config_data, indent=2))
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
# Set back to local
|
|
result = runner.invoke(app, ["project", "set-local", "research"])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify workspace_id was cleared
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
updated_data = json.loads(mock_config.read_text())
|
|
assert updated_data["projects"]["research"]["workspace_id"] is None
|
|
assert updated_data["projects"]["research"]["mode"] == "local"
|
|
|
|
|
|
class TestSetCloudWithWorkspace:
|
|
"""Tests for 'bm project set-cloud --workspace' option."""
|
|
|
|
def test_set_cloud_with_workspace_stores_workspace_id(self, runner, mock_config, monkeypatch):
|
|
"""Test that --workspace resolves to tenant_id and stores it."""
|
|
from basic_memory import config as config_module
|
|
from basic_memory.schemas.cloud import WorkspaceInfo
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
async def fake_get_available_workspaces():
|
|
return [
|
|
WorkspaceInfo(
|
|
tenant_id="11111111-1111-1111-1111-111111111111",
|
|
workspace_type="personal",
|
|
name="Personal",
|
|
role="owner",
|
|
),
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
"basic_memory.mcp.project_context.get_available_workspaces",
|
|
fake_get_available_workspaces,
|
|
)
|
|
|
|
result = runner.invoke(app, ["project", "set-cloud", "research", "--workspace", "Personal"])
|
|
assert result.exit_code == 0
|
|
assert "cloud mode" in result.stdout.lower()
|
|
assert "11111111-1111-1111-1111-111111111111" in result.stdout
|
|
|
|
# Verify workspace_id was persisted
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
updated_data = json.loads(mock_config.read_text())
|
|
assert (
|
|
updated_data["projects"]["research"]["workspace_id"]
|
|
== "11111111-1111-1111-1111-111111111111"
|
|
)
|
|
|
|
def test_set_cloud_with_workspace_not_found(self, runner, mock_config, monkeypatch):
|
|
"""Test --workspace with unknown workspace name."""
|
|
from basic_memory import config as config_module
|
|
from basic_memory.schemas.cloud import WorkspaceInfo
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
async def fake_get_available_workspaces():
|
|
return [
|
|
WorkspaceInfo(
|
|
tenant_id="11111111-1111-1111-1111-111111111111",
|
|
workspace_type="personal",
|
|
name="Personal",
|
|
role="owner",
|
|
),
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
"basic_memory.mcp.project_context.get_available_workspaces",
|
|
fake_get_available_workspaces,
|
|
)
|
|
|
|
result = runner.invoke(
|
|
app, ["project", "set-cloud", "research", "--workspace", "Nonexistent"]
|
|
)
|
|
assert result.exit_code == 1
|
|
assert "not found" in result.stdout.lower()
|
|
|
|
def test_set_cloud_uses_default_workspace_when_no_flag(self, runner, mock_config, monkeypatch):
|
|
"""Test that set-cloud uses default_workspace when --workspace is not passed."""
|
|
from basic_memory import config as config_module
|
|
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
# Set default_workspace in config
|
|
config_data = json.loads(mock_config.read_text())
|
|
config_data["default_workspace"] = "global-default-tenant-id"
|
|
mock_config.write_text(json.dumps(config_data, indent=2))
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
|
|
result = runner.invoke(app, ["project", "set-cloud", "research"])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify workspace_id was set from default
|
|
config_module._CONFIG_CACHE = None
|
|
config_module._CONFIG_MTIME = None
|
|
config_module._CONFIG_SIZE = None
|
|
updated_data = json.loads(mock_config.read_text())
|
|
assert updated_data["projects"]["research"]["workspace_id"] == "global-default-tenant-id"
|