mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
4791e19685
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
5.6 KiB
Python
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
|