"""Tests for the project CLI commands.""" import os from unittest.mock import patch, MagicMock from typer.testing import CliRunner from basic_memory.cli.main import app as cli_app @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_list_command(mock_run, cli_env): """Test the 'project list' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "projects": [{"name": "test", "path": "/path/to/test", "is_default": True}], "default_project": "test", "current_project": "test", } mock_run.return_value = mock_response runner = CliRunner() result = runner.invoke(cli_app, ["project", "list"]) # Just verify it runs without exception assert result.exit_code == 0 @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_add_command(mock_run, cli_env): """Test the 'project add' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Project 'test-project' added successfully", "status": "success", "default": False, } mock_run.return_value = mock_response runner = CliRunner() result = runner.invoke(cli_app, ["project", "add", "test-project", "/path/to/project"]) # Just verify it runs without exception assert result.exit_code == 0 @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_remove_command(mock_run, cli_env): """Test the 'project remove' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Project 'test-project' removed successfully", "status": "success", "default": False, } mock_run.return_value = mock_response runner = CliRunner() result = runner.invoke(cli_app, ["project", "remove", "test-project"]) # Just verify it runs without exception assert result.exit_code == 0 @patch("basic_memory.cli.commands.project.asyncio.run") @patch("importlib.reload") def test_project_default_command(mock_reload, mock_run, cli_env): """Test the 'project default' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Project 'test-project' set as default successfully", "status": "success", "default": True, } mock_run.return_value = mock_response # Mock necessary config methods to have the test-project handled # Patching call_put directly since it's imported at the module level # Patch the os.environ for checking # On Windows, preserve USERPROFILE to allow home directory detection env_vars = {} if os.name == "nt" and "USERPROFILE" in os.environ: env_vars["USERPROFILE"] = os.environ["USERPROFILE"] with patch.dict(os.environ, env_vars, clear=True): # Patch ConfigManager.set_default_project to prevent validation error with patch("basic_memory.config.ConfigManager.set_default_project"): runner = CliRunner() result = runner.invoke(cli_app, ["project", "default", "test-project"]) # Just verify it runs without exception and environment is set assert result.exit_code == 0 @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_sync_command(mock_run, cli_env): """Test the 'project sync' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Projects synchronized successfully between configuration and database", "status": "success", "default": False, } mock_run.return_value = mock_response runner = CliRunner() result = runner.invoke(cli_app, ["project", "sync-config"]) # Just verify it runs without exception assert result.exit_code == 0 @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_failure_exits_with_error(mock_run, cli_env): """Test that CLI commands properly exit with error code on API failures.""" # Mock an exception being raised mock_run.side_effect = Exception("API server not running") runner = CliRunner() # Test various commands for proper error handling list_result = runner.invoke(cli_app, ["project", "list"]) add_result = runner.invoke(cli_app, ["project", "add", "test-project", "/path/to/project"]) remove_result = runner.invoke(cli_app, ["project", "remove", "test-project"]) default_result = runner.invoke(cli_app, ["project", "default", "test-project"]) # All should exit with code 1 and show error message assert list_result.exit_code == 1 assert "Error listing projects" in list_result.output assert add_result.exit_code == 1 assert "Error adding project" in add_result.output assert remove_result.exit_code == 1 assert "Error removing project" in remove_result.output assert default_result.exit_code == 1 assert "Error setting default project" in default_result.output @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_move_command(mock_run, cli_env): """Test the 'project move' command with mocked API.""" # Mock the API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Project 'test-project' updated successfully", "status": "success", "default": False, } mock_run.return_value = mock_response runner = CliRunner() result = runner.invoke(cli_app, ["project", "move", "test-project", "/new/path/to/project"]) # Verify it runs without exception assert result.exit_code == 0 # Verify the important warning message is displayed assert "Manual File Movement Required" in result.output assert "You must manually move your project files" in result.output assert "/new/path/to/project" in result.output @patch("basic_memory.cli.commands.project.asyncio.run") def test_project_move_command_failure(mock_run, cli_env): """Test the 'project move' command with API failure.""" # Mock an exception being raised mock_run.side_effect = Exception("Project not found") runner = CliRunner() result = runner.invoke(cli_app, ["project", "move", "nonexistent-project", "/new/path"]) # Should exit with code 1 and show error message assert result.exit_code == 1 assert "Error moving project" in result.output @patch("basic_memory.cli.commands.project.call_patch") def test_project_move_command_uses_permalink(mock_call_patch, cli_env): """Test that the 'project move' command correctly generates and uses permalink in API call.""" # Mock successful API response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "message": "Project 'Test Project Name' updated successfully", "status": "success", "default": False, } mock_call_patch.return_value = mock_response runner = CliRunner() # Test with a project name that needs normalization (spaces, mixed case) project_name = "Test Project Name" new_path = os.path.join("new", "path", "to", "project") result = runner.invoke(cli_app, ["project", "move", project_name, new_path]) # Verify command executed successfully assert result.exit_code == 0 # Verify call_patch was called with the correct permalink-formatted project name mock_call_patch.assert_called_once() args, kwargs = mock_call_patch.call_args # Check the API endpoint uses the original name and normalized permalink # The actual implementation uses f"/{name}/project/{project_permalink}" expected_endpoint = "/Test Project Name/project/test-project-name" assert args[1] == expected_endpoint # Second argument is the endpoint URL # Verify the data contains the resolved path (using same normalization as the function) from pathlib import Path expected_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix() expected_data = {"path": expected_path} assert kwargs["json"] == expected_data