"""Tests for MCP schema."""
import pytest
import json
from codexlens.mcp.schema import (
MCPContext,
SymbolInfo,
ReferenceInfo,
RelatedSymbol,
)
class TestSymbolInfo:
"""Test SymbolInfo dataclass."""
def test_to_dict_includes_all_fields(self):
"""SymbolInfo.to_dict() includes all non-None fields."""
info = SymbolInfo(
name="func",
kind="function",
file_path="/test.py",
line_start=10,
line_end=20,
signature="def func():",
documentation="Test doc",
)
d = info.to_dict()
assert d["name"] == "func"
assert d["kind"] == "function"
assert d["file_path"] == "/test.py"
assert d["line_start"] == 10
assert d["line_end"] == 20
assert d["signature"] == "def func():"
assert d["documentation"] == "Test doc"
def test_to_dict_excludes_none(self):
"""SymbolInfo.to_dict() excludes None fields."""
info = SymbolInfo(
name="func",
kind="function",
file_path="/test.py",
line_start=10,
line_end=20,
)
d = info.to_dict()
assert "signature" not in d
assert "documentation" not in d
assert "name" in d
assert "kind" in d
def test_basic_creation(self):
"""SymbolInfo can be created with required fields only."""
info = SymbolInfo(
name="MyClass",
kind="class",
file_path="/src/module.py",
line_start=1,
line_end=50,
)
assert info.name == "MyClass"
assert info.kind == "class"
assert info.signature is None
assert info.documentation is None
class TestReferenceInfo:
"""Test ReferenceInfo dataclass."""
def test_to_dict(self):
"""ReferenceInfo.to_dict() returns all fields."""
ref = ReferenceInfo(
file_path="/src/main.py",
line=25,
column=4,
context="result = func()",
relationship_type="call",
)
d = ref.to_dict()
assert d["file_path"] == "/src/main.py"
assert d["line"] == 25
assert d["column"] == 4
assert d["context"] == "result = func()"
assert d["relationship_type"] == "call"
def test_all_fields_required(self):
"""ReferenceInfo requires all fields."""
ref = ReferenceInfo(
file_path="/test.py",
line=10,
column=0,
context="import module",
relationship_type="import",
)
assert ref.file_path == "/test.py"
assert ref.relationship_type == "import"
class TestRelatedSymbol:
"""Test RelatedSymbol dataclass."""
def test_to_dict_includes_all_fields(self):
"""RelatedSymbol.to_dict() includes all non-None fields."""
sym = RelatedSymbol(
name="BaseClass",
kind="class",
relationship="inherits",
file_path="/src/base.py",
)
d = sym.to_dict()
assert d["name"] == "BaseClass"
assert d["kind"] == "class"
assert d["relationship"] == "inherits"
assert d["file_path"] == "/src/base.py"
def test_to_dict_excludes_none(self):
"""RelatedSymbol.to_dict() excludes None file_path."""
sym = RelatedSymbol(
name="helper",
kind="function",
relationship="calls",
)
d = sym.to_dict()
assert "file_path" not in d
assert d["name"] == "helper"
assert d["relationship"] == "calls"
class TestMCPContext:
"""Test MCPContext dataclass."""
def test_to_dict_basic(self):
"""MCPContext.to_dict() returns basic structure."""
ctx = MCPContext(context_type="test")
d = ctx.to_dict()
assert d["version"] == "1.0"
assert d["context_type"] == "test"
assert d["metadata"] == {}
def test_to_dict_with_symbol(self):
"""MCPContext.to_dict() includes symbol when present."""
ctx = MCPContext(
context_type="test",
symbol=SymbolInfo("f", "function", "/t.py", 1, 2),
)
d = ctx.to_dict()
assert "symbol" in d
assert d["symbol"]["name"] == "f"
assert d["symbol"]["kind"] == "function"
def test_to_dict_with_references(self):
"""MCPContext.to_dict() includes references when present."""
ctx = MCPContext(
context_type="test",
references=[
ReferenceInfo("/a.py", 10, 0, "call()", "call"),
ReferenceInfo("/b.py", 20, 5, "import x", "import"),
],
)
d = ctx.to_dict()
assert "references" in d
assert len(d["references"]) == 2
assert d["references"][0]["line"] == 10
def test_to_dict_with_related_symbols(self):
"""MCPContext.to_dict() includes related_symbols when present."""
ctx = MCPContext(
context_type="test",
related_symbols=[
RelatedSymbol("Base", "class", "inherits"),
RelatedSymbol("helper", "function", "calls"),
],
)
d = ctx.to_dict()
assert "related_symbols" in d
assert len(d["related_symbols"]) == 2
def test_to_json(self):
"""MCPContext.to_json() returns valid JSON."""
ctx = MCPContext(context_type="test")
j = ctx.to_json()
parsed = json.loads(j)
assert parsed["version"] == "1.0"
assert parsed["context_type"] == "test"
def test_to_json_with_indent(self):
"""MCPContext.to_json() respects indent parameter."""
ctx = MCPContext(context_type="test")
j = ctx.to_json(indent=4)
# Check it's properly indented
assert " " in j
def test_to_prompt_injection_basic(self):
"""MCPContext.to_prompt_injection() returns formatted string."""
ctx = MCPContext(
symbol=SymbolInfo("my_func", "function", "/test.py", 10, 20),
definition="def my_func(): pass",
)
prompt = ctx.to_prompt_injection()
assert "" in prompt
assert "my_func" in prompt
assert "def my_func()" in prompt
assert "" in prompt
def test_to_prompt_injection_with_references(self):
"""MCPContext.to_prompt_injection() includes references."""
ctx = MCPContext(
symbol=SymbolInfo("func", "function", "/test.py", 1, 5),
references=[
ReferenceInfo("/a.py", 10, 0, "func()", "call"),
ReferenceInfo("/b.py", 20, 0, "from x import func", "import"),
],
)
prompt = ctx.to_prompt_injection()
assert "References (2 found)" in prompt
assert "/a.py:10" in prompt
assert "call" in prompt
def test_to_prompt_injection_limits_references(self):
"""MCPContext.to_prompt_injection() limits references to 5."""
refs = [
ReferenceInfo(f"/file{i}.py", i, 0, f"ref{i}", "call")
for i in range(10)
]
ctx = MCPContext(
symbol=SymbolInfo("func", "function", "/test.py", 1, 5),
references=refs,
)
prompt = ctx.to_prompt_injection()
# Should show "10 found" but only include 5
assert "References (10 found)" in prompt
assert "/file0.py" in prompt
assert "/file4.py" in prompt
assert "/file5.py" not in prompt
def test_to_prompt_injection_with_related_symbols(self):
"""MCPContext.to_prompt_injection() includes related symbols."""
ctx = MCPContext(
symbol=SymbolInfo("MyClass", "class", "/test.py", 1, 50),
related_symbols=[
RelatedSymbol("BaseClass", "class", "inherits"),
RelatedSymbol("helper", "function", "calls"),
],
)
prompt = ctx.to_prompt_injection()
assert "Related Symbols" in prompt
assert "BaseClass (inherits)" in prompt
assert "helper (calls)" in prompt
def test_to_prompt_injection_limits_related_symbols(self):
"""MCPContext.to_prompt_injection() limits related symbols to 10."""
related = [
RelatedSymbol(f"sym{i}", "function", "calls")
for i in range(15)
]
ctx = MCPContext(
symbol=SymbolInfo("func", "function", "/test.py", 1, 5),
related_symbols=related,
)
prompt = ctx.to_prompt_injection()
assert "sym0 (calls)" in prompt
assert "sym9 (calls)" in prompt
assert "sym10 (calls)" not in prompt
def test_empty_context(self):
"""MCPContext works with minimal data."""
ctx = MCPContext()
d = ctx.to_dict()
assert d["version"] == "1.0"
assert d["context_type"] == "code_context"
prompt = ctx.to_prompt_injection()
assert "" in prompt
assert "" in prompt
def test_metadata_preserved(self):
"""MCPContext preserves custom metadata."""
ctx = MCPContext(
context_type="custom",
metadata={
"source": "codex-lens",
"indexed_at": "2024-01-01",
"custom_key": "custom_value",
},
)
d = ctx.to_dict()
assert d["metadata"]["source"] == "codex-lens"
assert d["metadata"]["custom_key"] == "custom_value"