Files
gitea-platform-mcp/CLAUDE.md
T
a-limasov-ii a6162a855c test: add 106 unit + smoke tests, 93% coverage
- pytest + respx + pytest-cov added as dev dependencies
- tests/smoke/test_registration.py: verify all 66 tools registered and unique
- tests/unit/: 12 files covering every tool — HTTP method, URL, params/body, errors
- tests/config.test.json + conftest.py: zero-network test setup via GITEA_CONFIG
- CLAUDE.md: developer guide with testing philosophy, pyramid, and commands
- server/gitea_mcp.py: get_commit, list_pr_files tools added
- README.md: tool count corrected to 66
2026-06-08 09:09:44 +03:00

180 lines
5.3 KiB
Markdown

# CLAUDE.md — Developer Guide
## Project overview
MCP server exposing Gitea REST API as 66 tools. Single source file: `server/gitea_mcp.py`.
Config in `config.json` (gitignored). Run via `uv run server/gitea_mcp.py`.
## Commit style
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`
- No `Co-Authored-By` or any AI attribution lines in commit messages
## Adding tools
Each tool is a `@mcp.tool()` function. Pattern:
```python
@mcp.tool()
def my_tool(owner: str, repo: str, instance: str = "") -> Any:
"""One-line docstring — becomes the tool description shown to the LLM."""
return GET(instance, f"/repos/{owner}/{repo}/something")
```
Rules:
- `instance: str = ""` is always the last parameter
- Use `_strip({...})` to drop `None` values before sending
- URL-encode path segments that may contain slashes via `_b(value)`
- Update the tool count in `README.md` after adding tools
---
## Testing
### Philosophy
The server has no business logic — every tool is a thin mapping from MCP arguments to a
Gitea REST call. Tests therefore focus on **contract correctness**:
- correct HTTP method and URL
- correct request body / query parameters
- correct handling of API error responses
- all tools are registered and publicly discoverable
We do **not** test Gitea's own behavior — that is covered by Gitea's own test suite.
### Test pyramid
```
┌──────────────────────────────┐
│ Integration (opt-in) │ real Gitea via Docker Compose
├──────────────────────────────┤
│ Unit — per-tool contracts │ pytest + respx (httpx mock)
├──────────────────────────────┤
│ Smoke — registration check │ import + introspect registered tools
└──────────────────────────────┘
```
### Tooling
| Tool | Purpose |
|------|---------|
| `pytest` | test runner |
| `respx` | mock `httpx.Client` at the transport level — no monkey-patching |
| `pytest-cov` | coverage reporting |
Install dev dependencies:
```bash
uv add --dev pytest respx pytest-cov
```
### Directory layout
```
tests/
├── conftest.py # shared fixtures (mock transport, config override)
├── smoke/
│ └── test_registration.py # all tools are registered, count matches README
└── unit/
├── test_meta.py
├── test_repos.py
├── test_branches.py
├── test_files.py
├── test_issues.py
├── test_labels_milestones.py
├── test_pull_requests.py
├── test_releases.py
├── test_webhooks.py
├── test_users_orgs.py
├── test_commits.py
└── test_notifications.py
```
### What each unit test must cover
For every tool function, write at minimum:
1. **Happy path** — correct HTTP method, URL pattern, and response forwarded as-is
2. **Parameter mapping** — optional params omitted when empty, present when set
3. **Error propagation** — non-2xx status raises `RuntimeError` with the status code
```python
# Example pattern
import respx, httpx, pytest
from server.gitea_mcp import get_commit
@respx.mock
def test_get_commit_happy_path():
route = respx.get(
"https://gitea.example.com/api/v1/repos/owner/repo/git/commits/abc123"
).mock(return_value=httpx.Response(200, json={"sha": "abc123"}))
result = get_commit("owner", "repo", "abc123")
assert route.called
assert result["sha"] == "abc123"
@respx.mock
def test_get_commit_api_error():
respx.get(...).mock(return_value=httpx.Response(404, json={"message": "not found"}))
with pytest.raises(RuntimeError, match="404"):
get_commit("owner", "repo", "deadbeef")
```
### Smoke test: tool registration
```python
# tests/smoke/test_registration.py
from server.gitea_mcp import mcp
EXPECTED_TOOL_COUNT = 66 # update when adding tools
def test_all_tools_registered():
tools = mcp.list_tools() # or equivalent FastMCP introspection
assert len(tools) == EXPECTED_TOOL_COUNT
```
Update `EXPECTED_TOOL_COUNT` whenever you add or remove tools. If the count drifts from
the README table, the smoke test catches it.
### Running tests
```bash
# All tests
uv run pytest
# With coverage
uv run pytest --cov=server --cov-report=term-missing
# Smoke only
uv run pytest tests/smoke/
# One module
uv run pytest tests/unit/test_commits.py -v
```
### Coverage target
- **Unit tests**: 90 % line coverage on `server/gitea_mcp.py`
- **Smoke tests**: 100 % tool registration (no gaps)
- Integration tests are opt-in (require `GITEA_TEST_URL` + `GITEA_TEST_TOKEN` env vars)
### Integration tests (opt-in)
Integration tests live in `tests/integration/` and are skipped unless the environment
variables `GITEA_TEST_URL` and `GITEA_TEST_TOKEN` are set. Use a dedicated test
organisation/user on a throwaway Gitea instance — tests create and delete real resources.
```python
import pytest, os
pytestmark = pytest.mark.skipif(
not os.environ.get("GITEA_TEST_URL"),
reason="integration tests require GITEA_TEST_URL"
)
```
For local development, a Docker Compose file (`docker-compose.test.yml`) can spin up a
fresh Gitea instance automatically.