Files
coding-agent-gitea/tests/test_orchestrator.py
T
meeks ae4e2d46ac 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
2026-07-19 15:35:17 +02:00

149 lines
5.7 KiB
Python

import json
from pathlib import Path
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from core.orchestrator import AgentOrchestrator
from gitea.client import GiteaClient
from gitea.models import IssueModel, PullRequestModel, RepositoryModel
pytestmark = pytest.mark.anyio
@pytest.fixture
def temp_state_file(tmp_path: Path) -> Path:
"""Fixture to mock state file path."""
state_file = tmp_path / "agent_state.json"
return state_file
@patch("core.orchestrator.AgentOrchestrator._get_state_file_path")
@patch("core.orchestrator.AgentDispatcher")
@patch("core.orchestrator.WorkspaceManager")
@patch("core.orchestrator.NotificationReaderAgent")
async def test_poll_and_dispatch_no_notifications(
mock_notification_reader_class: MagicMock,
mock_workspace_class: MagicMock,
mock_dispatcher_class: MagicMock,
mock_get_path: MagicMock,
temp_state_file: Path
) -> None:
mock_get_path.return_value = temp_state_file
mock_client = MagicMock(spec=GiteaClient)
mock_tools = MagicMock()
# Return no notifications
mock_client.list_unread_notifications.return_value = []
orchestrator = AgentOrchestrator(mock_client, mock_tools)
await orchestrator.poll_and_dispatch()
mock_client.list_unread_notifications.assert_called_once_with(since=None)
assert not temp_state_file.exists()
@patch("core.orchestrator.AgentOrchestrator._get_state_file_path")
@patch("core.orchestrator.AgentDispatcher")
@patch("core.orchestrator.WorkspaceManager")
@patch("core.orchestrator.NotificationReaderAgent")
async def test_poll_and_dispatch_with_notifications(
mock_notification_reader_class: MagicMock,
mock_workspace_class: MagicMock,
mock_dispatcher_class: MagicMock,
mock_get_path: MagicMock,
temp_state_file: Path
) -> None:
mock_get_path.return_value = temp_state_file
mock_reader = MagicMock()
async def mock_decide_notification(mission: str, inspection_tools: list, notification_tools) -> str:
if "issue" in mission or "42" in mission:
notification_tools.process_issue("meeks", "repo1", 42, "Needs processing")
elif "pull" in mission or "10" in mission:
notification_tools.process_pr("meeks", "repo1", 10, "Needs processing")
else:
notification_tools.skip_notification("Unrelated")
return "Decided"
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()
# Set up mock Gitea notifications
notifications = [
{
"id": 101,
"updated_at": "2026-06-30T10:00:00Z",
"subject": {
"type": "issue",
"url": "https://gitea.meeks.freeddns.org/api/v1/repos/meeks/repo1/issues/42"
},
"repository": {
"name": "repo1",
"full_name": "meeks/repo1",
"owner": {"login": "meeks"}
}
},
{
"id": 102,
"updated_at": "2026-06-30T11:00:00Z",
"subject": {
"type": "pull",
"url": "https://gitea.meeks.freeddns.org/api/v1/repos/meeks/repo1/pulls/10"
},
"repository": {
"name": "repo1",
"full_name": "meeks/repo1",
"owner": {"login": "meeks"}
}
}
]
mock_client.list_unread_notifications.return_value = notifications
# Mock issue and PR get methods
issue_model = IssueModel(number=42, title="Bug issue", repository=RepositoryModel(name="repo1", full_name="meeks/repo1"))
pr_model = PullRequestModel(number=10, title="Fix PR", repository=RepositoryModel(name="repo1", full_name="meeks/repo1"))
mock_client.get_issue.return_value = issue_model
mock_client.get_pull_request.return_value = pr_model
# Mock dispatcher and workspace path
mock_dispatcher_instance = MagicMock()
mock_dispatcher_instance.dispatch = AsyncMock(return_value=["Issue comment posted", "PR verified"])
mock_dispatcher_class.return_value = mock_dispatcher_instance
mock_workspace_instance = MagicMock()
mock_workspace_instance.get_repo_path.return_value.exists.return_value = True
mock_workspace_class.return_value = mock_workspace_instance
# Create orchestrator and poll
orchestrator = AgentOrchestrator(mock_client, mock_tools)
await orchestrator.poll_and_dispatch()
# Assert notifications were checked with None (first execution)
mock_client.list_unread_notifications.assert_called_once_with(since=None)
# Assert issue and PR details were fetched
mock_client.get_issue.assert_called_once_with("meeks", "repo1", 42)
mock_client.get_pull_request.assert_called_once_with("meeks", "repo1", 10)
# Assert work was processed by dispatcher
mock_dispatcher_instance.dispatch.assert_called_once()
work_items = mock_dispatcher_instance.dispatch.call_args[0][1]
assert len(work_items) == 2
assert work_items[0].task_number == 42
assert work_items[0].notification_id == 101
assert work_items[1].task_number == 10
assert work_items[1].notification_id == 102
# Assert notifications were marked as read
mock_client.mark_notification_as_read.assert_any_call(101)
mock_client.mark_notification_as_read.assert_any_call(102)
assert mock_client.mark_notification_as_read.call_count == 2
# Assert checkpoint date was persisted
assert temp_state_file.exists()
with open(temp_state_file, "r") as f:
state = json.load(f)
# Checkpoint should match latest updated_at
assert state["last_checked"] == "2026-06-30T11:00:00Z"