Files
Claude-Code-Workflow/codex-lens/tests/api/test_references.py
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

283 lines
9.2 KiB
Python

"""Tests for codexlens.api.references module."""
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from codexlens.api.references import (
find_references,
_read_line_from_file,
_proximity_score,
_group_references_by_definition,
_transform_to_reference_result,
)
from codexlens.api.models import (
DefinitionResult,
ReferenceResult,
GroupedReferences,
)
class TestReadLineFromFile:
"""Tests for _read_line_from_file helper."""
def test_read_existing_line(self, tmp_path):
"""Test reading an existing line from a file."""
test_file = tmp_path / "test.py"
test_file.write_text("line 1\nline 2\nline 3\n")
assert _read_line_from_file(str(test_file), 1) == "line 1"
assert _read_line_from_file(str(test_file), 2) == "line 2"
assert _read_line_from_file(str(test_file), 3) == "line 3"
def test_read_nonexistent_line(self, tmp_path):
"""Test reading a line that doesn't exist."""
test_file = tmp_path / "test.py"
test_file.write_text("line 1\nline 2\n")
assert _read_line_from_file(str(test_file), 10) == ""
def test_read_nonexistent_file(self):
"""Test reading from a file that doesn't exist."""
assert _read_line_from_file("/nonexistent/path/file.py", 1) == ""
def test_strips_trailing_whitespace(self, tmp_path):
"""Test that trailing whitespace is stripped."""
test_file = tmp_path / "test.py"
test_file.write_text("line with spaces \n")
assert _read_line_from_file(str(test_file), 1) == "line with spaces"
class TestProximityScore:
"""Tests for _proximity_score helper."""
def test_same_file(self):
"""Same file should return highest score."""
score = _proximity_score("/a/b/c.py", "/a/b/c.py")
assert score == 1000
def test_same_directory(self):
"""Same directory should return 100."""
score = _proximity_score("/a/b/x.py", "/a/b/y.py")
assert score == 100
def test_different_directories(self):
"""Different directories should return common prefix length."""
score = _proximity_score("/a/b/c/x.py", "/a/b/d/y.py")
# Common path is /a/b
assert score > 0
def test_empty_paths(self):
"""Empty paths should return 0."""
assert _proximity_score("", "/a/b/c.py") == 0
assert _proximity_score("/a/b/c.py", "") == 0
assert _proximity_score("", "") == 0
class TestGroupReferencesByDefinition:
"""Tests for _group_references_by_definition helper."""
def test_single_definition(self):
"""Single definition should have all references."""
definition = DefinitionResult(
name="foo",
kind="function",
file_path="/a/b/c.py",
line=10,
end_line=20,
)
references = [
ReferenceResult(
file_path="/a/b/d.py",
line=5,
column=0,
context_line="foo()",
relationship="call",
),
ReferenceResult(
file_path="/a/x/y.py",
line=10,
column=0,
context_line="foo()",
relationship="call",
),
]
result = _group_references_by_definition([definition], references)
assert len(result) == 1
assert result[0].definition == definition
assert len(result[0].references) == 2
def test_multiple_definitions(self):
"""Multiple definitions should group by proximity."""
def1 = DefinitionResult(
name="foo",
kind="function",
file_path="/a/b/c.py",
line=10,
end_line=20,
)
def2 = DefinitionResult(
name="foo",
kind="function",
file_path="/x/y/z.py",
line=10,
end_line=20,
)
# Reference closer to def1
ref1 = ReferenceResult(
file_path="/a/b/d.py",
line=5,
column=0,
context_line="foo()",
relationship="call",
)
# Reference closer to def2
ref2 = ReferenceResult(
file_path="/x/y/w.py",
line=10,
column=0,
context_line="foo()",
relationship="call",
)
result = _group_references_by_definition(
[def1, def2], [ref1, ref2], include_definition=True
)
assert len(result) == 2
# Each definition should have the closer reference
def1_refs = [g for g in result if g.definition == def1][0].references
def2_refs = [g for g in result if g.definition == def2][0].references
assert any(r.file_path == "/a/b/d.py" for r in def1_refs)
assert any(r.file_path == "/x/y/w.py" for r in def2_refs)
def test_empty_definitions(self):
"""Empty definitions should return empty result."""
result = _group_references_by_definition([], [])
assert result == []
class TestTransformToReferenceResult:
"""Tests for _transform_to_reference_result helper."""
def test_normalizes_relationship_type(self, tmp_path):
"""Test that relationship type is normalized."""
test_file = tmp_path / "test.py"
test_file.write_text("def foo(): pass\n")
# Create a mock raw reference
raw_ref = MagicMock()
raw_ref.file_path = str(test_file)
raw_ref.line = 1
raw_ref.column = 0
raw_ref.relationship_type = "calls" # Plural form
result = _transform_to_reference_result(raw_ref)
assert result.relationship == "call" # Normalized form
assert result.context_line == "def foo(): pass"
class TestFindReferences:
"""Tests for find_references API function."""
def test_raises_for_invalid_project_root(self):
"""Test that ValueError is raised for invalid project root."""
with pytest.raises(ValueError, match="does not exist"):
find_references("/nonexistent/path", "some_symbol")
@patch("codexlens.search.chain_search.ChainSearchEngine")
@patch("codexlens.storage.registry.RegistryStore")
@patch("codexlens.storage.path_mapper.PathMapper")
@patch("codexlens.config.Config")
def test_returns_grouped_references(
self, mock_config, mock_mapper, mock_registry, mock_engine_class, tmp_path
):
"""Test that find_references returns GroupedReferences."""
# Setup mocks
mock_engine = MagicMock()
mock_engine_class.return_value = mock_engine
# Mock symbol search (for definitions)
mock_symbol = MagicMock()
mock_symbol.name = "test_func"
mock_symbol.kind = "function"
mock_symbol.file = str(tmp_path / "test.py")
mock_symbol.range = (10, 20)
mock_engine.search_symbols.return_value = [mock_symbol]
# Mock reference search
mock_ref = MagicMock()
mock_ref.file_path = str(tmp_path / "caller.py")
mock_ref.line = 5
mock_ref.column = 0
mock_ref.relationship_type = "call"
mock_engine.search_references.return_value = [mock_ref]
# Create test files
test_file = tmp_path / "test.py"
test_file.write_text("def test_func():\n pass\n")
caller_file = tmp_path / "caller.py"
caller_file.write_text("test_func()\n")
# Call find_references
result = find_references(str(tmp_path), "test_func")
# Verify result structure
assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], GroupedReferences)
assert result[0].definition.name == "test_func"
assert len(result[0].references) == 1
@patch("codexlens.search.chain_search.ChainSearchEngine")
@patch("codexlens.storage.registry.RegistryStore")
@patch("codexlens.storage.path_mapper.PathMapper")
@patch("codexlens.config.Config")
def test_respects_include_definition_false(
self, mock_config, mock_mapper, mock_registry, mock_engine_class, tmp_path
):
"""Test include_definition=False behavior."""
mock_engine = MagicMock()
mock_engine_class.return_value = mock_engine
mock_engine.search_symbols.return_value = []
mock_engine.search_references.return_value = []
result = find_references(
str(tmp_path), "test_func", include_definition=False
)
# Should still return a result with placeholder definition
assert len(result) == 1
assert result[0].definition.name == "test_func"
class TestImports:
"""Tests for module imports and exports."""
def test_find_references_exported_from_api(self):
"""Test that find_references is exported from codexlens.api."""
from codexlens.api import find_references as api_find_references
assert callable(api_find_references)
def test_models_exported_from_api(self):
"""Test that result models are exported from codexlens.api."""
from codexlens.api import (
GroupedReferences,
ReferenceResult,
DefinitionResult,
)
assert GroupedReferences is not None
assert ReferenceResult is not None
assert DefinitionResult is not None