mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
84d2aaf641
Co-authored-by: Claude <noreply@anthropic.com>
363 lines
13 KiB
Python
363 lines
13 KiB
Python
"""Tests for the initialization service."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
|
|
import pytest
|
|
|
|
from basic_memory.services.initialization import (
|
|
ensure_initialization,
|
|
initialize_app,
|
|
initialize_database,
|
|
reconcile_projects_with_config,
|
|
migrate_legacy_projects,
|
|
migrate_legacy_project_data,
|
|
initialize_file_sync,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_initialize_database(mock_get_or_create_db, app_config):
|
|
"""Test initializing the database."""
|
|
mock_get_or_create_db.return_value = (MagicMock(), MagicMock())
|
|
await initialize_database(app_config)
|
|
mock_get_or_create_db.assert_called_once_with(app_config.database_path)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_initialize_database_error(mock_get_or_create_db, app_config):
|
|
"""Test handling errors during database initialization."""
|
|
mock_get_or_create_db.side_effect = Exception("Test error")
|
|
await initialize_database(app_config)
|
|
mock_get_or_create_db.assert_called_once_with(app_config.database_path)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.reconcile_projects_with_config")
|
|
@patch("basic_memory.services.migration_service.migration_manager")
|
|
@patch("basic_memory.services.initialization.initialize_database")
|
|
async def test_initialize_app(
|
|
mock_initialize_database,
|
|
mock_migration_manager,
|
|
mock_reconcile_projects,
|
|
app_config,
|
|
):
|
|
"""Test app initialization."""
|
|
mock_migration_manager.start_background_migration = AsyncMock()
|
|
|
|
result = await initialize_app(app_config)
|
|
|
|
mock_initialize_database.assert_called_once_with(app_config)
|
|
mock_reconcile_projects.assert_called_once_with(app_config)
|
|
mock_migration_manager.start_background_migration.assert_called_once_with(app_config)
|
|
assert result == mock_migration_manager
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.initialize_database")
|
|
@patch("basic_memory.services.initialization.reconcile_projects_with_config")
|
|
@patch("basic_memory.services.migration_service.migration_manager")
|
|
async def test_initialize_app_sync_disabled(
|
|
mock_migration_manager, mock_reconcile_projects, mock_initialize_database, app_config
|
|
):
|
|
"""Test app initialization with sync disabled."""
|
|
app_config.sync_changes = False
|
|
mock_migration_manager.start_background_migration = AsyncMock()
|
|
|
|
result = await initialize_app(app_config)
|
|
|
|
mock_initialize_database.assert_called_once_with(app_config)
|
|
mock_reconcile_projects.assert_called_once_with(app_config)
|
|
mock_migration_manager.start_background_migration.assert_called_once_with(app_config)
|
|
assert result == mock_migration_manager
|
|
|
|
|
|
@patch("basic_memory.services.initialization.asyncio.run")
|
|
def test_ensure_initialization(mock_run, project_config):
|
|
"""Test synchronous initialization wrapper."""
|
|
ensure_initialization(project_config)
|
|
mock_run.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_reconcile_projects_with_config(mock_get_db, app_config):
|
|
"""Test reconciling projects from config with database using ProjectService."""
|
|
# Setup mocks
|
|
mock_session_maker = AsyncMock()
|
|
mock_get_db.return_value = (None, mock_session_maker)
|
|
|
|
mock_repository = AsyncMock()
|
|
mock_project_service = AsyncMock()
|
|
mock_project_service.synchronize_projects = AsyncMock()
|
|
|
|
# Mock the repository and project service
|
|
with (
|
|
patch("basic_memory.services.initialization.ProjectRepository") as mock_repo_class,
|
|
patch(
|
|
"basic_memory.services.project_service.ProjectService",
|
|
return_value=mock_project_service,
|
|
),
|
|
):
|
|
mock_repo_class.return_value = mock_repository
|
|
|
|
# Set up app_config projects as a dictionary
|
|
app_config.projects = {"test_project": "/path/to/project", "new_project": "/path/to/new"}
|
|
app_config.default_project = "test_project"
|
|
|
|
# Run the function
|
|
await reconcile_projects_with_config(app_config)
|
|
|
|
# Assertions
|
|
mock_get_db.assert_called_once()
|
|
mock_repo_class.assert_called_once_with(mock_session_maker)
|
|
mock_project_service.synchronize_projects.assert_called_once()
|
|
|
|
# We should no longer be calling these directly since we're using the service
|
|
mock_repository.find_all.assert_not_called()
|
|
mock_repository.set_as_default.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_reconcile_projects_with_error_handling(mock_get_db, app_config):
|
|
"""Test error handling during project synchronization."""
|
|
# Setup mocks
|
|
mock_session_maker = AsyncMock()
|
|
mock_get_db.return_value = (None, mock_session_maker)
|
|
|
|
mock_repository = AsyncMock()
|
|
mock_project_service = AsyncMock()
|
|
mock_project_service.synchronize_projects = AsyncMock(
|
|
side_effect=ValueError("Project synchronization error")
|
|
)
|
|
|
|
# Mock the repository and project service
|
|
with (
|
|
patch("basic_memory.services.initialization.ProjectRepository") as mock_repo_class,
|
|
patch(
|
|
"basic_memory.services.project_service.ProjectService",
|
|
return_value=mock_project_service,
|
|
),
|
|
patch("basic_memory.services.initialization.logger") as mock_logger,
|
|
):
|
|
mock_repo_class.return_value = mock_repository
|
|
|
|
# Set up app_config projects as a dictionary
|
|
app_config.projects = {"test_project": "/path/to/project"}
|
|
app_config.default_project = "missing_project"
|
|
|
|
# Run the function which now has error handling
|
|
await reconcile_projects_with_config(app_config)
|
|
|
|
# Assertions
|
|
mock_get_db.assert_called_once()
|
|
mock_repo_class.assert_called_once_with(mock_session_maker)
|
|
mock_project_service.synchronize_projects.assert_called_once()
|
|
|
|
# Verify error was logged
|
|
mock_logger.error.assert_called_once_with(
|
|
"Error during project synchronization: Project synchronization error"
|
|
)
|
|
mock_logger.info.assert_any_call(
|
|
"Continuing with initialization despite synchronization error"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_migrate_legacy_projects_no_legacy_dirs(mock_get_db, app_config):
|
|
"""Test migration when no legacy dirs exist."""
|
|
# Setup mocks
|
|
mock_session_maker = AsyncMock()
|
|
mock_get_db.return_value = (None, mock_session_maker)
|
|
|
|
mock_repository = AsyncMock()
|
|
|
|
with (
|
|
patch("basic_memory.services.initialization.Path") as mock_path,
|
|
patch("basic_memory.services.initialization.ProjectRepository") as mock_repo_class,
|
|
patch("basic_memory.services.initialization.migrate_legacy_project_data") as mock_migrate,
|
|
):
|
|
# Create a mock for the Path instance
|
|
mock_legacy_dir = MagicMock()
|
|
mock_legacy_dir.exists.return_value = False
|
|
mock_path.return_value.__truediv__.return_value = mock_legacy_dir
|
|
|
|
mock_repo_class.return_value = mock_repository
|
|
|
|
# Set up app_config projects as a dictionary
|
|
app_config.projects = {"test_project": "/path/to/project"}
|
|
|
|
# Run the function
|
|
await migrate_legacy_projects(app_config)
|
|
|
|
# Assertions - should not call get_by_name or migrate_legacy_project_data
|
|
mock_repository.get_by_name.assert_not_called()
|
|
mock_migrate.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.migrate_legacy_project_data")
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
async def test_migrate_legacy_projects_with_legacy_dirs(
|
|
mock_get_db, mock_migrate_legacy, app_config, tmp_path
|
|
):
|
|
"""Test migration with legacy dirs."""
|
|
# Setup mocks
|
|
mock_session_maker = AsyncMock()
|
|
mock_get_db.return_value = (None, mock_session_maker)
|
|
|
|
mock_repository = AsyncMock()
|
|
mock_project = MagicMock()
|
|
mock_project.name = "test_project"
|
|
mock_project.id = 1 # Add numeric ID
|
|
|
|
# Create a temporary legacy dir
|
|
legacy_dir = tmp_path / ".basic-memory"
|
|
legacy_dir.mkdir(exist_ok=True)
|
|
|
|
# Mock the repository
|
|
with patch("basic_memory.services.initialization.ProjectRepository") as mock_repo_class:
|
|
mock_repo_class.return_value = mock_repository
|
|
mock_repository.get_by_name.return_value = mock_project
|
|
|
|
# Set up app_config projects as a dictionary
|
|
app_config.projects = {"test_project": str(tmp_path)}
|
|
|
|
# Run the function
|
|
with patch("basic_memory.services.initialization.Path", lambda x: Path(x)):
|
|
await migrate_legacy_projects(app_config)
|
|
|
|
# Assertions
|
|
mock_repository.get_by_name.assert_called_once_with("test_project")
|
|
mock_migrate_legacy.assert_called_once_with(mock_project, legacy_dir)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.shutil.rmtree")
|
|
async def test_migrate_legacy_project_data_success(mock_rmtree, tmp_path):
|
|
"""Test successful migration of legacy project data."""
|
|
# Setup mocks
|
|
mock_project = MagicMock()
|
|
mock_project.name = "test_project"
|
|
mock_project.path = str(tmp_path)
|
|
mock_project.id = 1 # Add numeric ID
|
|
|
|
mock_sync_service = AsyncMock()
|
|
mock_sync_service.sync = AsyncMock()
|
|
|
|
# Create a legacy dir
|
|
legacy_dir = tmp_path / ".basic-memory"
|
|
|
|
# Run the function
|
|
with patch(
|
|
"basic_memory.cli.commands.sync.get_sync_service", AsyncMock(return_value=mock_sync_service)
|
|
):
|
|
result = await migrate_legacy_project_data(mock_project, legacy_dir)
|
|
|
|
# Assertions
|
|
mock_sync_service.sync.assert_called_once_with(
|
|
Path(mock_project.path), project_name=mock_project.name
|
|
)
|
|
mock_rmtree.assert_called_once_with(legacy_dir)
|
|
assert result is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.shutil.rmtree")
|
|
async def test_migrate_legacy_project_data_rmtree_error(mock_rmtree, tmp_path):
|
|
"""Test migration of legacy project data with rmtree error."""
|
|
# Setup mocks
|
|
mock_project = MagicMock()
|
|
mock_project.name = "test_project"
|
|
mock_project.path = str(tmp_path)
|
|
mock_project.id = 1 # Add numeric ID
|
|
|
|
mock_sync_service = AsyncMock()
|
|
mock_sync_service.sync = AsyncMock()
|
|
|
|
# Make rmtree raise an exception
|
|
mock_rmtree.side_effect = Exception("Test error")
|
|
|
|
# Create a legacy dir
|
|
legacy_dir = tmp_path / ".basic-memory"
|
|
|
|
# Run the function
|
|
with patch(
|
|
"basic_memory.cli.commands.sync.get_sync_service", AsyncMock(return_value=mock_sync_service)
|
|
):
|
|
result = await migrate_legacy_project_data(mock_project, legacy_dir)
|
|
|
|
# Assertions
|
|
mock_sync_service.sync.assert_called_once_with(
|
|
Path(mock_project.path), project_name=mock_project.name
|
|
)
|
|
mock_rmtree.assert_called_once_with(legacy_dir)
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("basic_memory.services.initialization.db.get_or_create_db")
|
|
@patch("basic_memory.cli.commands.sync.get_sync_service")
|
|
@patch("basic_memory.sync.WatchService")
|
|
async def test_initialize_file_sync_sequential(
|
|
mock_watch_service_class, mock_get_sync_service, mock_get_db, app_config
|
|
):
|
|
"""Test file sync initialization with sequential project processing."""
|
|
# Setup mocks
|
|
mock_session_maker = AsyncMock()
|
|
mock_get_db.return_value = (None, mock_session_maker)
|
|
|
|
mock_watch_service = AsyncMock()
|
|
mock_watch_service.run = AsyncMock()
|
|
mock_watch_service_class.return_value = mock_watch_service
|
|
|
|
mock_repository = AsyncMock()
|
|
mock_project1 = MagicMock()
|
|
mock_project1.name = "project1"
|
|
mock_project1.path = "/path/to/project1"
|
|
mock_project1.id = 1
|
|
|
|
mock_project2 = MagicMock()
|
|
mock_project2.name = "project2"
|
|
mock_project2.path = "/path/to/project2"
|
|
mock_project2.id = 2
|
|
|
|
mock_sync_service = AsyncMock()
|
|
mock_sync_service.sync = AsyncMock()
|
|
mock_get_sync_service.return_value = mock_sync_service
|
|
|
|
# Mock the repository
|
|
with patch("basic_memory.services.initialization.ProjectRepository") as mock_repo_class:
|
|
mock_repo_class.return_value = mock_repository
|
|
mock_repository.get_active_projects.return_value = [mock_project1, mock_project2]
|
|
|
|
# Run the function
|
|
result = await initialize_file_sync(app_config)
|
|
|
|
# Assertions
|
|
mock_repository.get_active_projects.assert_called_once()
|
|
|
|
# Should call sync for each project sequentially
|
|
assert mock_get_sync_service.call_count == 2
|
|
mock_get_sync_service.assert_any_call(mock_project1)
|
|
mock_get_sync_service.assert_any_call(mock_project2)
|
|
|
|
# Should call sync on each project
|
|
assert mock_sync_service.sync.call_count == 2
|
|
mock_sync_service.sync.assert_any_call(
|
|
Path(mock_project1.path), project_name=mock_project1.name
|
|
)
|
|
mock_sync_service.sync.assert_any_call(
|
|
Path(mock_project2.path), project_name=mock_project2.name
|
|
)
|
|
|
|
# Should start the watch service
|
|
mock_watch_service.run.assert_called_once()
|
|
|
|
# Should return None
|
|
assert result is None
|