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.
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
import json
|
|
import pytest
|
|
import httpx
|
|
import respx
|
|
from gitea_mcp import list_releases, get_latest_release, create_release, delete_release
|
|
|
|
BASE = "https://gitea.test/api/v1"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_releases():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/releases").mock(
|
|
return_value=httpx.Response(200, json=[{"id": 1, "tag_name": "v1.0.0"}])
|
|
)
|
|
result = list_releases("owner", "repo")
|
|
assert route.called
|
|
assert result[0]["tag_name"] == "v1.0.0"
|
|
assert dict(route.calls[0].request.url.params)["limit"] == "10"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_latest_release():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/releases/latest").mock(
|
|
return_value=httpx.Response(200, json={"id": 3, "tag_name": "v2.0.0"})
|
|
)
|
|
result = get_latest_release("owner", "repo")
|
|
assert route.called
|
|
assert result["tag_name"] == "v2.0.0"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_latest_release_not_found():
|
|
respx.get(f"{BASE}/repos/owner/repo/releases/latest").mock(
|
|
return_value=httpx.Response(404, json={"message": "not found"})
|
|
)
|
|
with pytest.raises(RuntimeError, match="404"):
|
|
get_latest_release("owner", "repo")
|
|
|
|
|
|
@respx.mock
|
|
def test_create_release():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/releases").mock(
|
|
return_value=httpx.Response(201, json={"id": 5, "tag_name": "v3.0.0"})
|
|
)
|
|
result = create_release("owner", "repo", "v3.0.0", "Release 3.0.0")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["tag_name"] == "v3.0.0"
|
|
assert body["name"] == "Release 3.0.0"
|
|
assert body["draft"] is False
|
|
assert body["prerelease"] is False
|
|
assert "body" not in body
|
|
|
|
|
|
@respx.mock
|
|
def test_create_release_draft_with_notes():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/releases").mock(
|
|
return_value=httpx.Response(201, json={"id": 6})
|
|
)
|
|
create_release(
|
|
"owner", "repo", "v4.0.0-beta", "Beta release",
|
|
body="## Changes\n- new feature", draft=True, prerelease=True,
|
|
target_commitish="develop",
|
|
)
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["body"] == "## Changes\n- new feature"
|
|
assert body["draft"] is True
|
|
assert body["prerelease"] is True
|
|
assert body["target_commitish"] == "develop"
|
|
|
|
|
|
@respx.mock
|
|
def test_delete_release():
|
|
route = respx.delete(f"{BASE}/repos/owner/repo/releases/5").mock(
|
|
return_value=httpx.Response(204)
|
|
)
|
|
result = delete_release("owner", "repo", 5)
|
|
assert route.called
|
|
assert result == {"success": True}
|