"""Tests for repository-focused tool handlers.""" from __future__ import annotations import pytest from aegis_gitea_mcp.config import reset_settings from aegis_gitea_mcp.gitea_client import GiteaError from aegis_gitea_mcp.tools.repository import ( get_file_contents_tool, get_file_tree_tool, get_repository_info_tool, list_repositories_tool, ) @pytest.fixture(autouse=True) def repository_tool_env(monkeypatch: pytest.MonkeyPatch) -> None: """Provide minimal settings needed by response limit helpers.""" reset_settings() monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") monkeypatch.setenv("GITEA_TOKEN", "legacy-token") monkeypatch.setenv("MCP_API_KEYS", "a" * 64) monkeypatch.setenv("ENVIRONMENT", "test") yield reset_settings() class RepoStub: """Stub Gitea client for repository tools.""" async def list_repositories(self): return [{"name": "demo", "owner": {"login": "acme"}, "full_name": "acme/demo"}] async def get_repository(self, owner, repo): return {"name": repo, "owner": {"login": owner}, "full_name": f"{owner}/{repo}"} async def get_tree(self, owner, repo, ref, recursive): return {"tree": [{"path": "README.md", "type": "blob", "size": 11, "sha": "abc"}]} async def get_file_contents(self, owner, repo, filepath, ref): return { "content": "SGVsbG8gV29ybGQ=", "encoding": "base64", "size": 11, "sha": "abc", "html_url": f"https://example/{owner}/{repo}/{filepath}", } class RepoErrorStub(RepoStub): """Stub that raises backend errors.""" async def list_repositories(self): raise GiteaError("backend down") @pytest.mark.asyncio async def test_list_repositories_tool_success() -> None: """Repository listing tool normalizes output shape.""" result = await list_repositories_tool(RepoStub(), {}) assert result["count"] == 1 assert result["repositories"][0]["owner"] == "acme" @pytest.mark.asyncio async def test_list_repositories_tool_failure_mode() -> None: """Repository listing tool wraps backend errors.""" with pytest.raises(RuntimeError, match="Failed to list repositories"): await list_repositories_tool(RepoErrorStub(), {}) @pytest.mark.asyncio async def test_get_repository_info_tool_success() -> None: """Repository info tool returns normalized metadata.""" result = await get_repository_info_tool(RepoStub(), {"owner": "acme", "repo": "demo"}) assert result["full_name"] == "acme/demo" @pytest.mark.asyncio async def test_get_file_tree_tool_success() -> None: """File tree tool returns bounded tree entries.""" result = await get_file_tree_tool( RepoStub(), {"owner": "acme", "repo": "demo", "ref": "main", "recursive": False}, ) assert result["count"] == 1 assert result["tree"][0]["path"] == "README.md" @pytest.mark.asyncio async def test_get_file_contents_tool_decodes_base64() -> None: """File contents tool decodes UTF-8 base64 payloads.""" result = await get_file_contents_tool( RepoStub(), {"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"}, ) assert result["content"] == "Hello World" @pytest.mark.asyncio async def test_get_file_contents_tool_handles_invalid_base64() -> None: """Invalid base64 payloads are returned safely without crashing.""" class InvalidBase64Stub(RepoStub): async def get_file_contents(self, owner, repo, filepath, ref): return {"content": "%%%not-base64%%%", "encoding": "base64", "size": 4, "sha": "abc"} result = await get_file_contents_tool( InvalidBase64Stub(), {"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"}, ) assert result["content"] == "%%%not-base64%%%" @pytest.mark.asyncio async def test_get_file_contents_tool_failure_mode() -> None: """File contents tool wraps backend failures.""" class ErrorFileStub(RepoStub): async def get_file_contents(self, owner, repo, filepath, ref): raise GiteaError("boom") with pytest.raises(RuntimeError, match="Failed to get file contents"): await get_file_contents_tool( ErrorFileStub(), {"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"}, )