Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,213 @@ async def test_parser_failure_preserves_existing_index_entries() -> None:
store.delete_by_file.assert_not_called()
store.upsert_chunks.assert_not_called()
assert result["files"] == 0


async def test_unchanged_file_is_skipped_without_fetching_content() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])
github_file = GitHubFile(rel_path="unchanged.py", blob_sha="same-sha")

store = AsyncMock()
store.get_indexed_file_hashes.return_value = {"svc/unchanged.py": "same-sha"}

pipeline = _make_pipeline(store)

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module, "list_github_files", AsyncMock(return_value=[github_file])
),
patch.object(pipeline_module, "fetch_blob_content", AsyncMock()) as mock_fetch,
):
result = await pipeline.index_service("svc", force=False)

mock_fetch.assert_not_called()
store.delete_by_file.assert_not_called()
assert result == {"files": 0, "chunks": 0, "skipped": 1}


async def test_force_reindexes_even_when_hash_unchanged() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])
github_file = GitHubFile(rel_path="unchanged.py", blob_sha="same-sha")
symbol = CodeSymbol(
name="fn",
symbol_type="function",
language="python",
source="def fn(): pass",
file_path="svc/unchanged.py",
start_line=1,
end_line=1,
)

store = AsyncMock()
store.get_indexed_file_hashes.return_value = {"svc/unchanged.py": "same-sha"}

pipeline = _make_pipeline(store)

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module, "list_github_files", AsyncMock(return_value=[github_file])
),
patch.object(
pipeline_module,
"fetch_blob_content",
AsyncMock(return_value=b"def fn(): pass"),
),
patch.object(pipeline_module, "parse_file", return_value=[symbol]),
):
result = await pipeline.index_service("svc", force=True)

store.delete_by_file.assert_called_once_with("svc", "svc/unchanged.py")
store.upsert_chunks.assert_called_once()
assert result == {"files": 1, "chunks": 1, "skipped": 0}


async def test_stale_index_entries_removed_when_file_no_longer_in_repo() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])

store = AsyncMock()
store.get_indexed_file_hashes.return_value = {
"svc/deleted.py": "old-sha",
"svc/still-there.py": "sha",
}

pipeline = _make_pipeline(store)

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module,
"list_github_files",
AsyncMock(
return_value=[GitHubFile(rel_path="still-there.py", blob_sha="sha")]
),
),
):
result = await pipeline.index_service("svc", force=False)

store.delete_by_file.assert_called_once_with("svc", "svc/deleted.py")
assert result["skipped"] == 1


async def test_file_with_no_symbols_deletes_stale_entries_and_skips_upsert() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])
github_file = GitHubFile(rel_path="empty.py", blob_sha="sha1")

store = AsyncMock()
store.get_indexed_file_hashes.return_value = {}

pipeline = _make_pipeline(store)

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module, "list_github_files", AsyncMock(return_value=[github_file])
),
patch.object(
pipeline_module, "fetch_blob_content", AsyncMock(return_value=b"")
),
patch.object(pipeline_module, "parse_file", return_value=[]),
):
result = await pipeline.index_service("svc", force=True)

store.delete_by_file.assert_called_once_with("svc", "svc/empty.py")
store.upsert_chunks.assert_not_called()
assert result["files"] == 0


async def test_embedding_failure_preserves_existing_index_entries() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])
github_file = GitHubFile(rel_path="broken.py", blob_sha="sha1")
symbol = CodeSymbol(
name="fn",
symbol_type="function",
language="python",
source="def fn(): pass",
file_path="svc/broken.py",
start_line=1,
end_line=1,
)

store = AsyncMock()
store.get_indexed_file_hashes.return_value = {"svc/broken.py": "old-sha"}

pipeline = _make_pipeline(store)
pipeline._embedder.embed_batch = AsyncMock(side_effect=RuntimeError("503"))

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module, "list_github_files", AsyncMock(return_value=[github_file])
),
patch.object(
pipeline_module,
"fetch_blob_content",
AsyncMock(return_value=b"def fn(): pass"),
),
patch.object(pipeline_module, "parse_file", return_value=[symbol]),
):
result = await pipeline.index_service("svc", force=True)

store.delete_by_file.assert_not_called()
store.upsert_chunks.assert_not_called()
assert result["files"] == 0


