diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py index 5c399e6a..64d4abe0 100644 --- a/haiku_rag_slim/haiku/rag/client.py +++ b/haiku_rag_slim/haiku/rag/client.py @@ -219,9 +219,13 @@ class HaikuRAG: if title is not None and title != existing_doc.title: existing_doc.title = title updated = True - if metadata: - existing_doc.metadata = {**(existing_doc.metadata or {}), **metadata} + + # Check if metadata actually changed (beyond contentType and md5) + merged_metadata = {**(existing_doc.metadata or {}), **metadata} + if merged_metadata != existing_doc.metadata: + existing_doc.metadata = merged_metadata updated = True + if updated: return await self.document_repository.update(existing_doc) return existing_doc @@ -290,13 +294,14 @@ class HaikuRAG: if title is not None and title != existing_doc.title: existing_doc.title = title updated = True + metadata.update({"contentType": content_type, "md5": md5_hash}) - if metadata: - existing_doc.metadata = { - **(existing_doc.metadata or {}), - **metadata, - } + # Check if metadata actually changed (beyond contentType and md5) + merged_metadata = {**(existing_doc.metadata or {}), **metadata} + if merged_metadata != existing_doc.metadata: + existing_doc.metadata = merged_metadata updated = True + if updated: return await self.document_repository.update(existing_doc) return existing_doc diff --git a/haiku_rag_slim/haiku/rag/monitor.py b/haiku_rag_slim/haiku/rag/monitor.py index 55547365..5d77637f 100644 --- a/haiku_rag_slim/haiku/rag/monitor.py +++ b/haiku_rag_slim/haiku/rag/monitor.py @@ -113,18 +113,22 @@ class FileWatcher: try: uri = file.as_uri() existing_doc = await self.client.get_document_by_uri(uri) + + result = await self.client.create_document_from_source(str(file)) + doc = result if isinstance(result, Document) else result[0] + if existing_doc: - result = await self.client.create_document_from_source(str(file)) - # Since we're passing a file (not directory), result should be a single Document - doc = result if isinstance(result, Document) else result[0] - logger.info(f"Updated document {existing_doc.id} from {file}") - return doc + # Check if document was actually updated by comparing updated_at timestamps + if doc.updated_at > existing_doc.updated_at: + logger.info(f"Updated document {existing_doc.id} from {file}") + else: + logger.info( + f"Skipped unchanged document {existing_doc.id} from {file}" + ) else: - result = await self.client.create_document_from_source(str(file)) - # Since we're passing a file (not directory), result should be a single Document - doc = result if isinstance(result, Document) else result[0] logger.info(f"Created new document {doc.id} from {file}") - return doc + + return doc except Exception as e: logger.error(f"Failed to upsert document from {file}: {e}") return None diff --git a/tests/test_client.py b/tests/test_client.py index 99fa57f6..00a32df8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -485,6 +485,28 @@ async def test_client_create_update_no_op_behavior(temp_db_path): assert retrieved_doc.content == updated_content +@pytest.mark.asyncio +async def test_client_unchanged_file_keeps_timestamp(temp_db_path): + """Test that unchanged files don't update the updated_at timestamp.""" + async with HaikuRAG(temp_db_path) as client: + # Create a temporary file + test_content = "Test content for timestamp check." + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) / "test.txt" + temp_path.write_text(test_content) + + # First call - create document + doc1 = await client.create_document_from_source(temp_path) + assert isinstance(doc1, Document) + original_updated_at = doc1.updated_at + + # Second call with same content - should not update timestamp + doc2 = await client.create_document_from_source(temp_path) + assert isinstance(doc2, Document) + assert doc2.id == doc1.id + assert doc2.updated_at == original_updated_at # Timestamp should not change + + @pytest.mark.asyncio async def test_client_url_create_update_no_op_behavior(temp_db_path): """Test create/update/no-op behavior for URLs based on MD5 changes.""" diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 75ea64ee..0cd86ba6 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -243,3 +243,43 @@ async def test_file_watcher_with_include_patterns(): # Should have only called for the .md file assert mock_client.create_document_from_source.call_count == 1 mock_client.create_document_from_source.assert_called_with(str(md_file)) + + +@pytest.mark.asyncio +async def test_file_watcher_skips_unchanged_document(caplog): + """Test FileWatcher skips document when content hasn't changed.""" + from datetime import datetime + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + test_file = temp_path / "test.txt" + test_content = "Test content" + test_file.write_text(test_content) + + mock_client = AsyncMock(spec=HaikuRAG) + # Existing document with a timestamp + now = datetime.now() + existing_doc = Document( + id="1", + content=test_content, + uri=test_file.as_uri(), + created_at=now, + updated_at=now, + ) + mock_client.get_document_by_uri.return_value = existing_doc + # Client returns same document with same timestamp (unchanged) + mock_client.create_document_from_source.return_value = existing_doc + + test_config = AppConfig(monitor=MonitorConfig(directories=[temp_path])) + watcher = FileWatcher(client=mock_client, config=test_config) + + with caplog.at_level("INFO"): + result = await watcher._upsert_document(test_file) + + assert result is not None + assert result.id == "1" + # Should log that document was skipped, not updated + assert any("Skipped" in record.message for record in caplog.records) + assert not any( + "Updated document" in record.message for record in caplog.records + )