fix: Use filesystem timestamps for entity sync instead of database operation time (#138) (#369)

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
Paul Hernandez
2025-10-16 14:21:27 -05:00
committed by GitHub
parent d1431bdb1b
commit b7497d7484
3 changed files with 101 additions and 5 deletions
+16 -3
View File
@@ -547,6 +547,11 @@ class SyncService:
file_content = await self._read_file_async(file_path)
file_contains_frontmatter = has_frontmatter(file_content)
# Get file timestamps for tracking modification times
file_stats = self.file_service.file_stats(path)
created = datetime.fromtimestamp(file_stats.st_ctime).astimezone()
modified = datetime.fromtimestamp(file_stats.st_mtime).astimezone()
# entity markdown will always contain front matter, so it can be used up create/update the entity
entity_markdown = await self.entity_parser.parse_file(path)
@@ -585,8 +590,11 @@ class SyncService:
# after relation processing is complete
final_checksum = await self._compute_checksum_async(path)
# set checksum
await self.entity_repository.update(entity.id, {"checksum": final_checksum})
# Update checksum and timestamps from file system
# This ensures temporal ordering in search and recent activity uses actual file modification times
await self.entity_repository.update(
entity.id, {"checksum": final_checksum, "created_at": created, "updated_at": modified}
)
logger.debug(
f"Markdown sync completed: path={path}, entity_id={entity.id}, "
@@ -659,13 +667,18 @@ class SyncService:
# Re-raise if it's a different integrity error
raise
else:
# Get file timestamps for updating modification time
file_stats = self.file_service.file_stats(path)
modified = datetime.fromtimestamp(file_stats.st_mtime).astimezone()
entity = await self.entity_repository.get_by_file_path(path)
if entity is None: # pragma: no cover
logger.error(f"Entity not found for existing file, path={path}")
raise ValueError(f"Entity not found for existing file: {path}")
# Update checksum and modification time from file system
updated = await self.entity_repository.update(
entity.id, {"file_path": path, "checksum": checksum}
entity.id, {"file_path": path, "checksum": checksum, "updated_at": modified}
)
if updated is None: # pragma: no cover
@@ -1,7 +1,6 @@
"""Tests for entity parser error handling (issues #184 and #185)."""
import pytest
from pathlib import Path
from textwrap import dedent
from basic_memory.markdown.entity_parser import EntityParser
@@ -213,4 +212,4 @@ async def test_parse_valid_file_still_works(tmp_path):
assert result is not None
assert result.frontmatter.title == "Valid File"
assert result.frontmatter.type == "knowledge"
assert result.frontmatter.tags == ["test", "valid"]
assert result.frontmatter.tags == ["test", "valid"]
+84
View File
@@ -645,6 +645,90 @@ Testing file timestamps
assert abs(entity_updated_epoch - file_stats.st_mtime) < tolerance # Allow tolerance difference
@pytest.mark.asyncio
async def test_sync_updates_timestamps_on_file_modification(
sync_service: SyncService,
project_config: ProjectConfig,
entity_service: EntityService,
):
"""Test that sync updates entity timestamps when files are modified.
This test specifically validates that when an existing file is modified and re-synced,
the updated_at timestamp in the database reflects the file's actual modification time,
not the database operation time. This is critical for accurate temporal ordering in
search and recent_activity queries.
"""
import time
project_dir = project_config.home
# Create initial file
initial_content = """
---
type: knowledge
---
# Test File
Initial content for timestamp test
"""
file_path = project_dir / "timestamp_test.md"
await create_test_file(file_path, initial_content)
# Initial sync
await sync_service.sync(project_config.home)
# Get initial entity and timestamps
entity_before = await entity_service.get_by_permalink("timestamp-test")
initial_updated_at = entity_before.updated_at
# Wait a bit to ensure filesystem timestamp changes
time.sleep(0.1)
# Modify the file content
modified_content = """
---
type: knowledge
---
# Test File
Modified content for timestamp test
## Observations
- [test] This was modified
"""
file_path.write_text(modified_content)
# Wait to ensure mtime is different
time.sleep(0.1)
# Get the file's modification time after our changes
file_stats_after_modification = file_path.stat()
# Re-sync the modified file
await sync_service.sync(project_config.home)
# Get entity after re-sync
entity_after = await entity_service.get_by_permalink("timestamp-test")
# Verify that updated_at changed
assert entity_after.updated_at != initial_updated_at, (
"updated_at should change when file is modified"
)
# Verify that updated_at matches the file's modification time, not db operation time
entity_updated_epoch = entity_after.updated_at.timestamp()
file_mtime = file_stats_after_modification.st_mtime
# Allow 2s difference on Windows due to filesystem timing precision
tolerance = 2 if os.name == "nt" else 1
assert abs(entity_updated_epoch - file_mtime) < tolerance, (
f"Entity updated_at ({entity_after.updated_at}) should match file mtime "
f"({datetime.fromtimestamp(file_mtime)}) within {tolerance}s tolerance"
)
# Verify the content was actually updated
assert len(entity_after.observations) == 1
assert entity_after.observations[0].content == "This was modified"
@pytest.mark.asyncio
async def test_file_move_updates_search_index(
sync_service: SyncService,