import pytest from unittest.mock import MagicMock, AsyncMock, patch, ANY from core.dispatcher import AgentDispatcher from core.queue import WorkItem 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() 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( number=101, title="fix: resolve bug", body="closes #42" ) mock_client.prs.list_repo_pull_requests.return_value = [pr] 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", 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.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() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() # 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.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: coord_tools.propose_plan(plan="- change X", issue_number=42) return "Agent proposed plan." mock_coord_instance = MagicMock() mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide) mock_coord_class.return_value = mock_coord_instance 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", 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 "POSTED_COMMENT: PROPOSE_PLAN" in results[0] @patch("core.dispatcher.CodingAgent") async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMock) -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() # 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.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.issues.get_issue.return_value = issue comment = CommentModel(id=1, body="First 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.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", 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.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() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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.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.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.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.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.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() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() # 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.prs.list_repo_pull_requests.return_value = [pr] 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() 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.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"), requested_reviewers=[], ) 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, issue_tools=MagicMock(), pr_tools=MagicMock(), file_tools=MagicMock(), git_tools=MagicMock(), ) 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 _make_dispatcher_for_reply_tests() -> AgentDispatcher: mock_client = MagicMock() mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai") return AgentDispatcher( client=mock_client, issue_tools=MagicMock(), pr_tools=MagicMock(), file_tools=MagicMock(), git_tools=MagicMock(), ) def test_is_awaiting_reply_no_comments() -> None: dispatcher = _make_dispatcher_for_reply_tests() assert dispatcher._is_awaiting_reply([]) is False def test_is_awaiting_reply_no_question() -> None: dispatcher = _make_dispatcher_for_reply_tests() comments = [_make_comment("unknown-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 = _make_dispatcher_for_reply_tests() body = "Should I use approach A or B?\n" comments = [_make_comment("unknown-ai", body)] assert dispatcher._is_awaiting_reply(comments) is True def test_is_awaiting_reply_agent_question_human_replied() -> None: dispatcher = _make_dispatcher_for_reply_tests() body = "Should I use approach A or B?\n" comments = [ _make_comment("unknown-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 = _make_dispatcher_for_reply_tests() comments = [_make_comment("unknown-ai", "Should I use approach A or B?")] assert dispatcher._is_awaiting_reply(comments) is False @patch("core.dispatcher.CoordinatorAgent") async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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\n", issue_number=42) return "Agent proposed plan." mock_coord_instance = MagicMock() mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide) mock_coord_class.return_value = mock_coord_instance 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", 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.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Implementation Plan\n\n- Add endpoint\n\n\n\nIs this plan ok for implementation or do you have any comments/changes?\n\n") @patch("core.dispatcher.CoordinatorAgent") async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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\n", issue_number=42) return "Agent answered question." mock_coord_instance = MagicMock() mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide) mock_coord_class.return_value = mock_coord_instance 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", 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.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n\n\n\nIs this answer satisfactory?\n\n") @patch("core.dispatcher.CoordinatorAgent") async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock) -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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.issues.get_issue_comments.return_value = [ _make_comment("unknown-ai", "Here is the answer.\n\n"), _make_comment("michael", "Yes, thanks! That makes sense.") ] async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str: coord_tools.close_issue(comment="Closing the issue now. Let me know if you need anything else!", issue_number=42) return "Agent closed issue." mock_coord_instance = MagicMock() mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide) mock_coord_class.return_value = mock_coord_instance 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", 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.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() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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\n"), _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.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: coord_tools.start_implementation(approved_plan="Step 1. Code X", issue_number=42) return "Agent decided execute plan." mock_coord_class.return_value.decide_action = AsyncMock(side_effect=mock_decide) # Mock coding agent executing plan mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Completed Successfully.") 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", 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.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.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() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-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.prs.list_repo_pull_requests.return_value = [wip_pr] mock_client.prs.get_pr_reviews.return_value = [] mock_client.issues.get_issue_comments.return_value = [ _make_comment("unknown-ai", "### Proposed Plan\n\n"), _make_comment("michael", "looks good, go ahead") ] 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: coord_tools.start_implementation(approved_plan="Step 1. Resume coding", issue_number=42) return "Agent decided execute plan." mock_coord_class.return_value.decide_action = AsyncMock(side_effect=mock_decide) # Mock coding agent executing plan mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Updated Successfully.") 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", 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.prs.create_pull_request.assert_not_called() async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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( number=201, title="some feature", state="open", user=UserModel(login="michael"), requested_reviewers=[] ) mock_client.prs.get_pull_request.return_value = pr_detail 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", 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] def test_coordinator_tools_registration() -> None: from core.coordinator_tools import CoordinatorTools tools: CoordinatorTools = CoordinatorTools() assert not tools.tool_called assert tools.action == "NO_ACTION" tools.propose_plan(plan="my plan", issue_number=42) assert tools.tool_called assert tools.action == "PROPOSE_PLAN" assert tools.arguments == {"plan": "my plan", "issue_number": 42} tools.start_implementation(approved_plan="my approved plan", issue_number=42) assert tools.action == "EXECUTE_PLAN" assert tools.arguments == {"approved_plan": "my approved plan", "issue_number": 42} @patch("core.dispatcher.CoordinatorAgent") async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMock) -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() 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: coord_tools.propose_plan(plan="Step 1. Code X", issue_number=42) return "Agent finished turn after tool calling." mock_coord_instance = MagicMock() mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide) mock_coord_class.return_value = mock_coord_instance 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", 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.issues.add_comment.assert_called_once_with( "meeks", "repo1", 42, "### Proposed Implementation Plan\n\nStep 1. Code X\n\nIs this plan ok for implementation or do you have any comments/changes?\n\n" ) async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None: 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.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, 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( repo_full_name="meeks/repo1", task_type="issue", task_number=42, task_info=PullRequestModel(number=42), # Invalid model type priority=0 ) with pytest.raises(TypeError, match="Expected task_info to be an IssueModel"): await dispatcher.dispatch("meeks/repo1", [work_item_invalid_issue]) # 2. Test _build_pr_mission raises TypeError if task_info is not PullRequestModel work_item_invalid_pr = WorkItem( repo_full_name="meeks/repo1", task_type="pr", task_number=42, task_info=IssueModel(number=42), # Invalid model type priority=0 ) with pytest.raises(TypeError, match="Expected task_info to be a PullRequestModel"): dispatcher._build_pr_mission(work_item_invalid_pr) # 3. Test _build_issue_mission raises TypeError if task_info is not IssueModel with pytest.raises(TypeError, match="Expected task_info to be an IssueModel"): dispatcher._build_issue_mission(work_item_invalid_issue) async def test_dispatch_fails_if_no_authenticated_user() -> None: mock_client: MagicMock = MagicMock() mock_client.repos = MagicMock() mock_tools: MagicMock = MagicMock() # Simulate get_authenticated_user returning None mock_client.repos.get_authenticated_user.return_value = None 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", task_number=42, task_info=IssueModel(number=42, title="add X", body=""), priority=0 ) with pytest.raises(RuntimeError, match="No authenticated user found."): await dispatcher.dispatch("meeks/repo1", [work_item]) # Simulate get_authenticated_user raising an Exception 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])