feat: redesign issue processing to planning and question board with WIP PR workflow (#3)
Co-authored-by: Michael <michael@example.com> Reviewed-on: #3
This commit is contained in:
+268
-5
@@ -1,10 +1,10 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from unittest.mock import MagicMock, AsyncMock, patch, ANY
|
||||
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
|
||||
from gitea.models import PullRequestModel, IssueModel, CommentModel, UserModel
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
@@ -54,7 +54,14 @@ async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock)
|
||||
|
||||
# Mock CodingAgent run_with_tools
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="Issue resolved.")
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "PROPOSE_PLAN",
|
||||
"reasoning": "Plan needs to be proposed first.",
|
||||
"comment_body": "### Proposed Plan\\n- change X",
|
||||
"approved_plan": ""
|
||||
}
|
||||
```""")
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
@@ -70,7 +77,7 @@ async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock)
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0] == "Issue resolved."
|
||||
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
@@ -181,10 +188,13 @@ async def test_dispatch_skips_already_reviewed_pr() -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
from gitea.models import UserModel
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
pr = PullRequestModel(
|
||||
number=104,
|
||||
title="already reviewed PR",
|
||||
body="closes #42"
|
||||
body="closes #42",
|
||||
user=UserModel(login="meeks-ai")
|
||||
)
|
||||
mock_client.get_pull_request.return_value = pr
|
||||
mock_client.get_pull_request_diff.return_value = "diff"
|
||||
@@ -248,3 +258,256 @@ def test_is_awaiting_reply_no_marker_not_detected() -> None:
|
||||
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
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_proposes_plan(mock_agent_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.list_repo_pull_requests.return_value = []
|
||||
mock_client.get_issue_comments.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "PROPOSE_PLAN",
|
||||
"reasoning": "We need to add a new endpoint.",
|
||||
"comment_body": "### Proposed Plan\\n- Add endpoint\\n<!-- agent:plan-proposal -->\\n<!-- agent:awaiting-reply -->"
|
||||
}
|
||||
```""")
|
||||
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="add X", body=""),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Plan\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_answers_question(mock_agent_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.list_repo_pull_requests.return_value = []
|
||||
mock_client.get_issue_comments.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "ANSWER_QUESTION",
|
||||
"reasoning": "This is a question about how X works.",
|
||||
"comment_body": "X works by doing Y.\\n<!-- agent:question-response -->\\n<!-- agent:awaiting-reply -->"
|
||||
}
|
||||
```""")
|
||||
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="how does X work", body=""),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "POSTED_COMMENT: ANSWER_QUESTION" in results[0]
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_closes_issue_on_satisfaction(mock_agent_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.list_repo_pull_requests.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
# Human comments indicating satisfaction after our answer
|
||||
mock_client.get_issue_comments.return_value = [
|
||||
_make_comment("meeks-ai", "Here is the answer.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->"),
|
||||
_make_comment("michael", "Yes, thanks! That makes sense.")
|
||||
]
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "CLOSE_ISSUE",
|
||||
"reasoning": "User is satisfied.",
|
||||
"comment_body": "Closing the issue now. Let me know if you need anything else!"
|
||||
}
|
||||
```""")
|
||||
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="how does X work", body=""),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "CLOSED_ISSUE: Issue #42 closed." in results[0]
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "Closing the issue now. Let me know if you need anything else!")
|
||||
mock_client.close_issue.assert_called_once_with("meeks", "repo1", 42)
|
||||
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_agent_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.list_repo_pull_requests.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
mock_client.get_issue_comments.return_value = [
|
||||
_make_comment("meeks-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
||||
_make_comment("michael", "looks good, go ahead")
|
||||
]
|
||||
|
||||
# Return PR object on creation
|
||||
mock_pr = PullRequestModel(number=105, title="WIP: add X", html_url="http://gitea/pr/105", head={"ref": "fix/issue-42-add-x"})
|
||||
mock_client.create_pull_request.return_value = mock_pr
|
||||
|
||||
# Mock planning agent deciding EXECUTE_PLAN
|
||||
mock_agent_instance1 = MagicMock()
|
||||
mock_agent_instance1.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "EXECUTE_PLAN",
|
||||
"reasoning": "Plan was approved.",
|
||||
"approved_plan": "Step 1. Code X"
|
||||
}
|
||||
```""")
|
||||
|
||||
# Mock coding agent executing plan
|
||||
mock_agent_instance2 = MagicMock()
|
||||
mock_agent_instance2.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
|
||||
|
||||
mock_agent_class.side_effect = [mock_agent_instance1, mock_agent_instance2]
|
||||
|
||||
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="add X", body=""),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert results[0] == "PR Completed Successfully."
|
||||
|
||||
# Verify subprocess git commands
|
||||
mock_run.assert_any_call(["git", "checkout", "master"], cwd=ANY, check=True)
|
||||
mock_run.assert_any_call(["git", "checkout", "-b", "fix/issue-42-add-x"], cwd=ANY, check=True)
|
||||
|
||||
# Verify WIP PR creation and starting comment
|
||||
mock_client.create_pull_request.assert_called_once_with(
|
||||
"meeks", "repo1", head="fix/issue-42-add-x", base="master", title="WIP: add X", description="Work in progress for issue #42."
|
||||
)
|
||||
mock_client.add_comment.assert_any_call("meeks", "repo1", 42, "Started work on PR #105 (http://gitea/pr/105).")
|
||||
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_resumes_wip_pr(mock_agent_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
# Existing WIP PR addressing issue #42
|
||||
wip_pr = PullRequestModel(
|
||||
number=105,
|
||||
title="WIP: add X",
|
||||
state="open",
|
||||
head={"ref": "fix/issue-42-add-x"}
|
||||
)
|
||||
mock_client.list_repo_pull_requests.return_value = [wip_pr]
|
||||
mock_client.get_pr_reviews.return_value = []
|
||||
|
||||
mock_client.get_issue_comments.return_value = [
|
||||
_make_comment("meeks-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
||||
_make_comment("michael", "looks good, go ahead")
|
||||
]
|
||||
mock_client.get_pull_request_comments.return_value = []
|
||||
|
||||
# Mock planning agent deciding EXECUTE_PLAN
|
||||
mock_agent_instance1 = MagicMock()
|
||||
mock_agent_instance1.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "EXECUTE_PLAN",
|
||||
"reasoning": "WIP PR exists, resume coding.",
|
||||
"approved_plan": "Step 1. Resume coding"
|
||||
}
|
||||
```""")
|
||||
|
||||
# Mock coding agent executing plan
|
||||
mock_agent_instance2 = MagicMock()
|
||||
mock_agent_instance2.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
|
||||
|
||||
mock_agent_class.side_effect = [mock_agent_instance1, mock_agent_instance2]
|
||||
|
||||
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="add X", body=""),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert results[0] == "PR Updated Successfully."
|
||||
|
||||
# Ensure create_pull_request was NOT called since it already exists
|
||||
mock_client.create_pull_request.assert_not_called()
|
||||
|
||||
|
||||
async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
# PR authored by michael, requested reviewers is empty (agent not requested)
|
||||
pr_detail = PullRequestModel(
|
||||
number=201,
|
||||
title="some feature",
|
||||
state="open",
|
||||
user=UserModel(login="michael"),
|
||||
requested_reviewers=[]
|
||||
)
|
||||
mock_client.get_pull_request.return_value = pr_detail
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
repo_full_name="meeks/repo1",
|
||||
task_type="pr",
|
||||
task_number=201,
|
||||
task_info=PullRequestModel(number=201, title="some feature"),
|
||||
priority=0
|
||||
)
|
||||
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "SKIP: Agent is not a requested reviewer" in results[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user