Files
basicmachines-co-basic-memory/tests/sync/test_sync_service_telemetry.py
Paul Hernandez 4791e19685 feat: add Logfire phased instrumentation (#692)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:39:42 -05:00

171 lines
5.6 KiB
Python

"""Telemetry coverage for sync phase spans."""
from __future__ import annotations
import importlib
from contextlib import contextmanager
from types import SimpleNamespace
import pytest
from basic_memory.sync.sync_service import SyncReport
sync_service_module = importlib.import_module("basic_memory.sync.sync_service")
def _capture_sync_telemetry():
operations: list[tuple[str, dict]] = []
spans: list[tuple[str, dict]] = []
@contextmanager
def fake_operation(name: str, **attrs):
operations.append((name, attrs))
yield
@contextmanager
def fake_span(name: str, **attrs):
spans.append((name, attrs))
yield
return operations, spans, fake_operation, fake_span
@pytest.mark.asyncio
async def test_sync_emits_phase_spans(sync_service, project_config, monkeypatch) -> None:
operations, spans, fake_operation, fake_span = _capture_sync_telemetry()
report = SyncReport(
new={"new.md"},
modified={"modified.md"},
deleted={"deleted.md"},
moves={"old.md": "moved.md"},
)
async def fake_scan(directory, force_full=False):
return report
async def fake_handle_move(old_path, new_path):
return None
async def fake_handle_delete(path):
return None
async def fake_sync_file(path, new=True):
return None, None
async def fake_should_skip_file(path):
return False
async def fake_resolve_relations(entity_id=None):
return None
async def fake_quick_count_files(directory):
return 3
async def fake_find_by_id(project_id):
return SimpleNamespace(id=project_id)
async def fake_update(project_id, values):
return None
monkeypatch.setattr(sync_service_module.telemetry, "operation", fake_operation)
monkeypatch.setattr(sync_service_module.telemetry, "span", fake_span)
monkeypatch.setattr(sync_service, "scan", fake_scan)
monkeypatch.setattr(sync_service, "handle_move", fake_handle_move)
monkeypatch.setattr(sync_service, "handle_delete", fake_handle_delete)
monkeypatch.setattr(sync_service, "sync_file", fake_sync_file)
monkeypatch.setattr(sync_service, "_should_skip_file", fake_should_skip_file)
monkeypatch.setattr(sync_service, "resolve_relations", fake_resolve_relations)
monkeypatch.setattr(sync_service, "_quick_count_files", fake_quick_count_files)
monkeypatch.setattr(sync_service.project_repository, "find_by_id", fake_find_by_id)
monkeypatch.setattr(sync_service.project_repository, "update", fake_update)
sync_service.app_config.semantic_search_enabled = False
result = await sync_service.sync(
project_config.home,
project_name=project_config.name,
force_full=True,
)
assert result is report
assert operations == [
(
"sync.project.run",
{"project_name": project_config.name, "force_full": True},
)
]
assert [name for name, _ in spans] == [
"sync.project.scan",
"sync.project.apply_changes",
"sync.project.resolve_relations",
"sync.project.update_watermark",
]
@pytest.mark.asyncio
async def test_sync_file_emits_failure_span(sync_service, monkeypatch) -> None:
_, spans, _, fake_span = _capture_sync_telemetry()
recorded_failures: list[tuple[str, str]] = []
async def fake_record_failure(path, error):
recorded_failures.append((path, error))
async def fail_sync_markdown_file(path, new=True):
raise ValueError("boom")
monkeypatch.setattr(sync_service_module.telemetry, "span", fake_span)
monkeypatch.setattr(sync_service, "_should_skip_file", lambda path: _false_async())
monkeypatch.setattr(sync_service.file_service, "is_markdown", lambda path: True)
monkeypatch.setattr(sync_service, "sync_markdown_file", fail_sync_markdown_file)
monkeypatch.setattr(sync_service, "_record_failure", fake_record_failure)
result = await sync_service.sync_file("notes/broken.md", new=True)
assert result == (None, None)
assert spans == [
(
"sync.file.failure",
{
"failure_type": "ValueError",
"path": "notes/broken.md",
"file_kind": "markdown",
"is_new": True,
"is_fatal": False,
},
)
]
assert recorded_failures == [("notes/broken.md", "boom")]
@pytest.mark.asyncio
async def test_sync_file_logs_slow_operation(sync_service, monkeypatch) -> None:
warning_messages: list[str] = []
async def fake_sync_regular_file(path, new=True):
return SimpleNamespace(id=1), "deadbeef"
times = iter([10.0, 10.8])
monkeypatch.setattr(sync_service, "_should_skip_file", lambda path: _false_async())
monkeypatch.setattr(sync_service.file_service, "is_markdown", lambda path: False)
monkeypatch.setattr(sync_service, "sync_regular_file", fake_sync_regular_file)
monkeypatch.setattr(sync_service.search_service, "index_entity", _none_async)
monkeypatch.setattr(sync_service, "_clear_failure", lambda path: None)
monkeypatch.setattr(sync_service_module.time, "time", lambda: next(times))
monkeypatch.setattr(sync_service_module.logger, "warning", warning_messages.append)
entity, checksum = await sync_service.sync_file("assets/large.bin", new=False)
assert entity.id == 1
assert checksum == "deadbeef"
assert warning_messages == [
"Slow file sync detected: path=assets/large.bin, file_kind=regular, duration_ms=800"
]
async def _false_async() -> bool:
return False
async def _none_async(*args, **kwargs) -> None:
return None