async def test_delete_by_file_called_before_upsert_for_changed_file() -> None:
svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[])
github_file = GitHubFile(rel_path="mod.py", blob_sha="new-sha")
symbol = CodeSymbol(
name="fn",
symbol_type="function",
language="python",
source="def fn(): pass",
file_path="svc/mod.py",
start_line=1,
end_line=1,
)

call_order: list[str] = []
store = AsyncMock()
store.get_indexed_file_hashes.return_value = {"svc/mod.py": "old-sha"}
store.delete_by_file.side_effect = lambda *a, **k: call_order.append("delete")
store.upsert_chunks.side_effect = lambda *a, **k: call_order.append("upsert")

pipeline = _make_pipeline(store)

with (
patch.object(
type(pipeline_module.settings), "load_services", return_value=[svc]
),
patch.object(
pipeline_module, "list_github_files", AsyncMock(return_value=[github_file])
),
patch.object(
pipeline_module,
"fetch_blob_content",
AsyncMock(return_value=b"def fn(): pass"),
),
patch.object(pipeline_module, "parse_file", return_value=[symbol]),
):
await pipeline.index_service("svc", force=False)

assert call_order == ["delete", "upsert"]


async def test_unknown_service_returns_error_without_touching_github() -> None:
pipeline = _make_pipeline(AsyncMock())

with patch.object(type(pipeline_module.settings), "load_services", return_value=[]):
result = await pipeline.index_service("missing")

assert result == {"error": 1, "files": 0, "chunks": 0}
Empty file added tests/tools/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions tests/tools/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from mcp.server.fastmcp import FastMCP


def get_tool(register: Callable[[FastMCP], None], name: str) -> Callable:
"""Register a tool module against a throwaway FastMCP instance and return
the underlying async function, bypassing MCP's request/response plumbing."""
mcp = FastMCP("test")
register(mcp)
return mcp._tool_manager._tools[name].fn


@dataclass
class StubHit:
payload: dict[str, Any] = field(default_factory=dict)
score: float = 1.0
89 changes: 89 additions & 0 deletions tests/tools/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from unittest.mock import AsyncMock, patch

from server.config import ServiceConfig
from server.tools.admin import register_admin_tools
from tests.tools.conftest import get_tool


def _tool(name: str):
return get_tool(register_admin_tools, name)


async def test_list_indexed_services_reports_when_empty() -> None:
list_indexed_services = _tool("list_indexed_services")
store = AsyncMock()
store.get_service_stats.return_value = []

with patch("server.tools.admin.get_store", return_value=store):
result = await list_indexed_services()

assert "No services indexed yet" in result


async def test_list_indexed_services_formats_stats_sorted_by_name() -> None:
list_indexed_services = _tool("list_indexed_services")
store = AsyncMock()
store.get_service_stats.return_value = [
{
"service": "zebra",
"chunk_count": 5,
"file_count": 2,
"languages": ["python"],
"last_indexed": "2026-01-01",
},
{
"service": "alpha",
"chunk_count": 10,
"file_count": 4,
"languages": ["java", "python"],
"last_indexed": "2026-01-02",
},
]

with patch("server.tools.admin.get_store", return_value=store):
result = await list_indexed_services()

assert result.index("alpha") < result.index("zebra")
assert "Chunks: 10" in result


async def test_index_stats_reports_qdrant_error() -> None:
index_stats = _tool("index_stats")
store = AsyncMock()
store.collection_info.side_effect = RuntimeError("connection refused")

with (
patch("server.tools.admin.get_store", return_value=store),
patch("server.tools.admin.settings") as mock_settings,
):
mock_settings.load_services.return_value = []
result = await index_stats()

assert "Could not reach Qdrant" in result
assert "connection refused" in result


async def test_index_stats_includes_provider_endpoint() -> None:
index_stats = _tool("index_stats")
store = AsyncMock()
store.collection_info.return_value = {
"collection": "code_symbols",
"total_vectors": 100,
"vector_size": 768,
"status": "green",
}
svc = ServiceConfig(name="orders", github_repo="org/orders", exclude=[])

with (
patch("server.tools.admin.get_store", return_value=store),
patch("server.tools.admin.settings") as mock_settings,
):
mock_settings.load_services.return_value = [svc]
mock_settings.embeddings_provider = "voyage"
result = await index_stats()

assert "**Embeddings provider**: voyage" in result
assert "https://api.voyageai.com/v1/embeddings" in result
assert "`orders` — `org/orders@main`" in result
Loading
Loading