import tempfile from pathlib import Path from unittest.mock import AsyncMock import pytest from haiku.rag.client import HaikuRAG from haiku.rag.config import AppConfig, MonitorConfig from haiku.rag.monitor import FileWatcher from haiku.rag.store.models.document import Document @pytest.mark.asyncio async def test_file_watcher_upsert_document(): """Test FileWatcher._upsert_document method.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) / "test.txt" temp_path.write_text("Test content for file watcher") mock_client = AsyncMock(spec=HaikuRAG) mock_doc = Document(id="1", content="Test content", uri=temp_path.as_uri()) mock_client.create_document_from_source.return_value = mock_doc mock_client.get_document_by_uri.return_value = None # No existing document test_config = AppConfig(monitor=MonitorConfig(directories=[temp_path.parent])) watcher = FileWatcher(client=mock_client, config=test_config) result = await watcher._upsert_document(temp_path) assert result is not None assert result.id == "1" mock_client.get_document_by_uri.assert_called_once_with(temp_path.as_uri()) mock_client.create_document_from_source.assert_called_once_with(str(temp_path)) @pytest.mark.asyncio async def test_file_watcher_upsert_existing_document(): """Test FileWatcher._upsert_document with existing document.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) / "test.txt" temp_path.write_text("Test content for file watcher") mock_client = AsyncMock(spec=HaikuRAG) existing_doc = Document(id="1", content="Old content", uri=temp_path.as_uri()) updated_doc = Document( id="1", content="Updated content", uri=temp_path.as_uri() ) mock_client.get_document_by_uri.return_value = existing_doc mock_client.create_document_from_source.return_value = updated_doc test_config = AppConfig(monitor=MonitorConfig(directories=[temp_path.parent])) watcher = FileWatcher(client=mock_client, config=test_config) result = await watcher._upsert_document(temp_path) assert result is not None assert result.content == "Updated content" mock_client.get_document_by_uri.assert_called_once_with(temp_path.as_uri()) mock_client.create_document_from_source.assert_called_once_with(str(temp_path)) @pytest.mark.asyncio async def test_file_watcher_delete_document(): """Test FileWatcher._delete_document method.""" temp_path = Path("/tmp/test_file.txt") mock_client = AsyncMock(spec=HaikuRAG) existing_doc = Document(id="1", content="Content to delete", uri=temp_path.as_uri()) mock_client.get_document_by_uri.return_value = existing_doc mock_client.delete_document.return_value = True test_config = AppConfig(monitor=MonitorConfig(directories=[temp_path.parent])) watcher = FileWatcher(client=mock_client, config=test_config) await watcher._delete_document(temp_path) mock_client.get_document_by_uri.assert_called_once_with(temp_path.as_uri()) mock_client.delete_document.assert_called_once_with("1") @pytest.mark.asyncio async def test_file_watcher_delete_nonexistent_document(): """Test FileWatcher._delete_document with non-existent document.""" temp_path = Path("/tmp/nonexistent_file.txt") mock_client = AsyncMock(spec=HaikuRAG) mock_client.get_document_by_uri.return_value = None test_config = AppConfig(monitor=MonitorConfig(directories=[temp_path.parent])) watcher = FileWatcher(client=mock_client, config=test_config) await watcher._delete_document(temp_path) mock_client.get_document_by_uri.assert_called_once_with(temp_path.as_uri()) mock_client.delete_document.assert_not_called() @pytest.mark.asyncio async def test_file_filter_ignore_patterns(): """Test FileFilter with ignore patterns.""" from watchfiles import Change from haiku.rag.monitor import FileFilter filter = FileFilter(ignore_patterns=["*draft*.md", "temp/", "**/archive/**"]) # Should ignore draft markdown files assert not filter(Change.added, "/path/to/draft-post.md") # Should ignore files in temp/ directory assert not filter(Change.added, "/path/temp/notes.txt") # Should ignore files in archive directories assert not filter(Change.added, "/path/to/archive/old.pdf") # Should NOT ignore regular markdown files assert filter(Change.added, "/path/to/readme.md") # Should NOT ignore files outside temp/ assert filter(Change.added, "/path/to/notes.txt") @pytest.mark.asyncio async def test_file_filter_include_patterns(): """Test FileFilter with include patterns (whitelist mode).""" from watchfiles import Change from haiku.rag.monitor import FileFilter filter = FileFilter(include_patterns=["*.md", "**/docs/**"]) # Should include .md files assert filter(Change.added, "/path/to/file.md") # Should include files in docs/ directory assert filter(Change.added, "/path/to/docs/guide.txt") # Should NOT include .txt files outside docs/ assert not filter(Change.added, "/path/to/file.txt") @pytest.mark.asyncio async def test_file_filter_combined_patterns(): """Test FileFilter with both include and ignore patterns.""" from watchfiles import Change from haiku.rag.monitor import FileFilter # Include all markdown files, but ignore drafts filter = FileFilter( include_patterns=["*.md"], ignore_patterns=["*draft*.md", "archive/"] ) # Should include regular .md files assert filter(Change.added, "/path/to/readme.md") # Should ignore draft .md files (ignore takes precedence after include) assert not filter(Change.added, "/path/to/draft-post.md") # Should ignore .md files in archive/ directory assert not filter(Change.added, "/path/archive/old.md") # Should NOT include .txt files (not in include patterns) assert not filter(Change.added, "/path/to/file.txt") @pytest.mark.asyncio async def test_file_filter_extension_check(): """Test that FileFilter still respects extension filtering.""" from watchfiles import Change from haiku.rag.monitor import FileFilter filter = FileFilter() # Should include files with supported extensions assert filter(Change.added, "/path/to/document.pdf") assert filter(Change.added, "/path/to/notes.md") # Should not include files with unsupported extensions assert not filter(Change.added, "/path/to/file.xyz") assert not filter(Change.added, "/path/to/binary.bin") @pytest.mark.asyncio async def test_file_watcher_with_ignore_patterns(): """Test FileWatcher respects ignore patterns from config.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) draft_file = temp_path / "draft.md" readme_file = temp_path / "readme.md" draft_file.write_text("Draft content") readme_file.write_text("Readme content") mock_client = AsyncMock(spec=HaikuRAG) mock_doc = Document(id="1", content="Readme", uri=readme_file.as_uri()) mock_client.create_document_from_source.return_value = mock_doc mock_client.get_document_by_uri.return_value = None test_config = AppConfig( monitor=MonitorConfig(directories=[temp_path], ignore_patterns=["draft*"]) ) watcher = FileWatcher(client=mock_client, config=test_config) # Run refresh which should only process readme.md, not draft.md await watcher.refresh() # Should have only called for the readme file assert mock_client.create_document_from_source.call_count == 1 mock_client.create_document_from_source.assert_called_with(str(readme_file)) @pytest.mark.asyncio async def test_file_watcher_with_include_patterns(): """Test FileWatcher respects include patterns from config.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) md_file = temp_path / "readme.md" pdf_file = temp_path / "document.pdf" py_file = temp_path / "script.py" md_file.write_text("Markdown content") pdf_file.write_text("PDF content") py_file.write_text("Python content") mock_client = AsyncMock(spec=HaikuRAG) mock_doc = Document(id="1", content="Markdown", uri=md_file.as_uri()) mock_client.create_document_from_source.return_value = mock_doc mock_client.get_document_by_uri.return_value = None test_config = AppConfig( monitor=MonitorConfig(directories=[temp_path], include_patterns=["*.md"]) ) watcher = FileWatcher(client=mock_client, config=test_config) # Run refresh which should only process .md file, not .pdf or .py await watcher.refresh() # 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))