700 lines
No EOL
23 KiB
Python
700 lines
No EOL
23 KiB
Python
"""
|
|
Tests for file watcher system functionality.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock, Mock
|
|
|
|
from app.core.file_watcher import (
|
|
ProjectFileHandler,
|
|
ProjectWatcher,
|
|
GlobalFileWatcher,
|
|
get_global_watcher
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestProjectFileHandler:
|
|
"""Test ProjectFileHandler class."""
|
|
|
|
def test_file_handler_initialization(self):
|
|
"""Test file handler initialization."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
assert handler.project_id == 123
|
|
assert handler.mcp_type == "knowledge_docs"
|
|
assert handler.on_change == callback
|
|
assert handler.debounce_delay == 2.0
|
|
assert handler.debounce_timer is None
|
|
|
|
def test_on_modified_file(self):
|
|
"""Test handling file modification."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Create mock event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
|
|
handler.on_modified(mock_event)
|
|
|
|
# Should have a debounce timer
|
|
assert handler.debounce_timer is not None
|
|
assert isinstance(handler.debounce_timer, threading.Timer)
|
|
|
|
def test_on_created_file(self):
|
|
"""Test handling file creation."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Create mock event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
|
|
handler.on_created(mock_event)
|
|
|
|
# Should have a debounce timer
|
|
assert handler.debounce_timer is not None
|
|
|
|
def test_on_modified_directory_ignored(self):
|
|
"""Test that directory modifications are ignored."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Create mock directory event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = True
|
|
|
|
handler.on_modified(mock_event)
|
|
|
|
# Should not create timer for directories
|
|
assert handler.debounce_timer is None
|
|
|
|
def test_on_created_directory_ignored(self):
|
|
"""Test that directory creations are ignored."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Create mock directory event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = True
|
|
|
|
handler.on_created(mock_event)
|
|
|
|
# Should not create timer for directories
|
|
assert handler.debounce_timer is None
|
|
|
|
def test_debounce_sync_cancels_previous_timer(self):
|
|
"""Test that debounce cancels previous timer."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Create mock event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
|
|
# First event
|
|
handler.on_modified(mock_event)
|
|
first_timer = handler.debounce_timer
|
|
|
|
# Second event (should cancel first)
|
|
handler.on_modified(mock_event)
|
|
second_timer = handler.debounce_timer
|
|
|
|
# Timers should be different
|
|
assert first_timer is not second_timer
|
|
assert handler.debounce_timer is second_timer
|
|
|
|
@patch('threading.Timer')
|
|
def test_trigger_sync_callback(self, mock_timer_class):
|
|
"""Test that debounce timer triggers callback."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Mock timer
|
|
mock_timer = Mock()
|
|
mock_timer_class.return_value = mock_timer
|
|
|
|
# Trigger file event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
handler.on_modified(mock_event)
|
|
|
|
# Get the timer callback
|
|
timer_callback = mock_timer_class.call_args[0][0]
|
|
|
|
# Execute the callback
|
|
timer_callback()
|
|
|
|
# Should call the original callback
|
|
callback.assert_called_once()
|
|
|
|
@patch('threading.Timer')
|
|
def test_trigger_sync_error_handling(self, mock_timer_class):
|
|
"""Test error handling in sync callback."""
|
|
callback = Mock(side_effect=Exception("Sync error"))
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Mock timer
|
|
mock_timer = Mock()
|
|
mock_timer_class.return_value = mock_timer
|
|
|
|
# Trigger file event
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
handler.on_modified(mock_event)
|
|
|
|
# Get and execute timer callback
|
|
timer_callback = mock_timer_class.call_args[0][0]
|
|
|
|
# Should handle error gracefully
|
|
# In real implementation, this would be caught by logger
|
|
timer_callback()
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestProjectWatcher:
|
|
"""Test ProjectWatcher class."""
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_watcher_initialization(self, mock_observer_class, mock_hook_class):
|
|
"""Test project watcher initialization."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
|
|
assert watcher.project_id == project_id
|
|
assert watcher.hook == mock_hook
|
|
assert watcher.observer is None
|
|
assert watcher.event_handlers == {}
|
|
assert watcher.watched_paths == {}
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_start_with_no_hooks(self, mock_observer_class, mock_hook_class):
|
|
"""Test starting watcher with no hooks configured."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {"hooks": {}}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.start()
|
|
|
|
# Should not start observer
|
|
mock_observer_class.return_value.start.assert_not_called()
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_start_with_disabled_hooks(self, mock_observer_class, mock_hook_class):
|
|
"""Test starting watcher with disabled hooks."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {
|
|
"hooks": {
|
|
"knowledge_docs": {
|
|
"enabled": False,
|
|
"folder_path": "/path/to/docs"
|
|
}
|
|
}
|
|
}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.start()
|
|
|
|
# Should not start observer
|
|
mock_observer_class.return_value.start.assert_not_called()
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_start_with_enabled_hooks(self, mock_observer_class, mock_hook_class, tmp_path):
|
|
"""Test starting watcher with enabled hooks."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
|
|
# Create a real directory for testing
|
|
watch_dir = tmp_path / "watched"
|
|
watch_dir.mkdir()
|
|
|
|
mock_hook.get_hook_status.return_value = {
|
|
"hooks": {
|
|
"knowledge_docs": {
|
|
"enabled": True,
|
|
"folder_path": str(watch_dir)
|
|
}
|
|
}
|
|
}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
mock_observer = Mock()
|
|
mock_observer_class.return_value = mock_observer
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.start()
|
|
|
|
# Should start observer
|
|
mock_observer.start.assert_called_once()
|
|
|
|
# Should schedule watching
|
|
mock_observer.schedule.assert_called_once()
|
|
schedule_args = mock_observer.schedule.call_args
|
|
assert schedule_args[0][1] == str(watch_dir) # path
|
|
assert schedule_args[0][2] is True # recursive
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_start_with_nonexistent_folder(self, mock_observer_class, mock_hook_class):
|
|
"""Test starting watcher with non-existent folder."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {
|
|
"hooks": {
|
|
"knowledge_docs": {
|
|
"enabled": True,
|
|
"folder_path": "/nonexistent/path"
|
|
}
|
|
}
|
|
}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.start()
|
|
|
|
# Should not schedule watching for non-existent path
|
|
mock_observer_class.return_value.schedule.assert_not_called()
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_start_multiple_hooks(self, mock_observer_class, mock_hook_class, tmp_path):
|
|
"""Test starting watcher with multiple enabled hooks."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
|
|
# Create directories
|
|
docs_dir = tmp_path / "docs"
|
|
profile_dir = tmp_path / "profile"
|
|
docs_dir.mkdir()
|
|
profile_dir.mkdir()
|
|
|
|
mock_hook.get_hook_status.return_value = {
|
|
"hooks": {
|
|
"knowledge_docs": {
|
|
"enabled": True,
|
|
"folder_path": str(docs_dir)
|
|
},
|
|
"project_profile": {
|
|
"enabled": True,
|
|
"folder_path": str(profile_dir)
|
|
}
|
|
}
|
|
}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
mock_observer = Mock()
|
|
mock_observer_class.return_value = mock_observer
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.start()
|
|
|
|
# Should schedule both paths
|
|
assert mock_observer.schedule.call_count == 2
|
|
|
|
# Check event handlers were created
|
|
assert len(watcher.event_handlers) == 2
|
|
assert "knowledge_docs" in watcher.event_handlers
|
|
assert "project_profile" in watcher.event_handlers
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_stop_watcher(self, mock_observer_class, mock_hook_class):
|
|
"""Test stopping watcher."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {"hooks": {}}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
mock_observer = Mock()
|
|
mock_observer_class.return_value = mock_observer
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.observer = mock_observer
|
|
watcher.event_handlers["test"] = Mock()
|
|
watcher.watched_paths["test"] = "/path"
|
|
|
|
watcher.stop()
|
|
|
|
# Should stop observer
|
|
mock_observer.stop.assert_called_once()
|
|
mock_observer.join.assert_called_once()
|
|
|
|
# Should clear state
|
|
assert watcher.observer is None
|
|
assert len(watcher.event_handlers) == 0
|
|
assert len(watcher.watched_paths) == 0
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_restart_watcher(self, mock_observer_class, mock_hook_class):
|
|
"""Test restarting watcher."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {"hooks": {}}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
mock_observer = Mock()
|
|
mock_observer_class.return_value = mock_observer
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
watcher.observer = mock_observer
|
|
|
|
watcher.restart()
|
|
|
|
# Should stop and start
|
|
mock_observer.stop.assert_called_once()
|
|
mock_observer.join.assert_called_once()
|
|
mock_observer.start.assert_called_once()
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_on_folder_change(self, mock_observer_class, mock_hook_class):
|
|
"""Test folder change handling."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {"hooks": {}}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
|
|
# Mock sync result
|
|
mock_hook.sync_kb_folder.return_value = {
|
|
"synced_files": ["file1.txt", "file2.txt"],
|
|
"errors": []
|
|
}
|
|
|
|
watcher._on_folder_change("knowledge_docs")
|
|
|
|
# Should call sync
|
|
mock_hook.sync_kb_folder.assert_called_once_with("knowledge_docs")
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
@patch('app.core.file_watcher.Observer')
|
|
def test_on_folder_change_with_errors(self, mock_observer_class, mock_hook_class):
|
|
"""Test folder change handling with sync errors."""
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {"hooks": {}}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
|
|
# Mock sync result with errors
|
|
mock_hook.sync_kb_folder.return_value = {
|
|
"synced_files": ["file1.txt"],
|
|
"errors": [{"file": "file2.txt", "error": "Permission denied"}]
|
|
}
|
|
|
|
watcher._on_folder_change("knowledge_docs")
|
|
|
|
# Should still call sync despite errors
|
|
mock_hook.sync_kb_folder.assert_called_once_with("knowledge_docs")
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestGlobalFileWatcher:
|
|
"""Test GlobalFileWatcher class."""
|
|
|
|
def test_global_watcher_initialization(self):
|
|
"""Test global watcher initialization."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
assert global_watcher.watchers == {}
|
|
assert global_watcher.enabled is False
|
|
assert hasattr(global_watcher, 'lock')
|
|
|
|
def test_start_project(self):
|
|
"""Test starting to watch a project."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw = Mock()
|
|
mock_pw_class.return_value = mock_pw
|
|
|
|
global_watcher.start_project(123)
|
|
|
|
assert 123 in global_watcher.watchers
|
|
assert global_watcher.watchers[123] == mock_pw
|
|
mock_pw.start.assert_called_once()
|
|
|
|
def test_start_project_already_exists(self):
|
|
"""Test starting project that's already being watched."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw1 = Mock()
|
|
mock_pw2 = Mock()
|
|
mock_pw_class.side_effect = [mock_pw1, mock_pw2]
|
|
|
|
# Start project twice
|
|
global_watcher.start_project(123)
|
|
global_watcher.start_project(123)
|
|
|
|
# Should still only have one watcher
|
|
assert len(global_watcher.watchers) == 1
|
|
assert global_watcher.watchers[123] == mock_pw1
|
|
mock_pw2.start.assert_not_called()
|
|
|
|
def test_stop_project(self):
|
|
"""Test stopping watching a project."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw = Mock()
|
|
mock_pw_class.return_value = mock_pw
|
|
|
|
# Start then stop
|
|
global_watcher.start_project(123)
|
|
global_watcher.stop_project(123)
|
|
|
|
assert 123 not in global_watcher.watchers
|
|
mock_pw.stop.assert_called_once()
|
|
|
|
def test_stop_project_not_watched(self):
|
|
"""Test stopping project that's not being watched."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
# Should not raise error
|
|
global_watcher.stop_project(123)
|
|
assert 123 not in global_watcher.watchers
|
|
|
|
def test_restart_project(self):
|
|
"""Test restarting project watcher."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw = Mock()
|
|
mock_pw_class.return_value = mock_pw
|
|
|
|
# Start then restart
|
|
global_watcher.start_project(123)
|
|
global_watcher.restart_project(123)
|
|
|
|
mock_pw.restart.assert_called_once()
|
|
|
|
def test_restart_project_not_watched(self):
|
|
"""Test restarting project that's not being watched."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw = Mock()
|
|
mock_pw_class.return_value = mock_pw
|
|
|
|
global_watcher.restart_project(123)
|
|
|
|
# Should start the project
|
|
mock_pw.start.assert_called_once()
|
|
assert 123 in global_watcher.watchers
|
|
|
|
def test_start_all_projects(self):
|
|
"""Test starting all projects."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw1 = Mock()
|
|
mock_pw2 = Mock()
|
|
mock_pw3 = Mock()
|
|
mock_pw_class.side_effect = [mock_pw1, mock_pw2, mock_pw3]
|
|
|
|
project_ids = [123, 456, 789]
|
|
global_watcher.start_all(project_ids)
|
|
|
|
assert len(global_watcher.watchers) == 3
|
|
assert 123 in global_watcher.watchers
|
|
assert 456 in global_watcher.watchers
|
|
assert 789 in global_watcher.watchers
|
|
|
|
mock_pw1.start.assert_called_once()
|
|
mock_pw2.start.assert_called_once()
|
|
mock_pw3.start.assert_called_once()
|
|
|
|
assert global_watcher.enabled is True
|
|
|
|
def test_start_all_projects_with_errors(self):
|
|
"""Test starting all projects with some errors."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw1 = Mock()
|
|
mock_pw2 = Mock()
|
|
mock_pw3 = Mock()
|
|
|
|
# Make second watcher fail
|
|
mock_pw2.start.side_effect = Exception("Start failed")
|
|
mock_pw_class.side_effect = [mock_pw1, mock_pw2, mock_pw3]
|
|
|
|
project_ids = [123, 456, 789]
|
|
global_watcher.start_all(project_ids)
|
|
|
|
# Should still start others
|
|
assert len(global_watcher.watchers) == 2 # Only 2 succeeded
|
|
assert 123 in global_watcher.watchers
|
|
assert 789 in global_watcher.watchers
|
|
assert 456 not in global_watcher.watchers
|
|
|
|
def test_stop_all(self):
|
|
"""Test stopping all project watchers."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw1 = Mock()
|
|
mock_pw2 = Mock()
|
|
mock_pw_class.side_effect = [mock_pw1, mock_pw2]
|
|
|
|
# Start some projects
|
|
global_watcher.start_project(123)
|
|
global_watcher.start_project(456)
|
|
|
|
# Stop all
|
|
global_watcher.stop_all()
|
|
|
|
assert len(global_watcher.watchers) == 0
|
|
assert global_watcher.enabled is False
|
|
|
|
mock_pw1.stop.assert_called_once()
|
|
mock_pw2.stop.assert_called_once()
|
|
|
|
def test_get_status(self):
|
|
"""Test getting global watcher status."""
|
|
global_watcher = GlobalFileWatcher()
|
|
|
|
with patch('app.core.file_watcher.ProjectWatcher') as mock_pw_class:
|
|
mock_pw1 = Mock()
|
|
mock_pw2 = Mock()
|
|
mock_pw1.watched_paths = {"docs": "/path/docs"}
|
|
mock_pw1.event_handlers = {"docs": "handler1"}
|
|
mock_pw2.watched_paths = {"profile": "/path/profile"}
|
|
mock_pw2.event_handlers = {"profile": "handler2"}
|
|
mock_pw_class.side_effect = [mock_pw1, mock_pw2]
|
|
|
|
# Start projects
|
|
global_watcher.start_project(123)
|
|
global_watcher.start_project(456)
|
|
|
|
status = global_watcher.get_status()
|
|
|
|
assert status["enabled"] is True
|
|
assert status["projects_watched"] == 2
|
|
assert "123" in status["projects"]
|
|
assert "456" in status["projects"]
|
|
assert status["projects"]["123"]["watched_paths"] == {"docs": "/path/docs"}
|
|
assert status["projects"]["123"]["event_handlers"] == ["docs"]
|
|
assert status["projects"]["456"]["watched_paths"] == {"profile": "/path/profile"}
|
|
assert status["projects"]["456"]["event_handlers"] == ["profile"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFileWatcherUtility:
|
|
"""Test file watcher utility functions."""
|
|
|
|
def test_get_global_watcher_singleton(self):
|
|
"""Test that get_global_watcher returns singleton."""
|
|
watcher1 = get_global_watcher()
|
|
watcher2 = get_global_watcher()
|
|
|
|
# Should be the same instance
|
|
assert watcher1 is watcher2
|
|
|
|
def test_get_global_watcher_type(self):
|
|
"""Test that get_global_watcher returns correct type."""
|
|
watcher = get_global_watcher()
|
|
|
|
assert isinstance(watcher, GlobalFileWatcher)
|
|
|
|
@patch('app.core.file_watcher.GlobalFileWatcher')
|
|
def test_get_global_watcher_creates_instance(self, mock_gfw_class):
|
|
"""Test that get_global_watcher creates instance on first call."""
|
|
mock_instance = Mock()
|
|
mock_gfw_class.return_value = mock_instance
|
|
|
|
# Clear any existing instance
|
|
import app.core.file_watcher
|
|
app.core.file_watcher._global_watcher = None
|
|
|
|
watcher1 = get_global_watcher()
|
|
watcher2 = get_global_watcher()
|
|
|
|
# Should create instance only once
|
|
mock_gfw_class.assert_called_once()
|
|
assert watcher1 is mock_instance
|
|
assert watcher2 is mock_instance
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestFileWatcherIntegration:
|
|
"""Integration tests for file watcher system."""
|
|
|
|
@patch('app.core.file_watcher.ProjectHook')
|
|
def test_file_change_detection(self, mock_hook_class, tmp_path):
|
|
"""Test actual file change detection."""
|
|
# This test would require real file system watching
|
|
# For now, we'll test the integration points
|
|
|
|
project_id = 123
|
|
mock_hook = Mock()
|
|
mock_hook.get_hook_status.return_value = {
|
|
"hooks": {
|
|
"knowledge_docs": {
|
|
"enabled": True,
|
|
"folder_path": str(tmp_path)
|
|
}
|
|
}
|
|
}
|
|
mock_hook_class.return_value = mock_hook
|
|
|
|
watcher = ProjectWatcher(project_id)
|
|
|
|
# Test that the handler is created correctly
|
|
assert "knowledge_docs" in watcher.event_handlers
|
|
handler = watcher.event_handlers["knowledge_docs"]
|
|
|
|
assert handler.project_id == project_id
|
|
assert handler.mcp_type == "knowledge_docs"
|
|
assert callable(handler.on_change)
|
|
|
|
def test_debounce_timing(self):
|
|
"""Test debounce timing behavior."""
|
|
callback = Mock()
|
|
handler = ProjectFileHandler(123, "knowledge_docs", callback)
|
|
|
|
# Mock timer to track calls
|
|
with patch('threading.Timer') as mock_timer_class:
|
|
mock_timer = Mock()
|
|
mock_timer_class.return_value = mock_timer
|
|
|
|
# Trigger multiple rapid events
|
|
mock_event = Mock()
|
|
mock_event.is_directory = False
|
|
|
|
handler.on_modified(mock_event)
|
|
first_call_args = mock_timer_class.call_args
|
|
first_callback = first_call_args[0][0]
|
|
|
|
handler.on_modified(mock_event)
|
|
second_call_args = mock_timer_class.call_args
|
|
second_callback = second_call_args[0][0]
|
|
|
|
# Should have created two timers
|
|
assert mock_timer_class.call_count == 2
|
|
|
|
# First timer should be cancelled (implicitly by creating new one)
|
|
# In real implementation, the old timer would be cancelled
|
|
assert first_callback is not None
|
|
assert second_callback is not None |