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.
150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
import json
|
|
import pytest
|
|
import httpx
|
|
import respx
|
|
from gitea_mcp import (
|
|
list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue,
|
|
list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment,
|
|
)
|
|
|
|
BASE = "https://gitea.test/api/v1"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_issues_defaults():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/issues").mock(
|
|
return_value=httpx.Response(200, json=[])
|
|
)
|
|
list_issues("owner", "repo")
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["state"] == "open"
|
|
assert params["type"] == "issues"
|
|
assert params["limit"] == "20"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_issues_with_filters():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/issues").mock(
|
|
return_value=httpx.Response(200, json=[])
|
|
)
|
|
list_issues("owner", "repo", state="closed", labels="bug", assignee="alice")
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["state"] == "closed"
|
|
assert params["labels"] == "bug"
|
|
assert params["assignee"] == "alice"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_issue():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/issues/42").mock(
|
|
return_value=httpx.Response(200, json={"number": 42, "title": "Bug"})
|
|
)
|
|
result = get_issue("owner", "repo", 42)
|
|
assert route.called
|
|
assert result["number"] == 42
|
|
|
|
|
|
@respx.mock
|
|
def test_get_issue_not_found():
|
|
respx.get(f"{BASE}/repos/owner/repo/issues/999").mock(
|
|
return_value=httpx.Response(404, json={"message": "not found"})
|
|
)
|
|
with pytest.raises(RuntimeError, match="404"):
|
|
get_issue("owner", "repo", 999)
|
|
|
|
|
|
@respx.mock
|
|
def test_create_issue():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/issues").mock(
|
|
return_value=httpx.Response(201, json={"number": 1, "title": "New bug"})
|
|
)
|
|
result = create_issue("owner", "repo", "New bug", body="description")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["title"] == "New bug"
|
|
assert body["body"] == "description"
|
|
|
|
|
|
@respx.mock
|
|
def test_create_issue_with_assignees_and_labels():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/issues").mock(
|
|
return_value=httpx.Response(201, json={"number": 2})
|
|
)
|
|
create_issue("owner", "repo", "Task", assignees=["alice"], labels=[1, 2])
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["assignees"] == ["alice"]
|
|
assert body["labels"] == [1, 2]
|
|
|
|
|
|
@respx.mock
|
|
def test_update_issue():
|
|
route = respx.patch(f"{BASE}/repos/owner/repo/issues/5").mock(
|
|
return_value=httpx.Response(200, json={"number": 5})
|
|
)
|
|
update_issue("owner", "repo", 5, title="Updated title")
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["title"] == "Updated title"
|
|
assert "body" not in body
|
|
|
|
|
|
@respx.mock
|
|
def test_close_issue():
|
|
route = respx.patch(f"{BASE}/repos/owner/repo/issues/3").mock(
|
|
return_value=httpx.Response(200, json={"state": "closed"})
|
|
)
|
|
result = close_issue("owner", "repo", 3)
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["state"] == "closed"
|
|
|
|
|
|
@respx.mock
|
|
def test_reopen_issue():
|
|
route = respx.patch(f"{BASE}/repos/owner/repo/issues/3").mock(
|
|
return_value=httpx.Response(200, json={"state": "open"})
|
|
)
|
|
reopen_issue("owner", "repo", 3)
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["state"] == "open"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_issue_comments():
|
|
route = respx.get(f"{BASE}/repos/owner/repo/issues/7/comments").mock(
|
|
return_value=httpx.Response(200, json=[{"id": 1, "body": "hi"}])
|
|
)
|
|
result = list_issue_comments("owner", "repo", 7)
|
|
assert route.called
|
|
assert result[0]["id"] == 1
|
|
|
|
|
|
@respx.mock
|
|
def test_add_issue_comment():
|
|
route = respx.post(f"{BASE}/repos/owner/repo/issues/7/comments").mock(
|
|
return_value=httpx.Response(201, json={"id": 10, "body": "LGTM"})
|
|
)
|
|
result = add_issue_comment("owner", "repo", 7, "LGTM")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["body"] == "LGTM"
|
|
|
|
|
|
@respx.mock
|
|
def test_edit_issue_comment():
|
|
route = respx.patch(f"{BASE}/repos/owner/repo/issues/comments/42").mock(
|
|
return_value=httpx.Response(200, json={"id": 42})
|
|
)
|
|
edit_issue_comment("owner", "repo", 42, "edited body")
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["body"] == "edited body"
|
|
|
|
|
|
@respx.mock
|
|
def test_delete_issue_comment():
|
|
route = respx.delete(f"{BASE}/repos/owner/repo/issues/comments/42").mock(
|
|
return_value=httpx.Response(204)
|
|
)
|
|
result = delete_issue_comment("owner", "repo", 42)
|
|
assert route.called
|
|
assert result == {"success": True}
|