db5a43b19a
Relocate the script into the skill (skills/gitea/scripts/gitea_mcp.py) and add a PEP 723 dependency block, so `uv run scripts/gitea_mcp.py` provisions deps with no install step and the skill folder is portable (drop into ~/.cline/skills/). Drop the non-standard `metadata` frontmatter — Cline only recognizes name + description. - .mcp.json points at the relocated script and runs with --no-project (PEP 723) - SKILL.md uses relative scripts/ paths; metadata block removed - find_config walks up the tree to locate config.json (skill root or plugin root) - tests import gitea_mcp via pytest pythonpath; README/CLAUDE paths updated Tests: 119 passed, 92% coverage. Standalone list-tools verified via uv --no-project.
109 lines
3.7 KiB
Python
109 lines
3.7 KiB
Python
import base64
|
|
import json
|
|
import pytest
|
|
import httpx
|
|
import respx
|
|
from gitea_mcp import list_directory, get_file, create_file, update_file, delete_file
|
|
|
|
BASE = "https://gitea.test/api/v1"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_directory_root():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/contents/").mock(
|
|
return_value=httpx.Response(200, json=[{"name": "README.md", "type": "file"}])
|
|
)
|
|
result = list_directory("owner", "repo")
|
|
assert route.called
|
|
assert result[0]["name"] == "README.md"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_directory_with_ref():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/contents/src").mock(
|
|
return_value=httpx.Response(200, json=[])
|
|
)
|
|
list_directory("owner", "repo", path="src", ref="develop")
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["ref"] == "develop"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_file():
|
|
encoded = base64.b64encode(b"hello world").decode()
|
|
route = respx.get(f"{BASE}/repos/owner/repo/contents/README.md").mock(
|
|
return_value=httpx.Response(200, json={"name": "README.md", "content": encoded, "sha": "abc"})
|
|
)
|
|
result = get_file("owner", "repo", "README.md")
|
|
assert route.called
|
|
assert result["content"] == encoded
|
|
assert result["sha"] == "abc"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_file_with_ref():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/contents/README.md").mock(
|
|
return_value=httpx.Response(200, json={"name": "README.md"})
|
|
)
|
|
get_file("owner", "repo", "README.md", ref="v1.0")
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["ref"] == "v1.0"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_file_not_found():
|
|
respx.get(f"{BASE}/repos/owner/repo/contents/missing.txt").mock(
|
|
return_value=httpx.Response(404, json={"message": "not found"})
|
|
)
|
|
with pytest.raises(RuntimeError, match="404"):
|
|
get_file("owner", "repo", "missing.txt")
|
|
|
|
|
|
@respx.mock
|
|
def test_create_file():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/contents/hello.txt").mock(
|
|
return_value=httpx.Response(201, json={"content": {"name": "hello.txt"}})
|
|
)
|
|
result = create_file("owner", "repo", "hello.txt", "hello world", "add hello")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["content"] == base64.b64encode(b"hello world").decode()
|
|
assert body["message"] == "add hello"
|
|
assert "branch" not in body
|
|
|
|
|
|
@respx.mock
|
|
def test_create_file_on_branch():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/contents/file.py").mock(
|
|
return_value=httpx.Response(201, json={"content": {"name": "file.py"}})
|
|
)
|
|
create_file("owner", "repo", "file.py", "print('hi')", "add file", branch="develop")
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["branch"] == "develop"
|
|
|
|
|
|
@respx.mock
|
|
def test_update_file():
|
|
new_content = "updated content"
|
|
route = respx.put(f"{BASE}/repos/owner/repo/contents/README.md").mock(
|
|
return_value=httpx.Response(200, json={"content": {"name": "README.md"}})
|
|
)
|
|
update_file("owner", "repo", "README.md", new_content, "update readme", sha="deadbeef")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["content"] == base64.b64encode(new_content.encode()).decode()
|
|
assert body["sha"] == "deadbeef"
|
|
assert body["message"] == "update readme"
|
|
|
|
|
|
@respx.mock
|
|
def test_delete_file():
|
|
route = respx.delete(f"{BASE}/repos/owner/repo/contents/old.txt").mock(
|
|
return_value=httpx.Response(200, json={"commit": {"sha": "abc"}})
|
|
)
|
|
result = delete_file("owner", "repo", "old.txt", "remove old file", sha="abc123")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["message"] == "remove old file"
|
|
assert body["sha"] == "abc123"
|