Files
gitea-platform-mcp/tests/unit/test_cli.py
T
a-limasov-ii db5a43b19a refactor: make gitea a self-contained Cline skill
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.
2026-06-24 14:35:28 +03:00

127 lines
4.6 KiB
Python

"""Contract tests for the CLI dispatcher (the second face of the same tools)."""
import json
import httpx
import respx
from gitea_mcp import TOOLS, dispatch, main
BASE = "https://gitea.test/api/v1"
# ── Registry parity ─────────────────────────────────────────────────────────────
def test_tools_registry_matches_count():
# Same number the smoke test asserts for MCP registration.
assert len(TOOLS) == 66
def test_every_tool_is_callable():
assert all(callable(fn) for fn in TOOLS.values())
# ── serve vs CLI routing ────────────────────────────────────────────────────────
def test_no_args_means_serve():
assert dispatch([]) is None
def test_serve_subcommand_means_serve():
assert dispatch(["serve"]) is None
# ── Discovery ───────────────────────────────────────────────────────────────────
def test_list_tools_payload():
env = dispatch(["list-tools"])
assert env["success"] is True
assert env["data"]["count"] == 66
names = {t["name"] for t in env["data"]["tools"]}
assert "get_repo" in names
# parameter spec carries name/type/required
get_repo = next(t for t in env["data"]["tools"] if t["name"] == "get_repo")
owner = next(p for p in get_repo["params"] if p["name"] == "owner")
assert owner["required"] is True
assert owner["type"] == "str"
# ── Happy path: flags map to a correct REST call ────────────────────────────────
@respx.mock
def test_dispatch_happy_path():
route = respx.get(f"{BASE}/repos/andrey/infra").mock(
return_value=httpx.Response(200, json={"name": "infra", "id": 7})
)
env = dispatch(["get_repo", "--owner", "andrey", "--repo", "infra"])
assert route.called
assert env["success"] is True
assert env["data"]["name"] == "infra"
@respx.mock
def test_int_flag_is_typed():
route = respx.get(f"{BASE}/user/repos").mock(return_value=httpx.Response(200, json=[]))
dispatch(["list_my_repos", "--limit", "5", "--page", "2"])
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "5"
assert params["page"] == "2"
@respx.mock
def test_bool_no_flag_sends_false():
route = respx.post(f"{BASE}/user/repos").mock(
return_value=httpx.Response(201, json={"name": "x"})
)
dispatch(["create_repo", "--name", "x", "--no-private", "--no-auto-init"])
body = json.loads(route.calls[0].request.content)
assert body["private"] is False
assert body["auto_init"] is False
@respx.mock
def test_list_flag_takes_multiple_values():
route = respx.put(f"{BASE}/repos/o/r/topics").mock(return_value=httpx.Response(204))
dispatch(["set_repo_topics", "--owner", "o", "--repo", "r", "--topics", "ci", "backend"])
body = json.loads(route.calls[0].request.content)
assert body["topics"] == ["ci", "backend"]
# ── Error handling ──────────────────────────────────────────────────────────────
def test_unknown_command():
env = dispatch(["no_such_tool"])
assert env["success"] is False
assert env["type"] == "UnknownCommand"
def test_missing_required_flag_is_usage_error():
env = dispatch(["get_repo", "--owner", "andrey"]) # no --repo
assert env["success"] is False
assert env["type"] == "UsageError"
@respx.mock
def test_api_error_is_propagated_as_envelope():
respx.get(f"{BASE}/repos/o/missing").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
env = dispatch(["get_repo", "--owner", "o", "--repo", "missing"])
assert env["success"] is False
assert "404" in env["error"]
# ── main() prints exactly one JSON object and exits 0 ───────────────────────────
@respx.mock
def test_main_prints_single_json_and_exits_zero(capsys, monkeypatch):
respx.get(f"{BASE}/repos/o/r").mock(return_value=httpx.Response(200, json={"name": "r"}))
monkeypatch.setattr("sys.argv", ["gitea_mcp.py", "get_repo", "--owner", "o", "--repo", "r"])
try:
main()
except SystemExit as e:
assert e.code == 0
out = capsys.readouterr().out.strip()
parsed = json.loads(out) # exactly one JSON object
assert parsed["success"] is True