mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
chore(core): use ty for typechecking
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1783,7 +1784,8 @@ type: note
|
||||
await sync_service.sync(project_dir)
|
||||
|
||||
# Patch index_entity to raise SemanticDependenciesMissingError
|
||||
original_index = sync_service.search_service.index_entity
|
||||
search_service_mock = cast(Any, sync_service.search_service)
|
||||
original_index = search_service_mock.index_entity
|
||||
call_count = 0
|
||||
|
||||
async def index_with_semantic_error(entity, **kwargs):
|
||||
@@ -1791,7 +1793,7 @@ type: note
|
||||
call_count += 1
|
||||
raise SemanticDependenciesMissingError("sqlite-vec package is missing")
|
||||
|
||||
sync_service.search_service.index_entity = AsyncMock(side_effect=index_with_semantic_error)
|
||||
search_service_mock.index_entity = AsyncMock(side_effect=index_with_semantic_error)
|
||||
|
||||
try:
|
||||
# Modify the file so it gets re-synced
|
||||
@@ -1809,4 +1811,4 @@ type: note
|
||||
# Verify circuit breaker was NOT triggered (failure not recorded)
|
||||
assert "semantic_test.md" not in sync_service._file_failures
|
||||
finally:
|
||||
sync_service.search_service.index_entity = original_index
|
||||
search_service_mock.index_entity = original_index
|
||||
|
||||
@@ -19,9 +19,31 @@ import pytest
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.indexing.models import IndexingBatchResult
|
||||
from basic_memory.models import Project
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
|
||||
async def _current_project(sync_service: SyncService) -> Project:
|
||||
project_id = sync_service.entity_repository.project_id
|
||||
assert project_id is not None
|
||||
|
||||
project = await sync_service.project_repository.find_by_id(project_id)
|
||||
assert project is not None
|
||||
return project
|
||||
|
||||
|
||||
def _last_scan_timestamp(project: Project) -> float:
|
||||
timestamp = project.last_scan_timestamp
|
||||
assert timestamp is not None
|
||||
return timestamp
|
||||
|
||||
|
||||
def _last_file_count(project: Project) -> int:
|
||||
file_count = project.last_file_count
|
||||
assert file_count is not None
|
||||
return file_count
|
||||
|
||||
|
||||
async def create_test_file(path: Path, content: str = "test content") -> None:
|
||||
"""Create a test file with given content."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -59,11 +81,9 @@ async def test_first_sync_uses_full_scan(sync_service: SyncService, project_conf
|
||||
assert "file2.md" in report.new
|
||||
|
||||
# Verify watermark was set
|
||||
project = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
project = await _current_project(sync_service)
|
||||
assert project.last_scan_timestamp is not None
|
||||
assert project.last_file_count >= 2 # May include config files
|
||||
assert _last_file_count(project) >= 2 # May include config files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -168,11 +188,8 @@ async def test_force_full_bypasses_watermark_optimization(
|
||||
assert len(report.new) == 2
|
||||
|
||||
# Verify watermark was set
|
||||
project = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
assert project.last_scan_timestamp is not None
|
||||
initial_timestamp = project.last_scan_timestamp
|
||||
project = await _current_project(sync_service)
|
||||
initial_timestamp = _last_scan_timestamp(project)
|
||||
|
||||
# Sleep to ensure time passes
|
||||
await sleep_past_watermark()
|
||||
@@ -202,11 +219,8 @@ async def test_force_full_bypasses_watermark_optimization(
|
||||
assert "file1.md" in report.modified
|
||||
|
||||
# Verify watermark was still updated after force_full
|
||||
project = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
assert project.last_scan_timestamp is not None
|
||||
assert project.last_scan_timestamp > initial_timestamp
|
||||
project = await _current_project(sync_service)
|
||||
assert _last_scan_timestamp(project) > initial_timestamp
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -534,9 +548,7 @@ async def test_watermark_updated_after_successful_sync(
|
||||
await create_test_file(project_dir / "file1.md", "# File 1")
|
||||
|
||||
# Get project before sync
|
||||
project_before = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
project_before = await _current_project(sync_service)
|
||||
assert project_before.last_scan_timestamp is None
|
||||
assert project_before.last_file_count is None
|
||||
|
||||
@@ -546,14 +558,12 @@ async def test_watermark_updated_after_successful_sync(
|
||||
sync_end = time.time()
|
||||
|
||||
# Verify watermark was set
|
||||
project_after = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
project_after = await _current_project(sync_service)
|
||||
assert project_after.last_scan_timestamp is not None
|
||||
assert project_after.last_file_count >= 1 # May include config files
|
||||
assert _last_file_count(project_after) >= 1 # May include config files
|
||||
|
||||
# Watermark should be between sync start and end
|
||||
assert sync_start <= project_after.last_scan_timestamp <= sync_end
|
||||
assert sync_start <= _last_scan_timestamp(project_after) <= sync_end
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -572,14 +582,13 @@ async def test_watermark_uses_sync_start_time(
|
||||
sync_end = time.time()
|
||||
|
||||
# Get watermark
|
||||
project = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
project = await _current_project(sync_service)
|
||||
|
||||
# Watermark should be closer to start than end
|
||||
# (In practice, watermark == sync_start_timestamp captured in sync())
|
||||
time_from_start = abs(project.last_scan_timestamp - sync_start)
|
||||
time_from_end = abs(project.last_scan_timestamp - sync_end)
|
||||
project_timestamp = _last_scan_timestamp(project)
|
||||
time_from_start = abs(project_timestamp - sync_start)
|
||||
time_from_end = abs(project_timestamp - sync_end)
|
||||
|
||||
assert time_from_start < time_from_end
|
||||
|
||||
@@ -600,10 +609,8 @@ async def test_watermark_file_count_accurate(
|
||||
await sync_service.sync(project_dir)
|
||||
|
||||
# Verify file count
|
||||
project1 = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
initial_count = project1.last_file_count
|
||||
project1 = await _current_project(sync_service)
|
||||
initial_count = _last_file_count(project1)
|
||||
assert initial_count >= 3 # May include config files
|
||||
|
||||
# Add more files
|
||||
@@ -615,10 +622,8 @@ async def test_watermark_file_count_accurate(
|
||||
await sync_service.sync(project_dir)
|
||||
|
||||
# Verify updated count increased by 2
|
||||
project2 = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
assert project2.last_file_count == initial_count + 2
|
||||
project2 = await _current_project(sync_service)
|
||||
assert _last_file_count(project2) == initial_count + 2
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@@ -667,9 +672,7 @@ async def test_empty_directory_handles_incremental_scan(
|
||||
assert len(report1.new) == 0
|
||||
|
||||
# Verify watermark was set even for empty directory
|
||||
project = await sync_service.project_repository.find_by_id(
|
||||
sync_service.entity_repository.project_id
|
||||
)
|
||||
project = await _current_project(sync_service)
|
||||
assert project.last_scan_timestamp is not None
|
||||
# May have config files, so just check it's set
|
||||
assert project.last_file_count is not None
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -33,11 +34,15 @@ class _Repo:
|
||||
return self.projects_return or []
|
||||
|
||||
|
||||
def _watch_service(config: BasicMemoryConfig, repo: _Repo) -> WatchService:
|
||||
return WatchService(config, cast(Any, repo), quiet=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_restart_uses_config_interval(monkeypatch):
|
||||
config = BasicMemoryConfig(watch_project_reload_interval=2)
|
||||
repo = _Repo()
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
stop_event = asyncio.Event()
|
||||
slept: list[int] = []
|
||||
@@ -58,12 +63,12 @@ async def test_schedule_restart_uses_config_interval(monkeypatch):
|
||||
async def test_watch_projects_cycle_handles_empty_project_list(monkeypatch):
|
||||
config = BasicMemoryConfig()
|
||||
repo = _Repo()
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
stop_event = asyncio.Event()
|
||||
stop_event.set()
|
||||
|
||||
captured = {"args": None, "kwargs": None}
|
||||
captured: dict[str, Any] = {"args": None, "kwargs": None}
|
||||
|
||||
async def awatch_stub(*args, **kwargs):
|
||||
captured["args"] = args
|
||||
@@ -76,18 +81,20 @@ async def test_watch_projects_cycle_handles_empty_project_list(monkeypatch):
|
||||
|
||||
await watch_service._watch_projects_cycle([], stop_event)
|
||||
|
||||
kwargs = captured["kwargs"]
|
||||
assert isinstance(kwargs, dict)
|
||||
assert captured["args"] == ()
|
||||
assert captured["kwargs"]["debounce"] == config.sync_delay
|
||||
assert captured["kwargs"]["watch_filter"] == watch_service.filter_changes
|
||||
assert captured["kwargs"]["recursive"] is True
|
||||
assert captured["kwargs"]["stop_event"] is stop_event
|
||||
assert kwargs["debounce"] == config.sync_delay
|
||||
assert kwargs["watch_filter"] == watch_service.filter_changes
|
||||
assert kwargs["recursive"] is True
|
||||
assert kwargs["stop_event"] is stop_event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_handles_no_projects(monkeypatch):
|
||||
config = BasicMemoryConfig()
|
||||
repo = _Repo(projects_return=[])
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
slept: list[int] = []
|
||||
|
||||
@@ -120,7 +127,7 @@ async def test_run_reloads_projects_each_cycle(monkeypatch, tmp_path):
|
||||
],
|
||||
]
|
||||
)
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
cycle_count = 0
|
||||
|
||||
@@ -159,7 +166,7 @@ async def test_run_filters_cloud_only_projects_each_cycle(monkeypatch, tmp_path)
|
||||
Project(id=2, name="cloud-only", path="cloud-slug", permalink="cloud-only"),
|
||||
]
|
||||
)
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
seen_project_names: list[list[str]] = []
|
||||
|
||||
@@ -200,7 +207,7 @@ async def test_run_keeps_cloud_projects_with_local_bisync(monkeypatch, tmp_path)
|
||||
),
|
||||
]
|
||||
)
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
seen_project_names: list[list[str]] = []
|
||||
|
||||
@@ -226,7 +233,7 @@ async def test_run_continues_after_cycle_error(monkeypatch, tmp_path):
|
||||
repo = _Repo(
|
||||
projects_return=[Project(id=1, name="test", path=str(tmp_path / "test"), permalink="test")]
|
||||
)
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
call_count = 0
|
||||
slept: list[int] = []
|
||||
@@ -261,7 +268,7 @@ async def test_timer_task_cancelled_properly(monkeypatch, tmp_path):
|
||||
repo = _Repo(
|
||||
projects_return=[Project(id=1, name="test", path=str(tmp_path / "test"), permalink="test")]
|
||||
)
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
created_tasks: list[asyncio.Task] = []
|
||||
real_create_task = asyncio.create_task
|
||||
@@ -312,7 +319,7 @@ async def test_new_project_addition_scenario(monkeypatch, tmp_path):
|
||||
]
|
||||
|
||||
repo = _Repo(projects_side_effect=[initial_projects, initial_projects, updated_projects])
|
||||
watch_service = WatchService(config, repo, quiet=True)
|
||||
watch_service = _watch_service(config, repo)
|
||||
|
||||
cycle_count = 0
|
||||
project_lists_used: list[list[Project]] = []
|
||||
|
||||
Reference in New Issue
Block a user