mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(codex-lens): add unified reranker architecture and file watcher
Unified Reranker Architecture: - Add BaseReranker ABC with factory pattern - Implement 4 backends: ONNX (default), API, LiteLLM, Legacy - Add .env configuration parsing for API credentials - Migrate from sentence-transformers to optimum+onnxruntime File Watcher Module: - Add real-time file system monitoring with watchdog - Implement IncrementalIndexer for single-file updates - Add WatcherManager with signal handling and graceful shutdown - Add 'codexlens watch' CLI command - Event filtering, debouncing, and deduplication - Thread-safe design with proper resource cleanup Tests: 16 watcher tests + 5 reranker test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
codex-lens/tests/test_watcher/__init__.py
Normal file
1
codex-lens/tests/test_watcher/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for watcher module."""
|
||||
43
codex-lens/tests/test_watcher/conftest.py
Normal file
43
codex-lens/tests/test_watcher/conftest.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Fixtures for watcher tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_project() -> Generator[Path, None, None]:
|
||||
"""Create a temporary project directory with sample files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project = Path(tmpdir)
|
||||
|
||||
# Create sample Python file
|
||||
py_file = project / "main.py"
|
||||
py_file.write_text("def hello():\n print('Hello')\n")
|
||||
|
||||
# Create sample JavaScript file
|
||||
js_file = project / "app.js"
|
||||
js_file.write_text("function greet() {\n console.log('Hi');\n}\n")
|
||||
|
||||
# Create subdirectory with file
|
||||
sub_dir = project / "src"
|
||||
sub_dir.mkdir()
|
||||
(sub_dir / "utils.py").write_text("def add(a, b):\n return a + b\n")
|
||||
|
||||
# Create ignored directory
|
||||
git_dir = project / ".git"
|
||||
git_dir.mkdir()
|
||||
(git_dir / "config").write_text("[core]\n")
|
||||
|
||||
yield project
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def watcher_config():
|
||||
"""Create default watcher configuration."""
|
||||
from codexlens.watcher import WatcherConfig
|
||||
return WatcherConfig(debounce_ms=100) # Short debounce for tests
|
||||
103
codex-lens/tests/test_watcher/test_events.py
Normal file
103
codex-lens/tests/test_watcher/test_events.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Tests for watcher event types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from codexlens.watcher import ChangeType, FileEvent, WatcherConfig, IndexResult, WatcherStats
|
||||
|
||||
|
||||
class TestChangeType:
|
||||
"""Tests for ChangeType enum."""
|
||||
|
||||
def test_change_types_exist(self):
|
||||
"""Verify all change types are defined."""
|
||||
assert ChangeType.CREATED.value == "created"
|
||||
assert ChangeType.MODIFIED.value == "modified"
|
||||
assert ChangeType.DELETED.value == "deleted"
|
||||
assert ChangeType.MOVED.value == "moved"
|
||||
|
||||
def test_change_type_count(self):
|
||||
"""Verify we have exactly 4 change types."""
|
||||
assert len(ChangeType) == 4
|
||||
|
||||
|
||||
class TestFileEvent:
|
||||
"""Tests for FileEvent dataclass."""
|
||||
|
||||
def test_create_event(self):
|
||||
"""Test creating a file event."""
|
||||
event = FileEvent(
|
||||
path=Path("/test/file.py"),
|
||||
change_type=ChangeType.CREATED,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
assert event.path == Path("/test/file.py")
|
||||
assert event.change_type == ChangeType.CREATED
|
||||
assert event.old_path is None
|
||||
|
||||
def test_moved_event(self):
|
||||
"""Test creating a moved event with old_path."""
|
||||
event = FileEvent(
|
||||
path=Path("/test/new.py"),
|
||||
change_type=ChangeType.MOVED,
|
||||
timestamp=time.time(),
|
||||
old_path=Path("/test/old.py"),
|
||||
)
|
||||
assert event.old_path == Path("/test/old.py")
|
||||
|
||||
|
||||
class TestWatcherConfig:
|
||||
"""Tests for WatcherConfig dataclass."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Test default configuration values."""
|
||||
config = WatcherConfig()
|
||||
assert config.debounce_ms == 1000
|
||||
assert ".git" in config.ignored_patterns
|
||||
assert "node_modules" in config.ignored_patterns
|
||||
assert "__pycache__" in config.ignored_patterns
|
||||
assert config.languages is None
|
||||
|
||||
def test_custom_debounce(self):
|
||||
"""Test custom debounce setting."""
|
||||
config = WatcherConfig(debounce_ms=500)
|
||||
assert config.debounce_ms == 500
|
||||
|
||||
|
||||
class TestIndexResult:
|
||||
"""Tests for IndexResult dataclass."""
|
||||
|
||||
def test_default_result(self):
|
||||
"""Test default result values."""
|
||||
result = IndexResult()
|
||||
assert result.files_indexed == 0
|
||||
assert result.files_removed == 0
|
||||
assert result.symbols_added == 0
|
||||
assert result.errors == []
|
||||
|
||||
def test_custom_result(self):
|
||||
"""Test creating result with values."""
|
||||
result = IndexResult(
|
||||
files_indexed=5,
|
||||
files_removed=2,
|
||||
symbols_added=50,
|
||||
errors=["error1"],
|
||||
)
|
||||
assert result.files_indexed == 5
|
||||
assert result.files_removed == 2
|
||||
|
||||
|
||||
class TestWatcherStats:
|
||||
"""Tests for WatcherStats dataclass."""
|
||||
|
||||
def test_default_stats(self):
|
||||
"""Test default stats values."""
|
||||
stats = WatcherStats()
|
||||
assert stats.files_watched == 0
|
||||
assert stats.events_processed == 0
|
||||
assert stats.last_event_time is None
|
||||
assert stats.is_running is False
|
||||
124
codex-lens/tests/test_watcher/test_file_watcher.py
Normal file
124
codex-lens/tests/test_watcher/test_file_watcher.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Tests for FileWatcher class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from codexlens.watcher import FileWatcher, WatcherConfig, FileEvent, ChangeType
|
||||
|
||||
|
||||
class TestFileWatcherInit:
|
||||
"""Tests for FileWatcher initialization."""
|
||||
|
||||
def test_init_with_valid_path(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test initializing with valid path."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
assert watcher.root_path == temp_project.resolve()
|
||||
assert watcher.config == watcher_config
|
||||
assert not watcher.is_running
|
||||
|
||||
def test_start_with_invalid_path(self, watcher_config: WatcherConfig):
|
||||
"""Test starting watcher with non-existent path."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(Path("/nonexistent/path"), watcher_config, lambda e: events.extend(e))
|
||||
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
watcher.start()
|
||||
|
||||
|
||||
class TestFileWatcherLifecycle:
|
||||
"""Tests for FileWatcher start/stop lifecycle."""
|
||||
|
||||
def test_start_stop(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test basic start and stop."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
watcher.start()
|
||||
assert watcher.is_running
|
||||
|
||||
watcher.stop()
|
||||
assert not watcher.is_running
|
||||
|
||||
def test_double_start(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test calling start twice."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
watcher.start()
|
||||
watcher.start() # Should not raise
|
||||
assert watcher.is_running
|
||||
|
||||
watcher.stop()
|
||||
|
||||
def test_double_stop(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test calling stop twice."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
watcher.start()
|
||||
watcher.stop()
|
||||
watcher.stop() # Should not raise
|
||||
assert not watcher.is_running
|
||||
|
||||
|
||||
class TestFileWatcherEvents:
|
||||
"""Tests for FileWatcher event detection."""
|
||||
|
||||
def test_detect_file_creation(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test detecting new file creation."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
try:
|
||||
watcher.start()
|
||||
time.sleep(0.3) # Let watcher start (longer for Windows)
|
||||
|
||||
# Create new file
|
||||
new_file = temp_project / "new_file.py"
|
||||
new_file.write_text("# New file\n")
|
||||
|
||||
# Wait for event with retries (watchdog timing varies by platform)
|
||||
max_wait = 2.0
|
||||
waited = 0.0
|
||||
while waited < max_wait:
|
||||
time.sleep(0.2)
|
||||
waited += 0.2
|
||||
# Windows may report MODIFIED instead of CREATED
|
||||
file_events = [e for e in events if e.change_type in (ChangeType.CREATED, ChangeType.MODIFIED)]
|
||||
if any(e.path.name == "new_file.py" for e in file_events):
|
||||
break
|
||||
|
||||
# Check event was detected (Windows may report MODIFIED instead of CREATED)
|
||||
relevant_events = [e for e in events if e.change_type in (ChangeType.CREATED, ChangeType.MODIFIED)]
|
||||
assert len(relevant_events) >= 1, f"Expected file event, got: {events}"
|
||||
assert any(e.path.name == "new_file.py" for e in relevant_events)
|
||||
finally:
|
||||
watcher.stop()
|
||||
|
||||
def test_filter_ignored_directories(self, temp_project: Path, watcher_config: WatcherConfig):
|
||||
"""Test that files in ignored directories are filtered."""
|
||||
events: List[FileEvent] = []
|
||||
watcher = FileWatcher(temp_project, watcher_config, lambda e: events.extend(e))
|
||||
|
||||
try:
|
||||
watcher.start()
|
||||
time.sleep(0.1)
|
||||
|
||||
# Create file in .git (should be ignored)
|
||||
git_file = temp_project / ".git" / "test.py"
|
||||
git_file.write_text("# In git\n")
|
||||
|
||||
time.sleep(watcher_config.debounce_ms / 1000.0 + 0.2)
|
||||
|
||||
# No events should be detected for .git files
|
||||
git_events = [e for e in events if ".git" in str(e.path)]
|
||||
assert len(git_events) == 0
|
||||
finally:
|
||||
watcher.stop()
|
||||
Reference in New Issue
Block a user