diff --git a/src/basic_memory/sync/sync_service.py b/src/basic_memory/sync/sync_service.py index 945add2a..c86149de 100644 --- a/src/basic_memory/sync/sync_service.py +++ b/src/basic_memory/sync/sync_service.py @@ -1087,6 +1087,22 @@ class SyncService: initial_markdown_content = initial_markdown_bytes.decode("utf-8") file_metadata = await self.file_service.get_file_metadata(path) initial_checksum = await compute_checksum(initial_markdown_bytes) + existing_entity = await self.entity_repository.get_by_file_path(path) + if existing_entity is not None and existing_entity.checksum == initial_checksum: + logger.debug( + f"Markdown sync skipped unchanged file: path={path}, " + f"entity_id={existing_entity.id}, checksum={initial_checksum[:8]}" + ) + return SyncedMarkdownFile( + entity=existing_entity, + checksum=initial_checksum, + markdown_content=initial_markdown_content, + file_path=path, + content_type=self.file_service.content_type(path), + updated_at=file_metadata.modified_at, + size=file_metadata.size, + ) + indexed = await self.batch_indexer.index_markdown_file( IndexInputFile( path=path, diff --git a/tests/sync/test_sync_one_markdown_file.py b/tests/sync/test_sync_one_markdown_file.py index b423f2bc..8df36544 100644 --- a/tests/sync/test_sync_one_markdown_file.py +++ b/tests/sync/test_sync_one_markdown_file.py @@ -194,6 +194,103 @@ async def test_sync_one_markdown_file_does_not_reread_for_initial_checksum_when_ assert result.checksum == await compute_checksum(file_path.read_bytes()) +@pytest.mark.asyncio +async def test_sync_one_markdown_file_skips_indexing_when_checksum_matches( + sync_service, + test_project, + monkeypatch, +): + """A matching DB checksum is the consistency boundary for derived indexes.""" + original_content = dedent( + f"""\ + --- + title: Already Current + type: note + permalink: {test_project.name}/notes/already-current + --- + + # Already Current + + - [note] Derived indexes are assumed current when the file checksum matches. + """ + ) + file_path = _write_markdown( + Path(test_project.path), + "notes/already-current.md", + original_content, + ) + + initial = await sync_service.sync_one_markdown_file( + "notes/already-current.md", + index_search=False, + ) + assert initial.checksum == await compute_checksum(file_path.read_bytes()) + + index_markdown_file = AsyncMock(side_effect=AssertionError("indexer should not run")) + index_entity_data = AsyncMock(side_effect=AssertionError("search should not refresh")) + monkeypatch.setattr(sync_service.batch_indexer, "index_markdown_file", index_markdown_file) + monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data) + + result = await sync_service.sync_one_markdown_file("notes/already-current.md") + + index_markdown_file.assert_not_awaited() + index_entity_data.assert_not_awaited() + assert result.entity.id == initial.entity.id + assert len(result.entity.observations) == 1 + assert result.markdown_content == file_path.read_bytes().decode("utf-8") + assert result.checksum == initial.checksum + + +@pytest.mark.asyncio +async def test_sync_one_markdown_file_indexes_when_checksum_differs( + sync_service, + test_project, + monkeypatch, +): + """A DB checksum mismatch still takes the full indexing path.""" + initial_content = dedent( + f"""\ + --- + title: Changed + type: note + permalink: {test_project.name}/notes/changed + --- + + # Changed + + Original body. + """ + ) + file_path = _write_markdown( + Path(test_project.path), + "notes/changed.md", + initial_content, + ) + initial = await sync_service.sync_one_markdown_file("notes/changed.md", index_search=False) + + updated_content = initial_content.replace("Original body.", "Updated body.") + file_path.write_text(updated_content, encoding="utf-8") + + original_index_markdown_file = sync_service.batch_indexer.index_markdown_file + + async def index_markdown_file_spy(*args, **kwargs): + return await original_index_markdown_file(*args, **kwargs) + + index_markdown_file = AsyncMock(side_effect=index_markdown_file_spy) + index_entity_data = AsyncMock() + monkeypatch.setattr(sync_service.batch_indexer, "index_markdown_file", index_markdown_file) + monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data) + + result = await sync_service.sync_one_markdown_file("notes/changed.md") + + index_markdown_file.assert_awaited_once() + index_entity_data.assert_awaited_once() + assert result.entity.id == initial.entity.id + assert result.markdown_content == file_path.read_bytes().decode("utf-8") + assert result.checksum == await compute_checksum(file_path.read_bytes()) + assert result.checksum != initial.checksum + + @pytest.mark.asyncio async def test_sync_markdown_file_remains_tuple_compatible(sync_service, test_project): """The legacy tuple-returning API still works for existing callers."""