feat: complete label management (name->id resolution, update/remove)
Resolves the long-standing problem that label tools passed names while Gitea's API requires numeric label ids. - gitea_client: add _resolve_label_ids() helper; create_issue and add_labels now resolve label names to ids (case-insensitive) and raise a clear "Unknown label(s)" error instead of a generic 500. - New tools: remove_labels (by name) and update_label (located by current name). - Register both write tools and document the name-based label contract. - Tests: resolver mapping + unknown-label error, add_labels id translation, update_label and remove_labels handlers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -65,9 +65,14 @@ Scope requirements:
|
|||||||
- `update_issue` (`owner`, `repo`, `issue_number`, one or more of `title`, `body`, `state`)
|
- `update_issue` (`owner`, `repo`, `issue_number`, one or more of `title`, `body`, `state`)
|
||||||
- `create_issue_comment` (`owner`, `repo`, `issue_number`, `body`)
|
- `create_issue_comment` (`owner`, `repo`, `issue_number`, `body`)
|
||||||
- `create_pr_comment` (`owner`, `repo`, `pull_number`, `body`)
|
- `create_pr_comment` (`owner`, `repo`, `pull_number`, `body`)
|
||||||
- `add_labels` (`owner`, `repo`, `issue_number`, `labels`)
|
- `add_labels` (`owner`, `repo`, `issue_number`, `labels` by name)
|
||||||
|
- `remove_labels` (`owner`, `repo`, `issue_number`, `labels` by name)
|
||||||
- `assign_issue` (`owner`, `repo`, `issue_number`, `assignees`)
|
- `assign_issue` (`owner`, `repo`, `issue_number`, `assignees`)
|
||||||
- `create_label` (`owner`, `repo`, `name`, `color` hex e.g. `#00aabb`, optional `description`, `exclusive`)
|
- `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`)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Validation and Limits
|
## Validation and Limits
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,45 @@ class GiteaClient:
|
|||||||
)
|
)
|
||||||
return result if isinstance(result, list) else []
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def _resolve_label_ids(
|
||||||
|
self, owner: str, repo: str, names: list[str], *, correlation_id: str
|
||||||
|
) -> list[int]:
|
||||||
|
"""Resolve label names to Gitea label ids for a repository.
|
||||||
|
|
||||||
|
Gitea's issue/label APIs require numeric label ids, not names. This maps
|
||||||
|
the caller-supplied names (case-insensitive) to ids and raises a clear
|
||||||
|
error for any name that does not exist in the repository.
|
||||||
|
"""
|
||||||
|
existing = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/labels",
|
||||||
|
params={"limit": 100},
|
||||||
|
correlation_id=correlation_id,
|
||||||
|
)
|
||||||
|
by_name: dict[str, int] = {}
|
||||||
|
if isinstance(existing, list):
|
||||||
|
for item in existing:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
label_name = str(item.get("name", ""))
|
||||||
|
label_id = item.get("id")
|
||||||
|
if label_name and isinstance(label_id, int):
|
||||||
|
by_name[label_name.lower()] = label_id
|
||||||
|
|
||||||
|
ids: list[int] = []
|
||||||
|
unknown: list[str] = []
|
||||||
|
for name in names:
|
||||||
|
match = by_name.get(name.strip().lower())
|
||||||
|
if match is None:
|
||||||
|
unknown.append(name)
|
||||||
|
else:
|
||||||
|
ids.append(match)
|
||||||
|
if unknown:
|
||||||
|
raise GiteaError(
|
||||||
|
f"Unknown label(s) for {owner}/{repo}: {', '.join(unknown)}. "
|
||||||
|
"Create them first with create_label."
|
||||||
|
)
|
||||||
|
return ids
|
||||||
|
|
||||||
async def create_issue(
|
async def create_issue(
|
||||||
self,
|
self,
|
||||||
owner: str,
|
owner: str,
|
||||||
@@ -593,18 +632,21 @@ class GiteaClient:
|
|||||||
assignees: list[str] | None = None,
|
assignees: list[str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create repository issue."""
|
"""Create repository issue."""
|
||||||
|
correlation_id = str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="create_issue", result_status="pending")
|
||||||
|
)
|
||||||
payload: dict[str, Any] = {"title": title, "body": body}
|
payload: dict[str, Any] = {"title": title, "body": body}
|
||||||
if labels:
|
if labels:
|
||||||
payload["labels"] = labels
|
payload["labels"] = await self._resolve_label_ids(
|
||||||
|
owner, repo, labels, correlation_id=correlation_id
|
||||||
|
)
|
||||||
if assignees:
|
if assignees:
|
||||||
payload["assignees"] = assignees
|
payload["assignees"] = assignees
|
||||||
result = await self._request(
|
result = await self._request(
|
||||||
"POST",
|
"POST",
|
||||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues",
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues",
|
||||||
json_body=payload,
|
json_body=payload,
|
||||||
correlation_id=str(
|
correlation_id=correlation_id,
|
||||||
self.audit.log_tool_invocation(tool_name="create_issue", result_status="pending")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return result if isinstance(result, dict) else {}
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
@@ -702,14 +744,82 @@ class GiteaClient:
|
|||||||
index: int,
|
index: int,
|
||||||
labels: list[str],
|
labels: list[str],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Add labels to issue/PR."""
|
"""Add labels to issue/PR by label name."""
|
||||||
|
correlation_id = str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="add_labels", result_status="pending")
|
||||||
|
)
|
||||||
|
label_ids = await self._resolve_label_ids(
|
||||||
|
owner, repo, labels, correlation_id=correlation_id
|
||||||
|
)
|
||||||
result = await self._request(
|
result = await self._request(
|
||||||
"POST",
|
"POST",
|
||||||
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues/{index}/labels",
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/issues/{index}/labels",
|
||||||
json_body={"labels": labels},
|
json_body={"labels": label_ids},
|
||||||
correlation_id=str(
|
correlation_id=correlation_id,
|
||||||
self.audit.log_tool_invocation(tool_name="add_labels", result_status="pending")
|
)
|
||||||
),
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
async def remove_labels(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
index: int,
|
||||||
|
labels: list[str],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Remove the given labels (by name) from an issue/PR.
|
||||||
|
|
||||||
|
Returns the issue's remaining labels.
|
||||||
|
"""
|
||||||
|
correlation_id = str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="remove_labels", result_status="pending")
|
||||||
|
)
|
||||||
|
label_ids = await self._resolve_label_ids(
|
||||||
|
owner, repo, labels, correlation_id=correlation_id
|
||||||
|
)
|
||||||
|
owner_q = quote(owner, safe="")
|
||||||
|
repo_q = quote(repo, safe="")
|
||||||
|
for label_id in label_ids:
|
||||||
|
await self._request(
|
||||||
|
"DELETE",
|
||||||
|
f"/api/v1/repos/{owner_q}/{repo_q}/issues/{index}/labels/{label_id}",
|
||||||
|
correlation_id=correlation_id,
|
||||||
|
)
|
||||||
|
result = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/v1/repos/{owner_q}/{repo_q}/issues/{index}/labels",
|
||||||
|
correlation_id=correlation_id,
|
||||||
|
)
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
async def update_label(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
new_name: str | None = None,
|
||||||
|
color: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Update an existing label, located by its current name."""
|
||||||
|
correlation_id = str(
|
||||||
|
self.audit.log_tool_invocation(tool_name="update_label", result_status="pending")
|
||||||
|
)
|
||||||
|
label_ids = await self._resolve_label_ids(
|
||||||
|
owner, repo, [name], correlation_id=correlation_id
|
||||||
|
)
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
if new_name is not None:
|
||||||
|
payload["name"] = new_name
|
||||||
|
if color is not None:
|
||||||
|
payload["color"] = color
|
||||||
|
if description is not None:
|
||||||
|
payload["description"] = description
|
||||||
|
result = await self._request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repo, safe='')}/labels/{label_ids[0]}",
|
||||||
|
json_body=payload,
|
||||||
|
correlation_id=correlation_id,
|
||||||
)
|
)
|
||||||
return result if isinstance(result, dict) else {}
|
return result if isinstance(result, dict) else {}
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,40 @@ AVAILABLE_TOOLS: list[MCPTool] = [
|
|||||||
},
|
},
|
||||||
write_operation=True,
|
write_operation=True,
|
||||||
),
|
),
|
||||||
|
_tool(
|
||||||
|
"update_label",
|
||||||
|
"Update an existing repository label, located by its current name (write-mode only).",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"name": {"type": "string", "description": "Current label name"},
|
||||||
|
"new_name": {"type": "string"},
|
||||||
|
"color": {"type": "string", "description": "Hex color, e.g. #00aabb"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "name"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
write_operation=True,
|
||||||
|
),
|
||||||
|
_tool(
|
||||||
|
"remove_labels",
|
||||||
|
"Remove labels (by name) from an issue or pull request (write-mode only).",
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"owner": {"type": "string"},
|
||||||
|
"repo": {"type": "string"},
|
||||||
|
"issue_number": {"type": "integer", "minimum": 1},
|
||||||
|
"labels": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
||||||
|
},
|
||||||
|
"required": ["owner", "repo", "issue_number", "labels"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
write_operation=True,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ from aegis_gitea_mcp.tools.write_tools import (
|
|||||||
create_issue_tool,
|
create_issue_tool,
|
||||||
create_label_tool,
|
create_label_tool,
|
||||||
create_pr_comment_tool,
|
create_pr_comment_tool,
|
||||||
|
remove_labels_tool,
|
||||||
update_issue_tool,
|
update_issue_tool,
|
||||||
|
update_label_tool,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -344,6 +346,8 @@ TOOL_HANDLERS: dict[str, ToolHandler] = {
|
|||||||
"add_labels": add_labels_tool,
|
"add_labels": add_labels_tool,
|
||||||
"assign_issue": assign_issue_tool,
|
"assign_issue": assign_issue_tool,
|
||||||
"create_label": create_label_tool,
|
"create_label": create_label_tool,
|
||||||
|
"update_label": update_label_tool,
|
||||||
|
"remove_labels": remove_labels_tool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,29 @@ class CreateLabelArgs(RepositoryArgs):
|
|||||||
exclusive: bool = Field(default=False)
|
exclusive: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateLabelArgs(RepositoryArgs):
|
||||||
|
"""Arguments for update_label (located by current name)."""
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=50)
|
||||||
|
new_name: str | None = Field(default=None, min_length=1, max_length=50)
|
||||||
|
color: str | None = Field(default=None, pattern=r"^#?[0-9A-Fa-f]{6}$")
|
||||||
|
description: str | None = Field(default=None, max_length=1000)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def require_change(self) -> UpdateLabelArgs:
|
||||||
|
"""Require at least one mutable field in the update payload."""
|
||||||
|
if self.new_name is None and self.color is None and self.description is None:
|
||||||
|
raise ValueError("At least one of new_name, color, or description must be provided")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveLabelsArgs(RepositoryArgs):
|
||||||
|
"""Arguments for remove_labels."""
|
||||||
|
|
||||||
|
issue_number: int = Field(..., ge=1)
|
||||||
|
labels: list[str] = Field(..., min_length=1, max_length=20)
|
||||||
|
|
||||||
|
|
||||||
def extract_repository(arguments: dict[str, object]) -> str | None:
|
def extract_repository(arguments: dict[str, object]) -> str | None:
|
||||||
"""Extract `owner/repo` from raw argument mapping.
|
"""Extract `owner/repo` from raw argument mapping.
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ from aegis_gitea_mcp.tools.arguments import (
|
|||||||
CreateIssueCommentArgs,
|
CreateIssueCommentArgs,
|
||||||
CreateLabelArgs,
|
CreateLabelArgs,
|
||||||
CreatePrCommentArgs,
|
CreatePrCommentArgs,
|
||||||
|
RemoveLabelsArgs,
|
||||||
UpdateIssueArgs,
|
UpdateIssueArgs,
|
||||||
|
UpdateLabelArgs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -51,6 +53,55 @@ async def create_label_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> di
|
|||||||
raise RuntimeError(f"Failed to create label: {exc}") from exc
|
raise RuntimeError(f"Failed to create label: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def update_label_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Update an existing repository label (located by current name)."""
|
||||||
|
parsed = UpdateLabelArgs.model_validate(arguments)
|
||||||
|
color = parsed.color
|
||||||
|
if color is not None and not color.startswith("#"):
|
||||||
|
color = f"#{color}"
|
||||||
|
try:
|
||||||
|
label = await gitea.update_label(
|
||||||
|
parsed.owner,
|
||||||
|
parsed.repo,
|
||||||
|
name=parsed.name,
|
||||||
|
new_name=parsed.new_name,
|
||||||
|
color=color,
|
||||||
|
description=parsed.description,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": label.get("id", 0),
|
||||||
|
"name": limit_text(str(label.get("name", ""))),
|
||||||
|
"color": label.get("color", ""),
|
||||||
|
"description": limit_text(str(label.get("description", ""))),
|
||||||
|
"url": label.get("url", ""),
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to update label: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_labels_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Remove labels (by name) from an issue or pull request."""
|
||||||
|
parsed = RemoveLabelsArgs.model_validate(arguments)
|
||||||
|
try:
|
||||||
|
result = await gitea.remove_labels(
|
||||||
|
parsed.owner, parsed.repo, parsed.issue_number, parsed.labels
|
||||||
|
)
|
||||||
|
remaining: list[str] = []
|
||||||
|
if isinstance(result, list):
|
||||||
|
remaining = [label.get("name", "") for label in result if isinstance(label, dict)]
|
||||||
|
return {
|
||||||
|
"issue_number": parsed.issue_number,
|
||||||
|
"removed": parsed.labels,
|
||||||
|
"remaining_labels": remaining,
|
||||||
|
}
|
||||||
|
except (GiteaAuthenticationError, GiteaAuthorizationError):
|
||||||
|
raise
|
||||||
|
except GiteaError as exc:
|
||||||
|
raise RuntimeError(f"Failed to remove labels: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
async def create_issue_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
async def create_issue_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Create a new issue in write mode."""
|
"""Create a new issue in write mode."""
|
||||||
parsed = CreateIssueArgs.model_validate(arguments)
|
parsed = CreateIssueArgs.model_validate(arguments)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ async def test_public_methods_delegate_to_request_and_normalize() -> None:
|
|||||||
if endpoint == "/api/v1/repos/acme/demo/pulls/2":
|
if endpoint == "/api/v1/repos/acme/demo/pulls/2":
|
||||||
return {"number": 2}
|
return {"number": 2}
|
||||||
if endpoint == "/api/v1/repos/acme/demo/labels":
|
if endpoint == "/api/v1/repos/acme/demo/labels":
|
||||||
return [{"name": "bug"}]
|
return [{"id": 1, "name": "bug"}]
|
||||||
if endpoint == "/api/v1/repos/acme/demo/tags":
|
if endpoint == "/api/v1/repos/acme/demo/tags":
|
||||||
return [{"name": "v1"}]
|
return [{"name": "v1"}]
|
||||||
if endpoint == "/api/v1/repos/acme/demo/releases":
|
if endpoint == "/api/v1/repos/acme/demo/releases":
|
||||||
@@ -246,6 +246,51 @@ async def test_list_user_repositories_unknown_user_returns_empty() -> None:
|
|||||||
assert await client.list_user_repositories("ghost") == []
|
assert await client.list_user_repositories("ghost") == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_label_ids_maps_names_case_insensitively() -> None:
|
||||||
|
"""Label names are resolved to ids regardless of case."""
|
||||||
|
client = GiteaClient(token="user-token")
|
||||||
|
|
||||||
|
async def fake_request(method: str, endpoint: str, **kwargs):
|
||||||
|
return [{"id": 3, "name": "Bug"}, {"id": 9, "name": "wontfix"}]
|
||||||
|
|
||||||
|
client._request = AsyncMock(side_effect=fake_request) # type: ignore[method-assign]
|
||||||
|
ids = await client._resolve_label_ids("o", "r", ["bug", "WONTFIX"], correlation_id="c")
|
||||||
|
assert ids == [3, 9]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_label_ids_rejects_unknown_label() -> None:
|
||||||
|
"""An unknown label name raises a clear error instead of a silent failure."""
|
||||||
|
client = GiteaClient(token="user-token")
|
||||||
|
|
||||||
|
async def fake_request(method: str, endpoint: str, **kwargs):
|
||||||
|
return [{"id": 3, "name": "bug"}]
|
||||||
|
|
||||||
|
client._request = AsyncMock(side_effect=fake_request) # type: ignore[method-assign]
|
||||||
|
with pytest.raises(GiteaError, match="Unknown label"):
|
||||||
|
await client._resolve_label_ids("o", "r", ["ghost"], correlation_id="c")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_labels_resolves_names_to_ids() -> None:
|
||||||
|
"""add_labels translates names to ids before POSTing to Gitea."""
|
||||||
|
client = GiteaClient(token="user-token")
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
async def fake_request(method: str, endpoint: str, **kwargs):
|
||||||
|
if endpoint.endswith("/labels") and method == "GET":
|
||||||
|
return [{"id": 42, "name": "bug"}]
|
||||||
|
if endpoint.endswith("/issues/1/labels") and method == "POST":
|
||||||
|
captured["body"] = kwargs.get("json_body")
|
||||||
|
return {"labels": [{"name": "bug"}]}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
client._request = AsyncMock(side_effect=fake_request) # type: ignore[method-assign]
|
||||||
|
await client.add_labels("o", "r", 1, ["bug"])
|
||||||
|
assert captured["body"] == {"labels": [42]}
|
||||||
|
|
||||||
|
|
||||||
def test_git_refs_allow_slash_containing_refs() -> None:
|
def test_git_refs_allow_slash_containing_refs() -> None:
|
||||||
"""Legitimate refs that contain '/' validate successfully."""
|
"""Legitimate refs that contain '/' validate successfully."""
|
||||||
tree = FileTreeArgs(owner="o", repo="r", ref="feature/foo")
|
tree = FileTreeArgs(owner="o", repo="r", ref="feature/foo")
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ from aegis_gitea_mcp.tools.write_tools import (
|
|||||||
create_issue_tool,
|
create_issue_tool,
|
||||||
create_label_tool,
|
create_label_tool,
|
||||||
create_pr_comment_tool,
|
create_pr_comment_tool,
|
||||||
|
remove_labels_tool,
|
||||||
update_issue_tool,
|
update_issue_tool,
|
||||||
|
update_label_tool,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +103,17 @@ class StubGitea:
|
|||||||
async def create_label(self, owner, repo, *, name, color, description="", exclusive=False):
|
async def create_label(self, owner, repo, *, name, color, description="", exclusive=False):
|
||||||
return {"id": 5, "name": name, "color": color, "description": description, "url": "u"}
|
return {"id": 5, "name": name, "color": color, "description": description, "url": "u"}
|
||||||
|
|
||||||
|
async def update_label(self, owner, repo, *, name, new_name=None, color=None, description=None):
|
||||||
|
return {
|
||||||
|
"id": 5,
|
||||||
|
"name": new_name or name,
|
||||||
|
"color": color or "#ffffff",
|
||||||
|
"description": description or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def remove_labels(self, owner, repo, index, labels):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ErrorGitea(StubGitea):
|
class ErrorGitea(StubGitea):
|
||||||
"""Stub that raises backend errors for failure-mode coverage."""
|
"""Stub that raises backend errors for failure-mode coverage."""
|
||||||
@@ -178,6 +191,16 @@ async def test_extended_read_tools_failure_mode() -> None:
|
|||||||
{"owner": "acme", "repo": "app", "name": "bug", "color": "#ff0000"},
|
{"owner": "acme", "repo": "app", "name": "bug", "color": "#ff0000"},
|
||||||
"id",
|
"id",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
update_label_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "name": "bug", "new_name": "defect"},
|
||||||
|
"id",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
remove_labels_tool,
|
||||||
|
{"owner": "acme", "repo": "app", "issue_number": 1, "labels": ["bug"]},
|
||||||
|
"removed",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_write_tools_success(tool, args, expected_key):
|
async def test_write_tools_success(tool, args, expected_key):
|
||||||
|
|||||||
Reference in New Issue
Block a user