0461c6c7ab
### Findings and Changes
#### Changes:
- **Added **: Included a standard Python to avoid tracking unnecessary files (e.g., , , ).
- **Added **: Prepared the project for better dependency management.
- **Enhanced Gitea Tools**:
- Implemented in .
- Implemented (via ) in .
#### Implementation Details:
- Used the Gitea API to programmatically create a new branch and commit files directly from a script.
- Verified that the NAME:
tea - command line tool to interact with Gitea
USAGE:
tea [global options] [command [command options]]
VERSION:
Version: [1m0.14.1[0m golang: 1.26.3 go-sdk: v0.25.1
DESCRIPTION:
tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available.
tea works best in a upstream/fork workflow, when the local main branch tracks the
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
COMMANDS:
help, h Shows a list of commands or help for one command
ENTITIES:
issues, issue, i List, create and update issues
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release, r Manage releases
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Manage repositories
branches, branch, b Consult branches
actions, action Manage repository actions
webhooks, webhook, hooks, hook Manage webhooks
comment, c Add a comment to an issue / pr
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
clone, C Clone a repository locally
api Make an authenticated API request
MISCELLANEOUS:
whoami Show current logged in user
admin, a Operations requiring admin access on the Gitea instance
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
ssh-keys, ssh-key Manage SSH public keys
GLOBAL OPTIONS:
--debug, --vvv Enable debug mode
--help, -h show help
--version, -v print the version CLI can be used for automated PR creation.
- Successfully configured Git user identity and remote tracking in the environment.
---------
Co-authored-by: Michael <michael@example.com>
Reviewed-on: #1
251 lines
9.1 KiB
Python
251 lines
9.1 KiB
Python
import pytest
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
from core.dispatcher import AgentDispatcher
|
|
from core.queue import WorkItem
|
|
from gitea.client import GiteaClient
|
|
from gitea.tools.gitea_tools import GiteaTools
|
|
from gitea.models import PullRequestModel, IssueModel, CommentModel
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
# Mock list_repo_pull_requests to return a PR that closes issue #42
|
|
pr = PullRequestModel(
|
|
number=101,
|
|
title="fix: resolve bug",
|
|
body="closes #42"
|
|
)
|
|
mock_client.list_repo_pull_requests.return_value = [pr]
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
|
|
work_item = WorkItem(
|
|
repo_full_name="meeks/repo1",
|
|
task_type="issue",
|
|
task_number=42,
|
|
task_info=IssueModel(number=42),
|
|
priority=0
|
|
)
|
|
|
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
|
|
|
assert len(results) == 1
|
|
assert "SKIP: A pull request (PR #101) addressing issue #42 already exists" in results[0]
|
|
mock_client.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
|
|
|
|
|
|
@patch("core.dispatcher.CodingAgent")
|
|
async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock) -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
# Mock list_repo_pull_requests to return PRs that don't address issue #42
|
|
pr = PullRequestModel(
|
|
number=101,
|
|
title="feat: add something",
|
|
body="closes #99"
|
|
)
|
|
mock_client.list_repo_pull_requests.return_value = [pr]
|
|
mock_client.get_issue_comments.return_value = []
|
|
|
|
# Mock CodingAgent run_with_tools
|
|
mock_agent_instance = MagicMock()
|
|
mock_agent_instance.run_with_tools = AsyncMock(return_value="Issue resolved.")
|
|
mock_agent_class.return_value = mock_agent_instance
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
|
|
work_item = WorkItem(
|
|
repo_full_name="meeks/repo1",
|
|
task_type="issue",
|
|
task_number=42,
|
|
task_info=IssueModel(number=42, title="test", body="test desc"),
|
|
priority=0
|
|
)
|
|
|
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
|
|
|
assert len(results) == 1
|
|
assert results[0] == "Issue resolved."
|
|
|
|
|
|
@patch("core.dispatcher.CodingAgent")
|
|
async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMock) -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
# Mock get_pull_request, get_pull_request_diff, etc.
|
|
pr = PullRequestModel(
|
|
number=101,
|
|
title="fix: resolve bug",
|
|
body="closes #42",
|
|
head={"ref": "branch1"},
|
|
base={"ref": "master"}
|
|
)
|
|
mock_client.get_pull_request.return_value = pr
|
|
mock_client.get_pull_request_diff.return_value = "diff context"
|
|
mock_client.get_pull_request_files.return_value = []
|
|
mock_client.get_pull_request_comments.return_value = []
|
|
|
|
# Mock the connected issue and its comments
|
|
issue = IssueModel(number=42, title="bug description")
|
|
mock_client.get_issue.return_value = issue
|
|
|
|
comment = CommentModel(id=1, body="First comment")
|
|
mock_client.get_issue_comments.return_value = [comment]
|
|
|
|
# Mock CodingAgent
|
|
mock_agent_instance = MagicMock()
|
|
mock_agent_instance.run_with_tools = AsyncMock(return_value="PR Reviewed.")
|
|
mock_agent_class.return_value = mock_agent_instance
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
|
|
work_item = WorkItem(
|
|
repo_full_name="meeks/repo1",
|
|
task_type="pr",
|
|
task_number=101,
|
|
task_info=PullRequestModel(number=101, title="fix: resolve bug", body="closes #42"),
|
|
priority=0
|
|
)
|
|
|
|
# We will patch the dispatcher._build_pr_mission output validation
|
|
mission = dispatcher._build_pr_mission(work_item)
|
|
|
|
assert "CONNECTED ISSUE CONTEXT" in mission
|
|
assert "Connected Issue #42" in mission
|
|
assert "bug description" in mission
|
|
assert "First comment" in mission
|
|
|
|
mock_client.get_issue.assert_called_once_with("meeks", "repo1", 42)
|
|
mock_client.get_issue_comments.assert_called_once_with("meeks", "repo1", 42)
|
|
|
|
|
|
async def test_find_pr_for_issue_by_branch() -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
|
|
# 1. Matches fix/issue-42-some-desc
|
|
pr1 = PullRequestModel(number=102, head={"ref": "fix/issue-42-some-desc"})
|
|
mock_client.list_repo_pull_requests.return_value = [pr1]
|
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
|
|
|
# 2. Matches fix/42
|
|
pr2 = PullRequestModel(number=102, head={"ref": "fix/42"})
|
|
mock_client.list_repo_pull_requests.return_value = [pr2]
|
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
|
|
|
# 3. Matches fix-42_desc
|
|
pr3 = PullRequestModel(number=102, head={"ref": "fix-42_desc"})
|
|
mock_client.list_repo_pull_requests.return_value = [pr3]
|
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
|
|
|
# 4. Does NOT match fix/142
|
|
pr4 = PullRequestModel(number=102, head={"ref": "fix/142"})
|
|
mock_client.list_repo_pull_requests.return_value = [pr4]
|
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
|
|
|
# 5. Does NOT match fix/421
|
|
pr5 = PullRequestModel(number=102, head={"ref": "fix/421"})
|
|
mock_client.list_repo_pull_requests.return_value = [pr5]
|
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
|
|
|
|
|
|
|
async def test_find_pr_for_issue_by_raw_mention() -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
# PR body mentions #42
|
|
pr = PullRequestModel(
|
|
number=103,
|
|
title="some fix",
|
|
body="This is for #42 to fix the bug",
|
|
head={"ref": "some-branch"}
|
|
)
|
|
mock_client.list_repo_pull_requests.return_value = [pr]
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
res = dispatcher._find_pr_for_issue("meeks/repo1", 42)
|
|
assert res is not None
|
|
assert res.number == 103
|
|
|
|
|
|
async def test_dispatch_skips_already_reviewed_pr() -> None:
|
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
|
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
|
|
pr = PullRequestModel(
|
|
number=104,
|
|
title="already reviewed PR",
|
|
body="closes #42"
|
|
)
|
|
mock_client.get_pull_request.return_value = pr
|
|
mock_client.get_pull_request_diff.return_value = "diff"
|
|
mock_client.get_pull_request_comments.return_value = [
|
|
CommentModel(id=1, body="Reviewed by AI Agent: Looks good.")
|
|
]
|
|
|
|
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
|
|
|
work_item = WorkItem(
|
|
repo_full_name="meeks/repo1",
|
|
task_type="pr",
|
|
task_number=104,
|
|
task_info=PullRequestModel(number=104, title="already reviewed PR", body="closes #42"),
|
|
priority=0
|
|
)
|
|
|
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
|
assert len(results) == 1
|
|
assert "SKIP: PR #104 has already been addressed by AI" in results[0]
|
|
|
|
|
|
# ── _is_awaiting_reply tests ─────────────────────────────────────────────────
|
|
|
|
def _make_comment(login: str, body: str) -> CommentModel:
|
|
from gitea.models import UserModel
|
|
user = UserModel(login=login)
|
|
return CommentModel(id=1, body=body, user=user)
|
|
|
|
|
|
def test_is_awaiting_reply_no_comments() -> None:
|
|
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
|
|
assert dispatcher._is_awaiting_reply([]) is False
|
|
|
|
|
|
def test_is_awaiting_reply_no_question() -> None:
|
|
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
|
|
comments = [_make_comment("meeks-ai", "I will fix this now.")]
|
|
assert dispatcher._is_awaiting_reply(comments) is False
|
|
|
|
|
|
def test_is_awaiting_reply_agent_question_no_human_reply() -> None:
|
|
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
|
|
body = "Should I use approach A or B?\n<!-- agent:awaiting-reply -->"
|
|
comments = [_make_comment("meeks-ai", body)]
|
|
assert dispatcher._is_awaiting_reply(comments) is True
|
|
|
|
|
|
def test_is_awaiting_reply_agent_question_human_replied() -> None:
|
|
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
|
|
body = "Should I use approach A or B?\n<!-- agent:awaiting-reply -->"
|
|
comments = [
|
|
_make_comment("meeks-ai", body),
|
|
_make_comment("michael", "Use approach A please."),
|
|
]
|
|
assert dispatcher._is_awaiting_reply(comments) is False
|
|
|
|
|
|
def test_is_awaiting_reply_no_marker_not_detected() -> None:
|
|
"""Agent asked a question but forgot the marker — should NOT be skipped."""
|
|
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
|
|
comments = [_make_comment("meeks-ai", "Should I use approach A or B?")]
|
|
assert dispatcher._is_awaiting_reply(comments) is False
|