feat: add PR/release/branch/milestone/comment write tools
Adds six opt-in write tools (write-mode + policy + per-user permission still
enforced; no destructive or admin actions):
- create_pull_request (POST /pulls)
- create_release / edit_release (POST/PATCH /releases)
- create_branch (POST /branches; create only, no deletion)
- create_milestone (POST /milestones)
- edit_issue_comment (PATCH /issues/comments/{id})
Each: arg schema (extra=forbid, GitRef on branch/ref-like fields), Gitea client
method with url-encoded path segments, handler that surfaces auth errors, MCP
registration (write_operation=True), server wiring, docs, and success tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,15 @@ Scope requirements:
|
||||
- `assign_issue` (`owner`, `repo`, `issue_number`, `assignees`)
|
||||
- `create_label` (`owner`, `repo`, `name`, `color` hex e.g. `#00aabb`, optional `description`, `exclusive`)
|
||||
- `update_label` (`owner`, `repo`, `name`, one or more of `new_name`, `color`, `description`)
|
||||
- `create_pull_request` (`owner`, `repo`, `title`, `head`, `base`, optional `body`)
|
||||
- `create_release` (`owner`, `repo`, `tag_name`, optional `name`, `body`, `draft`, `prerelease`, `target`)
|
||||
- `edit_release` (`owner`, `repo`, `release_id`, one or more of `name`, `body`, `draft`, `prerelease`)
|
||||
- `create_branch` (`owner`, `repo`, `new_branch_name`, optional `old_branch_name`)
|
||||
- `create_milestone` (`owner`, `repo`, `title`, optional `description`, `due_on`)
|
||||
- `edit_issue_comment` (`owner`, `repo`, `comment_id`, `body`)
|
||||
|
||||
Not supported by design: merge, branch/label/release deletion, force push, repo/admin
|
||||
management.
|
||||
|
||||
Note: `create_issue`, `add_labels`, and `remove_labels` accept label **names**; the
|
||||
server resolves them to Gitea label ids and returns a clear error for unknown labels.
|
||||
|
||||
@@ -840,3 +840,152 @@ class GiteaClient:
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def create_pull_request(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
title: str,
|
||||
head: str,
|
||||
base: str,
|
||||
body: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Open a pull request from head into base."""
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/pulls",
|
||||
json_body={"title": title, "head": head, "base": base, "body": body},
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(
|
||||
tool_name="create_pull_request", result_status="pending"
|
||||
)
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def create_release(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
tag_name: str,
|
||||
name: str = "",
|
||||
body: str = "",
|
||||
draft: bool = False,
|
||||
prerelease: bool = False,
|
||||
target: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a release for an existing or new tag."""
|
||||
payload: dict[str, Any] = {
|
||||
"tag_name": tag_name,
|
||||
"name": name or tag_name,
|
||||
"body": body,
|
||||
"draft": draft,
|
||||
"prerelease": prerelease,
|
||||
}
|
||||
if target:
|
||||
payload["target_commitish"] = target
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/releases",
|
||||
json_body=payload,
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(tool_name="create_release", result_status="pending")
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def edit_release(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
release_id: int,
|
||||
*,
|
||||
name: str | None = None,
|
||||
body: str | None = None,
|
||||
draft: bool | None = None,
|
||||
prerelease: bool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Edit fields of an existing release."""
|
||||
payload: dict[str, Any] = {}
|
||||
if name is not None:
|
||||
payload["name"] = name
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
if draft is not None:
|
||||
payload["draft"] = draft
|
||||
if prerelease is not None:
|
||||
payload["prerelease"] = prerelease
|
||||
result = await self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/releases/{release_id}",
|
||||
json_body=payload,
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(tool_name="edit_release", result_status="pending")
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def create_branch(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
new_branch_name: str,
|
||||
old_branch_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a branch, optionally from a specific existing branch."""
|
||||
payload: dict[str, Any] = {"new_branch_name": new_branch_name}
|
||||
if old_branch_name:
|
||||
payload["old_branch_name"] = old_branch_name
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/branches",
|
||||
json_body=payload,
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(tool_name="create_branch", result_status="pending")
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def create_milestone(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
title: str,
|
||||
description: str = "",
|
||||
due_on: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a repository milestone."""
|
||||
payload: dict[str, Any] = {"title": title, "description": description}
|
||||
if due_on:
|
||||
payload["due_on"] = due_on
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/milestones",
|
||||
json_body=payload,
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(
|
||||
tool_name="create_milestone", result_status="pending"
|
||||
)
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
async def edit_issue_comment(
|
||||
self, owner: str, repo: str, comment_id: int, body: str
|
||||
) -> dict[str, Any]:
|
||||
"""Edit an existing issue or PR comment."""
|
||||
result = await self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues/comments/{comment_id}",
|
||||
json_body={"body": body},
|
||||
correlation_id=str(
|
||||
self.audit.log_tool_invocation(
|
||||
tool_name="edit_issue_comment", result_status="pending"
|
||||
)
|
||||
),
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
@@ -426,6 +426,112 @@ AVAILABLE_TOOLS: list[MCPTool] = [
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_pull_request",
|
||||
"Open a pull request from head into base (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"head": {"type": "string", "description": "Source branch"},
|
||||
"base": {"type": "string", "description": "Target branch"},
|
||||
"body": {"type": "string", "default": ""},
|
||||
},
|
||||
"required": ["owner", "repo", "title", "head", "base"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_release",
|
||||
"Create a release for a tag (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"tag_name": {"type": "string"},
|
||||
"name": {"type": "string", "default": ""},
|
||||
"body": {"type": "string", "default": ""},
|
||||
"draft": {"type": "boolean", "default": False},
|
||||
"prerelease": {"type": "boolean", "default": False},
|
||||
"target": {"type": "string", "description": "Target commitish/branch"},
|
||||
},
|
||||
"required": ["owner", "repo", "tag_name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"edit_release",
|
||||
"Edit an existing release (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"release_id": {"type": "integer", "minimum": 1},
|
||||
"name": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
"draft": {"type": "boolean"},
|
||||
"prerelease": {"type": "boolean"},
|
||||
},
|
||||
"required": ["owner", "repo", "release_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_branch",
|
||||
"Create a branch, optionally from an existing branch (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"new_branch_name": {"type": "string"},
|
||||
"old_branch_name": {"type": "string", "description": "Source branch (optional)"},
|
||||
},
|
||||
"required": ["owner", "repo", "new_branch_name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"create_milestone",
|
||||
"Create a repository milestone (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string", "default": ""},
|
||||
"due_on": {"type": "string", "description": "ISO8601 due date (optional)"},
|
||||
},
|
||||
"required": ["owner", "repo", "title"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
_tool(
|
||||
"edit_issue_comment",
|
||||
"Edit an existing issue or PR comment (write-mode only).",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"comment_id": {"type": "integer", "minimum": 1},
|
||||
"body": {"type": "string"},
|
||||
},
|
||||
"required": ["owner", "repo", "comment_id", "body"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
write_operation=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -81,10 +81,16 @@ from aegis_gitea_mcp.tools.repository import (
|
||||
from aegis_gitea_mcp.tools.write_tools import (
|
||||
add_labels_tool,
|
||||
assign_issue_tool,
|
||||
create_branch_tool,
|
||||
create_issue_comment_tool,
|
||||
create_issue_tool,
|
||||
create_label_tool,
|
||||
create_milestone_tool,
|
||||
create_pr_comment_tool,
|
||||
create_pull_request_tool,
|
||||
create_release_tool,
|
||||
edit_issue_comment_tool,
|
||||
edit_release_tool,
|
||||
remove_labels_tool,
|
||||
update_issue_tool,
|
||||
update_label_tool,
|
||||
@@ -348,6 +354,12 @@ TOOL_HANDLERS: dict[str, ToolHandler] = {
|
||||
"create_label": create_label_tool,
|
||||
"update_label": update_label_tool,
|
||||
"remove_labels": remove_labels_tool,
|
||||
"create_pull_request": create_pull_request_tool,
|
||||
"create_release": create_release_tool,
|
||||
"edit_release": edit_release_tool,
|
||||
"create_branch": create_branch_tool,
|
||||
"create_milestone": create_milestone_tool,
|
||||
"edit_issue_comment": edit_issue_comment_tool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -253,6 +253,70 @@ class RemoveLabelsArgs(RepositoryArgs):
|
||||
labels: list[str] = Field(..., min_length=1, max_length=20)
|
||||
|
||||
|
||||
class CreatePullRequestArgs(RepositoryArgs):
|
||||
"""Arguments for create_pull_request."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=256)
|
||||
head: GitRef = Field(..., min_length=1, max_length=200)
|
||||
base: GitRef = Field(..., min_length=1, max_length=200)
|
||||
body: str = Field(default="", max_length=20_000)
|
||||
|
||||
|
||||
class CreateReleaseArgs(RepositoryArgs):
|
||||
"""Arguments for create_release."""
|
||||
|
||||
tag_name: GitRef = Field(..., min_length=1, max_length=200)
|
||||
name: str = Field(default="", max_length=256)
|
||||
body: str = Field(default="", max_length=20_000)
|
||||
draft: bool = Field(default=False)
|
||||
prerelease: bool = Field(default=False)
|
||||
target: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
|
||||
|
||||
class EditReleaseArgs(RepositoryArgs):
|
||||
"""Arguments for edit_release."""
|
||||
|
||||
release_id: int = Field(..., ge=1)
|
||||
name: str | None = Field(default=None, max_length=256)
|
||||
body: str | None = Field(default=None, max_length=20_000)
|
||||
draft: bool | None = Field(default=None)
|
||||
prerelease: bool | None = Field(default=None)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def require_change(self) -> EditReleaseArgs:
|
||||
"""Require at least one mutable field in the update payload."""
|
||||
if (
|
||||
self.name is None
|
||||
and self.body is None
|
||||
and self.draft is None
|
||||
and self.prerelease is None
|
||||
):
|
||||
raise ValueError("At least one of name, body, draft, or prerelease must be provided")
|
||||
return self
|
||||
|
||||
|
||||
class CreateBranchArgs(RepositoryArgs):
|
||||
"""Arguments for create_branch."""
|
||||
|
||||
new_branch_name: GitRef = Field(..., min_length=1, max_length=200)
|
||||
old_branch_name: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
|
||||
|
||||
class CreateMilestoneArgs(RepositoryArgs):
|
||||
"""Arguments for create_milestone."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=256)
|
||||
description: str = Field(default="", max_length=10_000)
|
||||
due_on: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class EditIssueCommentArgs(RepositoryArgs):
|
||||
"""Arguments for edit_issue_comment."""
|
||||
|
||||
comment_id: int = Field(..., ge=1)
|
||||
body: str = Field(..., min_length=1, max_length=10_000)
|
||||
|
||||
|
||||
def extract_repository(arguments: dict[str, object]) -> str | None:
|
||||
"""Extract `owner/repo` from raw argument mapping.
|
||||
|
||||
|
||||
@@ -14,10 +14,16 @@ from aegis_gitea_mcp.response_limits import limit_text
|
||||
from aegis_gitea_mcp.tools.arguments import (
|
||||
AddLabelsArgs,
|
||||
AssignIssueArgs,
|
||||
CreateBranchArgs,
|
||||
CreateIssueArgs,
|
||||
CreateIssueCommentArgs,
|
||||
CreateLabelArgs,
|
||||
CreateMilestoneArgs,
|
||||
CreatePrCommentArgs,
|
||||
CreatePullRequestArgs,
|
||||
CreateReleaseArgs,
|
||||
EditIssueCommentArgs,
|
||||
EditReleaseArgs,
|
||||
RemoveLabelsArgs,
|
||||
UpdateIssueArgs,
|
||||
UpdateLabelArgs,
|
||||
@@ -249,3 +255,147 @@ async def assign_issue_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> di
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to assign issue: {exc}") from exc
|
||||
|
||||
|
||||
async def create_pull_request_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Open a pull request in write mode."""
|
||||
parsed = CreatePullRequestArgs.model_validate(arguments)
|
||||
try:
|
||||
pull = await gitea.create_pull_request(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
title=parsed.title,
|
||||
head=parsed.head,
|
||||
base=parsed.base,
|
||||
body=parsed.body,
|
||||
)
|
||||
return {
|
||||
"number": pull.get("number", 0),
|
||||
"title": limit_text(str(pull.get("title", ""))),
|
||||
"state": pull.get("state", ""),
|
||||
"url": pull.get("html_url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to create pull request: {exc}") from exc
|
||||
|
||||
|
||||
async def create_release_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a release in write mode."""
|
||||
parsed = CreateReleaseArgs.model_validate(arguments)
|
||||
try:
|
||||
release = await gitea.create_release(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
tag_name=parsed.tag_name,
|
||||
name=parsed.name,
|
||||
body=parsed.body,
|
||||
draft=parsed.draft,
|
||||
prerelease=parsed.prerelease,
|
||||
target=parsed.target,
|
||||
)
|
||||
return {
|
||||
"id": release.get("id", 0),
|
||||
"tag_name": release.get("tag_name", ""),
|
||||
"name": limit_text(str(release.get("name", ""))),
|
||||
"draft": release.get("draft", False),
|
||||
"prerelease": release.get("prerelease", False),
|
||||
"url": release.get("html_url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to create release: {exc}") from exc
|
||||
|
||||
|
||||
async def edit_release_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Edit an existing release in write mode."""
|
||||
parsed = EditReleaseArgs.model_validate(arguments)
|
||||
try:
|
||||
release = await gitea.edit_release(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
parsed.release_id,
|
||||
name=parsed.name,
|
||||
body=parsed.body,
|
||||
draft=parsed.draft,
|
||||
prerelease=parsed.prerelease,
|
||||
)
|
||||
return {
|
||||
"id": release.get("id", parsed.release_id),
|
||||
"tag_name": release.get("tag_name", ""),
|
||||
"name": limit_text(str(release.get("name", ""))),
|
||||
"draft": release.get("draft", False),
|
||||
"prerelease": release.get("prerelease", False),
|
||||
"url": release.get("html_url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to edit release: {exc}") from exc
|
||||
|
||||
|
||||
async def create_branch_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a branch in write mode."""
|
||||
parsed = CreateBranchArgs.model_validate(arguments)
|
||||
try:
|
||||
branch = await gitea.create_branch(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
new_branch_name=parsed.new_branch_name,
|
||||
old_branch_name=parsed.old_branch_name,
|
||||
)
|
||||
commit = branch.get("commit", {}) if isinstance(branch, dict) else {}
|
||||
return {
|
||||
"name": branch.get("name", parsed.new_branch_name),
|
||||
"commit": commit.get("id", "") if isinstance(commit, dict) else "",
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to create branch: {exc}") from exc
|
||||
|
||||
|
||||
async def create_milestone_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a milestone in write mode."""
|
||||
parsed = CreateMilestoneArgs.model_validate(arguments)
|
||||
try:
|
||||
milestone = await gitea.create_milestone(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
title=parsed.title,
|
||||
description=parsed.description,
|
||||
due_on=parsed.due_on,
|
||||
)
|
||||
return {
|
||||
"id": milestone.get("id", 0),
|
||||
"title": limit_text(str(milestone.get("title", ""))),
|
||||
"state": milestone.get("state", ""),
|
||||
"url": milestone.get("html_url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to create milestone: {exc}") from exc
|
||||
|
||||
|
||||
async def edit_issue_comment_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Edit an existing issue or PR comment in write mode."""
|
||||
parsed = EditIssueCommentArgs.model_validate(arguments)
|
||||
try:
|
||||
comment = await gitea.edit_issue_comment(
|
||||
parsed.owner,
|
||||
parsed.repo,
|
||||
parsed.comment_id,
|
||||
parsed.body,
|
||||
)
|
||||
return {
|
||||
"id": comment.get("id", parsed.comment_id),
|
||||
"body": limit_text(str(comment.get("body", ""))),
|
||||
"url": comment.get("html_url", ""),
|
||||
}
|
||||
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||
raise
|
||||
except GiteaError as exc:
|
||||
raise RuntimeError(f"Failed to edit comment: {exc}") from exc
|
||||
|
||||
@@ -20,10 +20,16 @@ from aegis_gitea_mcp.tools.read_tools import (
|
||||
from aegis_gitea_mcp.tools.write_tools import (
|
||||
add_labels_tool,
|
||||
assign_issue_tool,
|
||||
create_branch_tool,
|
||||
create_issue_comment_tool,
|
||||
create_issue_tool,
|
||||
create_label_tool,
|
||||
create_milestone_tool,
|
||||
create_pr_comment_tool,
|
||||
create_pull_request_tool,
|
||||
create_release_tool,
|
||||
edit_issue_comment_tool,
|
||||
edit_release_tool,
|
||||
remove_labels_tool,
|
||||
update_issue_tool,
|
||||
update_label_tool,
|
||||
@@ -114,6 +120,28 @@ class StubGitea:
|
||||
async def remove_labels(self, owner, repo, index, labels):
|
||||
return []
|
||||
|
||||
async def create_pull_request(self, owner, repo, *, title, head, base, body=""):
|
||||
return {"number": 7, "title": title, "state": "open"}
|
||||
|
||||
async def create_release(
|
||||
self, owner, repo, *, tag_name, name="", body="", draft=False, prerelease=False, target=None
|
||||
):
|
||||
return {"id": 3, "tag_name": tag_name, "name": name or tag_name}
|
||||
|
||||
async def edit_release(
|
||||
self, owner, repo, release_id, *, name=None, body=None, draft=None, prerelease=None
|
||||
):
|
||||
return {"id": release_id, "tag_name": "v1", "name": name or "rel"}
|
||||
|
||||
async def create_branch(self, owner, repo, *, new_branch_name, old_branch_name=None):
|
||||
return {"name": new_branch_name, "commit": {"id": "abc"}}
|
||||
|
||||
async def create_milestone(self, owner, repo, *, title, description="", due_on=None):
|
||||
return {"id": 4, "title": title, "state": "open"}
|
||||
|
||||
async def edit_issue_comment(self, owner, repo, comment_id, body):
|
||||
return {"id": comment_id, "body": body}
|
||||
|
||||
|
||||
class ErrorGitea(StubGitea):
|
||||
"""Stub that raises backend errors for failure-mode coverage."""
|
||||
@@ -201,6 +229,36 @@ async def test_extended_read_tools_failure_mode() -> None:
|
||||
{"owner": "acme", "repo": "app", "issue_number": 1, "labels": ["bug"]},
|
||||
"removed",
|
||||
),
|
||||
(
|
||||
create_pull_request_tool,
|
||||
{"owner": "acme", "repo": "app", "title": "PR", "head": "feature", "base": "main"},
|
||||
"number",
|
||||
),
|
||||
(
|
||||
create_release_tool,
|
||||
{"owner": "acme", "repo": "app", "tag_name": "v1.0.0"},
|
||||
"id",
|
||||
),
|
||||
(
|
||||
edit_release_tool,
|
||||
{"owner": "acme", "repo": "app", "release_id": 3, "name": "x"},
|
||||
"id",
|
||||
),
|
||||
(
|
||||
create_branch_tool,
|
||||
{"owner": "acme", "repo": "app", "new_branch_name": "feature/x"},
|
||||
"name",
|
||||
),
|
||||
(
|
||||
create_milestone_tool,
|
||||
{"owner": "acme", "repo": "app", "title": "M1"},
|
||||
"id",
|
||||
),
|
||||
(
|
||||
edit_issue_comment_tool,
|
||||
{"owner": "acme", "repo": "app", "comment_id": 5, "body": "edited"},
|
||||
"id",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_write_tools_success(tool, args, expected_key):
|
||||
|
||||
Reference in New Issue
Block a user