mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
8df88e4d02
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.5 KiB
Python
169 lines
5.5 KiB
Python
"""Tests for SyncCoordinator - centralized sync/watch lifecycle."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from basic_memory.config import BasicMemoryConfig
|
|
from basic_memory.sync.coordinator import SyncCoordinator, SyncStatus
|
|
|
|
|
|
class TestSyncCoordinator:
|
|
"""Test SyncCoordinator class."""
|
|
|
|
@pytest.fixture
|
|
def mock_config(self):
|
|
"""Create a mock config for testing."""
|
|
return BasicMemoryConfig()
|
|
|
|
def test_initial_status(self, mock_config):
|
|
"""Coordinator starts in NOT_STARTED state."""
|
|
coordinator = SyncCoordinator(config=mock_config)
|
|
assert coordinator.status == SyncStatus.NOT_STARTED
|
|
assert coordinator.is_running is False
|
|
|
|
def test_quiet_defaults_to_true(self, mock_config):
|
|
"""quiet field defaults to True (for MCP/background use)."""
|
|
coordinator = SyncCoordinator(config=mock_config)
|
|
assert coordinator.quiet is True
|
|
|
|
def test_quiet_can_be_set_false(self, mock_config):
|
|
"""quiet field can be set to False (for CLI watch command)."""
|
|
coordinator = SyncCoordinator(config=mock_config, quiet=False)
|
|
assert coordinator.quiet is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_when_sync_disabled(self, mock_config):
|
|
"""When should_sync is False, start() sets status to STOPPED."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=False,
|
|
skip_reason="Test skip",
|
|
)
|
|
|
|
await coordinator.start()
|
|
|
|
assert coordinator.status == SyncStatus.STOPPED
|
|
assert coordinator.is_running is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_when_not_started(self, mock_config):
|
|
"""Stop is safe to call when not started."""
|
|
coordinator = SyncCoordinator(config=mock_config)
|
|
|
|
await coordinator.stop() # Should not raise
|
|
|
|
assert coordinator.status == SyncStatus.NOT_STARTED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_when_stopped(self, mock_config):
|
|
"""Stop is idempotent when already stopped."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=False,
|
|
)
|
|
await coordinator.start() # Sets to STOPPED
|
|
|
|
await coordinator.stop() # Should not raise
|
|
|
|
assert coordinator.status == SyncStatus.STOPPED
|
|
|
|
def test_get_status_info(self, mock_config):
|
|
"""get_status_info returns diagnostic info."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=True,
|
|
skip_reason=None,
|
|
)
|
|
|
|
info = coordinator.get_status_info()
|
|
|
|
assert info["status"] == "NOT_STARTED"
|
|
assert info["should_sync"] is True
|
|
assert info["skip_reason"] is None
|
|
assert info["has_task"] is False
|
|
|
|
def test_get_status_info_with_skip_reason(self, mock_config):
|
|
"""get_status_info includes skip reason."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=False,
|
|
skip_reason="Test environment detected",
|
|
)
|
|
|
|
info = coordinator.get_status_info()
|
|
|
|
assert info["should_sync"] is False
|
|
assert info["skip_reason"] == "Test environment detected"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_creates_task(self, mock_config):
|
|
"""When should_sync is True, start() creates a background task."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=True,
|
|
)
|
|
|
|
# Mock initialize_file_sync to avoid actually starting sync
|
|
# The import happens inside start(), so patch at the source module
|
|
with patch(
|
|
"basic_memory.services.initialization.initialize_file_sync",
|
|
new_callable=AsyncMock,
|
|
):
|
|
# Start coordinator
|
|
await coordinator.start()
|
|
|
|
# Should be running with a task
|
|
assert coordinator.status == SyncStatus.RUNNING
|
|
assert coordinator.is_running is True
|
|
assert coordinator._sync_task is not None
|
|
|
|
# Stop to clean up
|
|
await coordinator.stop()
|
|
|
|
assert coordinator.status == SyncStatus.STOPPED
|
|
assert coordinator._sync_task is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_passes_quiet_to_file_sync(self, mock_config):
|
|
"""quiet value is threaded through to initialize_file_sync."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=True,
|
|
quiet=False,
|
|
)
|
|
|
|
with patch(
|
|
"basic_memory.services.initialization.initialize_file_sync",
|
|
new_callable=AsyncMock,
|
|
) as mock_init:
|
|
await coordinator.start()
|
|
# Let the task run so initialize_file_sync gets called
|
|
await asyncio.sleep(0.01)
|
|
|
|
mock_init.assert_called_once_with(mock_config, quiet=False)
|
|
|
|
await coordinator.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_already_running(self, mock_config):
|
|
"""Starting when already running is a no-op."""
|
|
coordinator = SyncCoordinator(
|
|
config=mock_config,
|
|
should_sync=True,
|
|
)
|
|
|
|
with patch(
|
|
"basic_memory.services.initialization.initialize_file_sync",
|
|
new_callable=AsyncMock,
|
|
):
|
|
await coordinator.start()
|
|
first_task = coordinator._sync_task
|
|
|
|
# Start again - should not create new task
|
|
await coordinator.start()
|
|
assert coordinator._sync_task is first_task
|
|
|
|
await coordinator.stop()
|