Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions tests/test_write_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""github_create_issue — the first built-out write tool.

These exercise the tool's command construction and error handling WITHOUT touching
GitHub: ``run_gh`` is patched in the ``ghplugin.write_tools`` namespace (where the tool
looks it up), so we assert the exact ``gh issue create`` argv and the readable error
strings. asyncio_mode=auto (see pyproject) runs the async tests directly.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, patch

from ghplugin.write_tools import get_write_tools


def _create_issue():
return {t.name: t for t in get_write_tools()}["github_create_issue"]


def _labels_in(args: list[str]) -> list[str]:
"""The value following each ``--label`` flag, in order."""
return [args[i + 1] for i, a in enumerate(args) if a == "--label"]


async def test_returns_issue_url():
"""On success the tool returns the new issue URL (gh's stdout, stripped)."""
tool = _create_issue()
fake = AsyncMock(return_value=(0, "https://github.com/o/n/issues/42\n", ""))
with patch("ghplugin.write_tools.run_gh", fake):
out = await tool.ainvoke({"repo": "o/n", "title": "Bug", "body": "It broke"})
assert out == "https://github.com/o/n/issues/42"
args = fake.call_args.args[0]
assert args[:4] == ["issue", "create", "--repo", "o/n"]
assert "--title" in args and args[args.index("--title") + 1] == "Bug"
assert "--body" in args and args[args.index("--body") + 1] == "It broke"


async def test_no_labels_omits_label_flag():
"""No labels → no --label flags at all."""
tool = _create_issue()
fake = AsyncMock(return_value=(0, "https://github.com/o/n/issues/1", ""))
with patch("ghplugin.write_tools.run_gh", fake):
await tool.ainvoke({"repo": "o/n", "title": "t"})
assert "--label" not in fake.call_args.args[0]


async def test_labels_split_into_separate_flags():
"""Comma-separated labels become one --label flag each, stripped; blanks dropped."""
tool = _create_issue()
fake = AsyncMock(return_value=(0, "https://github.com/o/n/issues/7", ""))
with patch("ghplugin.write_tools.run_gh", fake):
await tool.ainvoke({"repo": "o/n", "title": "t", "labels": "bug, enhancement ,"})
assert _labels_in(fake.call_args.args[0]) == ["bug", "enhancement"]


async def test_bad_repo_short_circuits():
"""An invalid repo returns the bad_repo error and never shells out."""
tool = _create_issue()
fake = AsyncMock()
with patch("ghplugin.write_tools.run_gh", fake):
out = await tool.ainvoke({"repo": "not-a-repo", "title": "t"})
assert out.startswith("Error:")
assert "owner/name" in out
fake.assert_not_called()


async def test_gh_failure_returns_check_gh_error():
"""A nonzero gh exit becomes a readable Error string (via check_gh_error)."""
tool = _create_issue()
fake = AsyncMock(return_value=(1, "", "could not create issue: forbidden"))
with patch("ghplugin.write_tools.run_gh", fake):
out = await tool.ainvoke({"repo": "o/n", "title": "t"})
assert out == "Error (gh exit 1): could not create issue: forbidden"
14 changes: 11 additions & 3 deletions write_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from langchain_core.tools import tool

from .gh_cli import bad_repo
from .gh_cli import bad_repo, check_gh_error, run_gh


def get_write_tools() -> list:
Expand All @@ -36,11 +36,19 @@ async def github_create_issue(repo: str, title: str, body: str = "", labels: str
body: Issue body (Markdown).
labels: Optional comma-separated label names.

TODO(team): implement via `gh issue create`. Return the new issue URL.
Returns the new issue URL.
"""
if err := bad_repo(repo):
return err
return "Error: github_create_issue is not implemented yet (stub — to be built by the team)."
args = ["issue", "create", "--repo", repo, "--title", title, "--body", body]
for label in labels.split(","):
label = label.strip()
if label:
args += ["--label", label]
rc, out, serr = await run_gh(args)
if gh_err := check_gh_error(rc, serr):
return gh_err
return out.strip()

@tool
async def github_comment(repo: str, number: int, body: str) -> str:
Expand Down
Loading