chore(core): use ty for typechecking

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-04-13 01:42:22 -05:00
parent abd4a5a6da
commit 58dd6963bd
88 changed files with 961 additions and 551 deletions
+5 -3
View File
@@ -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
+41 -38
View File
@@ -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
+21 -14
View File
@@ -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]] = []