refactor: replace Any types with specific types and update bad_code.md

- Replace Any with object or specific types across codebase
- Add ReviewRequest dataclass for PR review payloads
- Update bad_code.md: mark 5.1 (Any Type Overuse) as resolved
- Fix summary table with accurate counts and unresolved issues list
This commit is contained in:
meeks
2026-07-19 15:35:17 +02:00
parent aa9e8222a3
commit ae4e2d46ac
13 changed files with 326 additions and 169 deletions
+27 -12
View File
@@ -9,7 +9,6 @@ from gitea.tools.coding_tools import CodingTools
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 IssueModel, PullRequestModel
pytestmark = pytest.mark.anyio
@@ -96,12 +95,16 @@ def test_run_verification_failure(tmp_path: Path) -> None:
@patch("core.dispatcher.CodingAgent")
@patch("core.dispatcher.PlanningAgent")
async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMock, mock_coding_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
# Mock sub-clients
mock_client.prs = MagicMock()
mock_client.issues = MagicMock()
mock_client.notifications = MagicMock()
# Mock no existing PRs
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.issues.get_issue_comments.return_value = []
from gitea.models import UserModel
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
@@ -110,13 +113,14 @@ async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMoc
number=42,
title="fix bug",
body="bug details",
user=UserModel(login="unknown-ai")
user=UserModel(login="unknown-ai"),
requested_reviewers=[UserModel(login="unknown-ai")],
)
mock_client.get_pull_request.return_value = mock_pr
mock_client.get_pull_request_diff.return_value = "diff"
mock_client.get_pull_request_comments.return_value = []
mock_client.get_pull_request_files.return_value = []
mock_client.get_pr_reviews.return_value = []
mock_client.prs.get_pull_request.return_value = mock_pr
mock_client.prs.get_pull_request_diff.return_value = "diff"
mock_client.prs.get_pull_request_comments.return_value = []
mock_client.prs.get_pull_request_files.return_value = []
mock_client.prs.get_pr_reviews.return_value = []
# Mock agent instances
mock_planning_agent = MagicMock()
@@ -127,7 +131,18 @@ async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMoc
mock_coding_agent.run_with_tools = AsyncMock(return_value="PR #1 Created")
mock_coding_class.return_value = mock_coding_agent
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
from gitea.tools.issue_tools import IssueTools
from gitea.tools.pr_tools import PRTools
from gitea.tools.file_tools import FileTools
from gitea.tools.git_tools import GitTools
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=IssueTools(mock_client),
pr_tools=PRTools(mock_client),
file_tools=FileTools(mock_client),
git_tools=GitTools(mock_client),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
+220 -108
View File
@@ -2,16 +2,19 @@ import pytest
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, UserModel
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_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
# Mock sub-clients
mock_client.prs = MagicMock()
mock_client.issues = MagicMock()
mock_client.notifications = MagicMock()
# Mock list_repo_pull_requests to return a PR that closes issue #42
pr = PullRequestModel(
@@ -19,9 +22,15 @@ async def test_dispatch_skips_issue_with_existing_pr() -> None:
title="fix: resolve bug",
body="closes #42"
)
mock_client.list_repo_pull_requests.return_value = [pr]
mock_client.prs.list_repo_pull_requests.return_value = [pr]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
@@ -35,13 +44,14 @@ async def test_dispatch_skips_issue_with_existing_pr() -> None:
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")
mock_client.prs.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
# Mock list_repo_pull_requests to return PRs that don't address issue #42
pr = PullRequestModel(
@@ -49,8 +59,8 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
title="feat: add something",
body="closes #99"
)
mock_client.list_repo_pull_requests.return_value = [pr]
mock_client.get_issue_comments.return_value = []
mock_client.prs.list_repo_pull_requests.return_value = [pr]
mock_client.issues.get_issue_comments.return_value = []
# Mock CoordinatorAgent invoking propose_plan tool
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
@@ -61,7 +71,13 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
mock_coord_class.return_value = mock_coord_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
@@ -79,8 +95,9 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
@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_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
# Mock get_pull_request, get_pull_request_diff, etc.
pr = PullRequestModel(
@@ -90,25 +107,31 @@ async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMoc
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_client.prs.get_pull_request.return_value = pr
mock_client.prs.get_pull_request_diff.return_value = "diff context"
mock_client.prs.get_pull_request_files.return_value = []
mock_client.prs.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
mock_client.issues.get_issue.return_value = issue
comment = CommentModel(id=1, body="First comment")
mock_client.get_issue_comments.return_value = [comment]
mock_client.issues.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
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
@@ -126,46 +149,54 @@ async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMoc
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)
mock_client.issues.get_issue.assert_called_once_with("meeks", "repo1", 42)
mock_client.issues.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)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
# 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]
mock_client.prs.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]
mock_client.prs.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]
mock_client.prs.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]
mock_client.prs.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]
mock_client.prs.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)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
# PR body mentions #42
pr = PullRequestModel(
@@ -174,33 +205,51 @@ async def test_find_pr_for_issue_by_raw_mention() -> None:
body="This is for #42 to fix the bug",
head={"ref": "some-branch"}
)
mock_client.list_repo_pull_requests.return_value = [pr]
mock_client.prs.list_repo_pull_requests.return_value = [pr]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
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)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_client.repos = MagicMock()
mock_client.prs = MagicMock()
mock_client.issues = MagicMock()
mock_client.notifications = MagicMock()
mock_tools: MagicMock = MagicMock()
from gitea.models import UserModel
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
pr = PullRequestModel(
number=104,
title="already reviewed PR",
body="closes #42",
user=UserModel(login="unknown-ai")
user=UserModel(login="unknown-ai"),
requested_reviewers=[],
)
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 = [
mock_client.prs.get_pull_request.return_value = pr
mock_client.prs.get_pull_request_diff.return_value = "diff"
mock_client.prs.get_pull_request_comments.return_value = [
CommentModel(id=1, body="Reviewed by AI Agent: Looks good.")
]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
@@ -225,7 +274,7 @@ def _make_comment(login: str, body: str) -> CommentModel:
def _make_dispatcher_for_reply_tests() -> AgentDispatcher:
mock_client = MagicMock()
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
return AgentDispatcher(client=mock_client, tools=MagicMock())
@@ -266,12 +315,13 @@ def test_is_awaiting_reply_no_marker_not_detected() -> None:
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.issues.get_issue_comments.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
coord_tools.propose_plan(plan="- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->", issue_number=42)
@@ -280,7 +330,13 @@ async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
mock_coord_class.return_value = mock_coord_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -292,17 +348,18 @@ async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
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 Implementation Plan\n\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Implementation Plan\n\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.issues.get_issue_comments.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
coord_tools.answer_question(answer="X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->", issue_number=42)
@@ -311,7 +368,13 @@ async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
mock_coord_class.return_value = mock_coord_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -323,19 +386,20 @@ async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
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 -->\n\nIs this answer satisfactory?\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->\n\nIs this answer satisfactory?\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
# Human comments indicating satisfaction after our answer
mock_client.get_issue_comments.return_value = [
mock_client.issues.get_issue_comments.return_value = [
_make_comment("unknown-ai", "Here is the answer.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->"),
_make_comment("michael", "Yes, thanks! That makes sense.")
]
@@ -347,7 +411,13 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
mock_coord_class.return_value = mock_coord_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -359,27 +429,28 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock
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)
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "Closing the issue now. Let me know if you need anything else!")
mock_client.issues.close_issue.assert_called_once_with("meeks", "repo1", 42)
@patch("subprocess.run")
@patch("core.dispatcher.CodingAgent")
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.get_issue_comments.return_value = [
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.issues.get_issue_comments.return_value = [
_make_comment("unknown-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_client.prs.create_pull_request.return_value = mock_pr
# Mock planning agent deciding EXECUTE_PLAN
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
@@ -390,7 +461,13 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_cla
# Mock coding agent executing plan
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -408,20 +485,21 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_cla
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(
mock_client.prs.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).")
mock_client.issues.add_comment.assert_any_call("meeks", "repo1", 42, "Started work on PR #105 (http://gitea/pr/105).")
@patch("subprocess.run")
@patch("core.dispatcher.CodingAgent")
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
# Existing WIP PR addressing issue #42
wip_pr = PullRequestModel(
@@ -430,14 +508,14 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
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.prs.list_repo_pull_requests.return_value = [wip_pr]
mock_client.prs.get_pr_reviews.return_value = []
mock_client.get_issue_comments.return_value = [
mock_client.issues.get_issue_comments.return_value = [
_make_comment("unknown-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_client.prs.get_pull_request_comments.return_value = []
# Mock planning agent deciding EXECUTE_PLAN
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
@@ -448,7 +526,13 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
# Mock coding agent executing plan
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -462,14 +546,15 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
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()
mock_client.prs.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: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
# PR authored by michael, requested reviewers is empty (agent not requested)
pr_detail = PullRequestModel(
@@ -479,9 +564,15 @@ async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None:
user=UserModel(login="michael"),
requested_reviewers=[]
)
mock_client.get_pull_request.return_value = pr_detail
mock_client.prs.get_pull_request.return_value = pr_detail
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="pr",
@@ -513,12 +604,13 @@ def test_coordinator_tools_registration() -> None:
@patch("core.dispatcher.CoordinatorAgent")
async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.issues.get_issue_comments.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
# Mock agent invoking propose_plan tool
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
@@ -529,7 +621,13 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
mock_coord_class.return_value = mock_coord_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -541,7 +639,7 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
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(
mock_client.issues.add_comment.assert_called_once_with(
"meeks",
"repo1",
42,
@@ -550,15 +648,22 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
# Mock return values for methods called prior to the isinstance check
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
mock_client.prs.list_repo_pull_requests.return_value = []
mock_client.issues.get_issue_comments.return_value = []
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
# 1. Test dispatch raises TypeError if task_info is not IssueModel for an issue task
work_item_invalid_issue = WorkItem(
@@ -588,13 +693,20 @@ async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None:
async def test_dispatch_fails_if_no_authenticated_user() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
mock_client: MagicMock = MagicMock()
mock_client.repos = MagicMock()
mock_tools: MagicMock = MagicMock()
# Simulate get_authenticated_user returning None
mock_client.get_authenticated_user.return_value = None
mock_client.repos.get_authenticated_user.return_value = None
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
dispatcher = AgentDispatcher(
client=mock_client,
issue_tools=MagicMock(),
pr_tools=MagicMock(),
file_tools=MagicMock(),
git_tools=MagicMock(),
)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
@@ -607,7 +719,7 @@ async def test_dispatch_fails_if_no_authenticated_user() -> None:
await dispatcher.dispatch("meeks/repo1", [work_item])
# Simulate get_authenticated_user raising an Exception
mock_client.get_authenticated_user.side_effect = Exception("API error")
mock_client.repos.get_authenticated_user.side_effect = Exception("API error")
with pytest.raises(RuntimeError, match="No authenticated user found."):
await dispatcher.dispatch("meeks/repo1", [work_item])
+2 -3
View File
@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, AsyncMock, patch
from core.orchestrator import AgentOrchestrator
from gitea.client import GiteaClient
from gitea.tools.gitea_tools import GiteaTools
from gitea.models import IssueModel, PullRequestModel, RepositoryModel
pytestmark = pytest.mark.anyio
@@ -31,7 +30,7 @@ async def test_poll_and_dispatch_no_notifications(
) -> None:
mock_get_path.return_value = temp_state_file
mock_client = MagicMock(spec=GiteaClient)
mock_tools = MagicMock(spec=GiteaTools)
mock_tools = MagicMock()
# Return no notifications
mock_client.list_unread_notifications.return_value = []
@@ -68,7 +67,7 @@ async def test_poll_and_dispatch_with_notifications(
mock_reader.decide_notification = AsyncMock(side_effect=mock_decide_notification)
mock_notification_reader_class.return_value = mock_reader
mock_client = MagicMock(spec=GiteaClient)
mock_tools = MagicMock(spec=GiteaTools)
mock_tools = MagicMock()
# Set up mock Gitea notifications
notifications = [