Files
catlog22 f14418603a feat(cli): 添加 --rule 选项支持模板自动发现
重构 ccw cli 模板系统:

- 新增 template-discovery.ts 模块,支持扁平化模板自动发现
- 添加 --rule <template> 选项,自动加载 protocol 和 template
- 模板目录从嵌套结构 (prompts/category/file.txt) 迁移到扁平结构 (prompts/category-function.txt)
- 更新所有 agent/command 文件,使用 $PROTO $TMPL 环境变量替代 $(cat ...) 模式
- 支持模糊匹配:--rule 02-review-architecture 可匹配 analysis-review-architecture.txt

其他更新:
- Dashboard: 添加 Claude Manager 和 Issue Manager 页面
- Codex-lens: 增强 chain_search 和 clustering 模块

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 19:20:24 +08:00

478 lines
15 KiB
Python

"""Tests for hover provider."""
from __future__ import annotations
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock
import tempfile
from codexlens.entities import Symbol
class TestHoverInfo:
"""Test HoverInfo dataclass."""
def test_hover_info_import(self):
"""HoverInfo can be imported."""
pytest.importorskip("pygls")
pytest.importorskip("lsprotocol")
from codexlens.lsp.providers import HoverInfo
assert HoverInfo is not None
def test_hover_info_fields(self):
"""HoverInfo has all required fields."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo
info = HoverInfo(
name="my_function",
kind="function",
signature="def my_function(x: int) -> str:",
documentation="A test function.",
file_path="/test/file.py",
line_range=(10, 15),
)
assert info.name == "my_function"
assert info.kind == "function"
assert info.signature == "def my_function(x: int) -> str:"
assert info.documentation == "A test function."
assert info.file_path == "/test/file.py"
assert info.line_range == (10, 15)
def test_hover_info_optional_documentation(self):
"""Documentation can be None."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo
info = HoverInfo(
name="func",
kind="function",
signature="def func():",
documentation=None,
file_path="/test.py",
line_range=(1, 2),
)
assert info.documentation is None
class TestHoverProvider:
"""Test HoverProvider class."""
def test_provider_import(self):
"""HoverProvider can be imported."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
assert HoverProvider is not None
def test_returns_none_for_unknown_symbol(self):
"""Returns None when symbol not found."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
mock_index = Mock()
mock_index.search.return_value = []
mock_registry = Mock()
provider = HoverProvider(mock_index, mock_registry)
result = provider.get_hover_info("unknown_symbol")
assert result is None
mock_index.search.assert_called_once_with(
name="unknown_symbol", limit=1, prefix_mode=False
)
def test_returns_none_for_non_exact_match(self):
"""Returns None when search returns non-exact matches."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
# Return a symbol with different name (prefix match but not exact)
mock_symbol = Mock()
mock_symbol.name = "my_function_extended"
mock_symbol.kind = "function"
mock_symbol.file = "/test/file.py"
mock_symbol.range = (10, 15)
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
mock_registry = Mock()
provider = HoverProvider(mock_index, mock_registry)
result = provider.get_hover_info("my_function")
assert result is None
def test_returns_hover_info_for_known_symbol(self):
"""Returns HoverInfo for found symbol."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
mock_symbol = Mock()
mock_symbol.name = "my_func"
mock_symbol.kind = "function"
mock_symbol.file = None # No file, will use fallback signature
mock_symbol.range = (10, 15)
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
mock_registry = Mock()
provider = HoverProvider(mock_index, mock_registry)
result = provider.get_hover_info("my_func")
assert result is not None
assert result.name == "my_func"
assert result.kind == "function"
assert result.line_range == (10, 15)
assert result.signature == "function my_func"
def test_extracts_signature_from_file(self):
"""Extracts signature from actual file content."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
# Create a temporary file with Python content
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write("# comment\n")
f.write("def test_function(x: int, y: str) -> bool:\n")
f.write(" return True\n")
temp_path = f.name
try:
mock_symbol = Mock()
mock_symbol.name = "test_function"
mock_symbol.kind = "function"
mock_symbol.file = temp_path
mock_symbol.range = (2, 3) # Line 2 (1-based)
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
provider = HoverProvider(mock_index, None)
result = provider.get_hover_info("test_function")
assert result is not None
assert "def test_function(x: int, y: str) -> bool:" in result.signature
finally:
Path(temp_path).unlink(missing_ok=True)
def test_extracts_multiline_signature(self):
"""Extracts multiline function signature."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
# Create a temporary file with multiline signature
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write("def complex_function(\n")
f.write(" arg1: int,\n")
f.write(" arg2: str,\n")
f.write(") -> bool:\n")
f.write(" return True\n")
temp_path = f.name
try:
mock_symbol = Mock()
mock_symbol.name = "complex_function"
mock_symbol.kind = "function"
mock_symbol.file = temp_path
mock_symbol.range = (1, 5) # Line 1 (1-based)
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
provider = HoverProvider(mock_index, None)
result = provider.get_hover_info("complex_function")
assert result is not None
assert "def complex_function(" in result.signature
# Should capture multiline signature
assert "arg1: int" in result.signature
finally:
Path(temp_path).unlink(missing_ok=True)
def test_handles_nonexistent_file_gracefully(self):
"""Returns fallback signature when file doesn't exist."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
mock_symbol = Mock()
mock_symbol.name = "my_func"
mock_symbol.kind = "function"
mock_symbol.file = "/nonexistent/path/file.py"
mock_symbol.range = (10, 15)
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
provider = HoverProvider(mock_index, None)
result = provider.get_hover_info("my_func")
assert result is not None
assert result.signature == "function my_func"
def test_handles_invalid_line_range(self):
"""Returns fallback signature when line range is invalid."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write("def test():\n")
f.write(" pass\n")
temp_path = f.name
try:
mock_symbol = Mock()
mock_symbol.name = "test"
mock_symbol.kind = "function"
mock_symbol.file = temp_path
mock_symbol.range = (100, 105) # Line beyond file length
mock_index = Mock()
mock_index.search.return_value = [mock_symbol]
provider = HoverProvider(mock_index, None)
result = provider.get_hover_info("test")
assert result is not None
assert result.signature == "function test"
finally:
Path(temp_path).unlink(missing_ok=True)
class TestFormatHoverMarkdown:
"""Test markdown formatting."""
def test_format_python_signature(self):
"""Formats Python signature with python code fence."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="func",
kind="function",
signature="def func(x: int) -> str:",
documentation=None,
file_path="/test/file.py",
line_range=(10, 15),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "```python" in result
assert "def func(x: int) -> str:" in result
assert "function" in result
assert "file.py" in result
assert "line 10" in result
def test_format_javascript_signature(self):
"""Formats JavaScript signature with javascript code fence."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="myFunc",
kind="function",
signature="function myFunc(x) {",
documentation=None,
file_path="/test/file.js",
line_range=(5, 10),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "```javascript" in result
assert "function myFunc(x) {" in result
def test_format_typescript_signature(self):
"""Formats TypeScript signature with typescript code fence."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="myFunc",
kind="function",
signature="function myFunc(x: number): string {",
documentation=None,
file_path="/test/file.ts",
line_range=(5, 10),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "```typescript" in result
def test_format_with_documentation(self):
"""Includes documentation when available."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="func",
kind="function",
signature="def func():",
documentation="This is a test function.",
file_path="/test/file.py",
line_range=(10, 15),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "This is a test function." in result
assert "---" in result # Separator before docs
def test_format_without_documentation(self):
"""Does not include documentation section when None."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="func",
kind="function",
signature="def func():",
documentation=None,
file_path="/test/file.py",
line_range=(10, 15),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
# Should have one separator for location, not two
# The result should not have duplicate doc separator
lines = result.split("\n")
separator_count = sum(1 for line in lines if line.strip() == "---")
assert separator_count == 1 # Only location separator
def test_format_unknown_extension(self):
"""Uses empty code fence for unknown file extensions."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="func",
kind="function",
signature="func code here",
documentation=None,
file_path="/test/file.xyz",
line_range=(1, 2),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
# Should have code fence without language specifier
assert "```\n" in result or "```xyz" not in result
def test_format_class_symbol(self):
"""Formats class symbol correctly."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="MyClass",
kind="class",
signature="class MyClass:",
documentation=None,
file_path="/test/file.py",
line_range=(1, 20),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "class MyClass:" in result
assert "*class*" in result
assert "line 1" in result
def test_format_empty_file_path(self):
"""Handles empty file path gracefully."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverInfo, HoverProvider
info = HoverInfo(
name="func",
kind="function",
signature="def func():",
documentation=None,
file_path="",
line_range=(1, 2),
)
mock_index = Mock()
provider = HoverProvider(mock_index, None)
result = provider.format_hover_markdown(info)
assert "unknown" in result or "```" in result
class TestHoverProviderRegistry:
"""Test HoverProvider with registry integration."""
def test_provider_accepts_none_registry(self):
"""HoverProvider works without registry."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
mock_index = Mock()
mock_index.search.return_value = []
provider = HoverProvider(mock_index, None)
result = provider.get_hover_info("test")
assert result is None
assert provider.registry is None
def test_provider_stores_registry(self):
"""HoverProvider stores registry reference."""
pytest.importorskip("pygls")
from codexlens.lsp.providers import HoverProvider
mock_index = Mock()
mock_registry = Mock()
provider = HoverProvider(mock_index, mock_registry)
assert provider.global_index is mock_index
assert provider.registry is mock_registry