Files
Paul Hernandez 8df88e4d02 feat: add basic-memory watch CLI command (#559)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:10:09 -06:00

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