"""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_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_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_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()