Files
basicmachines-co-basic-memory/tests/cli/test_db_reindex.py
2026-04-09 00:20:12 -05:00

323 lines
10 KiB
Python

"""Tests for `bm reindex` CLI wiring."""
import asyncio
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from typer.testing import CliRunner
from basic_memory.cli.app import app
import basic_memory.cli.commands.db as db_cmd # noqa: F401
runner = CliRunner()
def _stub_app_config(*, semantic_search_enabled: bool = True) -> SimpleNamespace:
"""Build the minimal config surface the CLI reindex path expects."""
return SimpleNamespace(
semantic_search_enabled=semantic_search_enabled,
database_path=Path("/tmp/basic-memory.db"),
get_project_mode=lambda project_name: None,
)
def _configure_reindex_cli(monkeypatch, app_config: SimpleNamespace) -> None:
"""Keep CLI tests focused on reindex wiring instead of full app startup."""
monkeypatch.setattr("basic_memory.cli.app.init_cli_logging", lambda: None)
monkeypatch.setattr("basic_memory.cli.app.maybe_show_init_line", lambda *_args: None)
monkeypatch.setattr("basic_memory.cli.app.maybe_show_cloud_promo", lambda *_args: None)
monkeypatch.setattr("basic_memory.cli.app.maybe_run_periodic_auto_update", lambda *_args: None)
monkeypatch.setattr(
"basic_memory.cli.app.CliContainer.create",
lambda: SimpleNamespace(config=app_config, mode=SimpleNamespace(is_cloud=False)),
)
monkeypatch.setattr(
db_cmd,
"ConfigManager",
lambda: SimpleNamespace(config=app_config),
)
def test_reindex_defaults_to_incremental_search_and_embeddings(monkeypatch):
app_config = _stub_app_config()
_configure_reindex_cli(monkeypatch, app_config)
captured: dict[str, object] = {}
async def _stub_reindex(app_config, *, search: bool, embeddings: bool, full: bool, project):
captured.update(
{
"app_config": app_config,
"search": search,
"embeddings": embeddings,
"full": full,
"project": project,
}
)
monkeypatch.setattr(db_cmd, "_reindex", _stub_reindex)
monkeypatch.setattr(db_cmd, "run_with_cleanup", lambda coro: asyncio.run(coro))
result = runner.invoke(app, ["reindex"])
assert result.exit_code == 0
assert captured == {
"app_config": app_config,
"search": True,
"embeddings": True,
"full": False,
"project": None,
}
def test_reindex_full_runs_full_search_and_embeddings(monkeypatch):
app_config = _stub_app_config()
_configure_reindex_cli(monkeypatch, app_config)
captured: dict[str, object] = {}
async def _stub_reindex(app_config, *, search: bool, embeddings: bool, full: bool, project):
captured.update(
{
"search": search,
"embeddings": embeddings,
"full": full,
"project": project,
}
)
monkeypatch.setattr(db_cmd, "_reindex", _stub_reindex)
monkeypatch.setattr(db_cmd, "run_with_cleanup", lambda coro: asyncio.run(coro))
result = runner.invoke(app, ["reindex", "--full"])
assert result.exit_code == 0
assert captured == {
"search": True,
"embeddings": True,
"full": True,
"project": None,
}
def test_reindex_full_search_runs_search_only(monkeypatch):
app_config = _stub_app_config()
_configure_reindex_cli(monkeypatch, app_config)
captured: dict[str, object] = {}
async def _stub_reindex(app_config, *, search: bool, embeddings: bool, full: bool, project):
captured.update(
{
"search": search,
"embeddings": embeddings,
"full": full,
"project": project,
}
)
monkeypatch.setattr(db_cmd, "_reindex", _stub_reindex)
monkeypatch.setattr(db_cmd, "run_with_cleanup", lambda coro: asyncio.run(coro))
result = runner.invoke(app, ["reindex", "--full", "--search"])
assert result.exit_code == 0
assert captured == {
"search": True,
"embeddings": False,
"full": True,
"project": None,
}
def test_reindex_embeddings_only_preserves_incremental_default(monkeypatch):
app_config = _stub_app_config()
_configure_reindex_cli(monkeypatch, app_config)
captured: dict[str, object] = {}
async def _stub_reindex(app_config, *, search: bool, embeddings: bool, full: bool, project):
captured.update(
{
"search": search,
"embeddings": embeddings,
"full": full,
"project": project,
}
)
monkeypatch.setattr(db_cmd, "_reindex", _stub_reindex)
monkeypatch.setattr(db_cmd, "run_with_cleanup", lambda coro: asyncio.run(coro))
result = runner.invoke(app, ["reindex", "--embeddings"])
assert result.exit_code == 0
assert captured == {
"search": False,
"embeddings": True,
"full": False,
"project": None,
}
@pytest.mark.asyncio
async def test_reindex_project_full_passes_force_full_to_sync_and_reports_mode(monkeypatch):
app_config = _stub_app_config()
project = SimpleNamespace(id=1, name="foo", path="/tmp/foo")
session_maker = object()
sync_service = SimpleNamespace(sync=AsyncMock())
printed_lines: list[str] = []
class StubProjectRepository:
def __init__(self, _session_maker):
self._session_maker = _session_maker
async def get_active_projects(self):
return [project]
class SilentProgress:
def __init__(self, *args, **kwargs):
self.tasks: dict[int, SimpleNamespace] = {}
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def add_task(self, description, total=1):
self.tasks[1] = SimpleNamespace(total=total, description=description)
return 1
def update(self, task_id, **kwargs):
if "total" in kwargs:
self.tasks[task_id].total = kwargs["total"]
monkeypatch.setattr(db_cmd, "reconcile_projects_with_config", AsyncMock())
monkeypatch.setattr(
db_cmd.db,
"get_or_create_db",
AsyncMock(return_value=(None, session_maker)),
)
monkeypatch.setattr(db_cmd.db, "shutdown_db", AsyncMock())
monkeypatch.setattr(db_cmd, "ProjectRepository", StubProjectRepository)
monkeypatch.setattr(db_cmd, "get_sync_service", AsyncMock(return_value=sync_service))
monkeypatch.setattr(db_cmd, "Progress", SilentProgress)
monkeypatch.setattr(
db_cmd.console,
"print",
lambda message="", *args, **kwargs: printed_lines.append(str(message)),
)
await db_cmd._reindex(
app_config,
search=True,
embeddings=False,
full=True,
project="foo",
)
sync_service.sync.assert_awaited_once()
sync_call = sync_service.sync.await_args
assert sync_call.args[0] == Path("/tmp/foo")
assert sync_call.kwargs["project_name"] == "foo"
assert sync_call.kwargs["force_full"] is True
assert sync_call.kwargs["sync_embeddings"] is False
assert callable(sync_call.kwargs["progress_callback"])
assert any("full scan" in line for line in printed_lines)
@pytest.mark.asyncio
async def test_reindex_embeddings_only_full_passes_force_full_to_vector_reindex(monkeypatch):
app_config = _stub_app_config()
project = SimpleNamespace(id=1, name="foo", path="/tmp/foo")
session_maker = object()
printed_lines: list[str] = []
vector_reindex_calls: list[dict[str, object]] = []
class StubProjectRepository:
def __init__(self, _session_maker):
self._session_maker = _session_maker
async def get_active_projects(self):
return [project]
class StubSearchService:
def __init__(self, search_repository, entity_repository, file_service):
self.search_repository = search_repository
self.entity_repository = entity_repository
self.file_service = file_service
async def reindex_vectors(self, *, progress_callback=None, force_full: bool = False):
vector_reindex_calls.append(
{
"progress_callback": progress_callback,
"force_full": force_full,
}
)
return {"total_entities": 2, "embedded": 2, "skipped": 0, "errors": 0}
class SilentProgress:
def __init__(self, *args, **kwargs):
self.tasks: dict[int, SimpleNamespace] = {}
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def add_task(self, description, total=None):
self.tasks[1] = SimpleNamespace(total=total, description=description)
return 1
def update(self, task_id, **kwargs):
if "total" in kwargs:
self.tasks[task_id].total = kwargs["total"]
monkeypatch.setattr(db_cmd, "reconcile_projects_with_config", AsyncMock())
monkeypatch.setattr(
db_cmd.db,
"get_or_create_db",
AsyncMock(return_value=(None, session_maker)),
)
monkeypatch.setattr(db_cmd.db, "shutdown_db", AsyncMock())
monkeypatch.setattr(db_cmd, "ProjectRepository", StubProjectRepository)
monkeypatch.setattr(
"basic_memory.repository.search_repository.create_search_repository",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.repository.EntityRepository", lambda *args, **kwargs: object()
)
monkeypatch.setattr(
"basic_memory.markdown.entity_parser.EntityParser",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.markdown.markdown_processor.MarkdownProcessor",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.services.file_service.FileService", lambda *args, **kwargs: object()
)
monkeypatch.setattr("basic_memory.services.search_service.SearchService", StubSearchService)
monkeypatch.setattr(db_cmd, "Progress", SilentProgress)
monkeypatch.setattr(
db_cmd.console,
"print",
lambda message="", *args, **kwargs: printed_lines.append(str(message)),
)
await db_cmd._reindex(
app_config,
search=False,
embeddings=True,
full=True,
project="foo",
)
assert len(vector_reindex_calls) == 1
assert vector_reindex_calls[0]["force_full"] is True
assert callable(vector_reindex_calls[0]["progress_callback"])
assert any("full rebuild" in line for line in printed_lines)