feat: implement NotificationReaderAgent to pre-screen Gitea notifications (#2)

This commit was merged in pull request #2.
This commit is contained in:
2026-06-30 22:01:07 +02:00
parent 287d20bc56
commit 9b72e20d5a
7 changed files with 288 additions and 2 deletions
+83
View File
@@ -0,0 +1,83 @@
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from core.notification_tools import NotificationTools
from core.notification_agent import NotificationReaderAgent, NotificationNoToolCalledError
pytestmark = pytest.mark.anyio
def test_notification_tools() -> None:
tools = NotificationTools()
assert tools.tool_called is False
assert tools.action == "NO_ACTION"
res = tools.process_issue("meeks", "repo", 42, "fix bug")
assert tools.tool_called is True
assert tools.action == "PROCESS_ISSUE"
assert tools.arguments == {
"owner": "meeks",
"repo": "repo",
"issue_number": 42,
"reason": "fix bug",
}
assert "marked for processing" in res
tools = NotificationTools()
res = tools.process_pr("meeks", "repo", 10, "review change")
assert tools.tool_called is True
assert tools.action == "PROCESS_PR"
assert tools.arguments == {
"owner": "meeks",
"repo": "repo",
"pr_number": 10,
"reason": "review change",
}
assert "marked for processing" in res
tools = NotificationTools()
res = tools.skip_notification("unrelated comments")
assert tools.tool_called is True
assert tools.action == "SKIP"
assert tools.arguments == {"reason": "unrelated comments"}
assert "marked to be skipped" in res
@patch("core.notification_agent.NotificationReaderAgent.initialize")
@patch("core.notification_agent.NotificationReaderAgent.run_with_tools")
async def test_notification_reader_agent_success(
mock_run_with_tools: MagicMock,
mock_initialize: MagicMock
) -> None:
mock_initialize.return_value = None
agent = NotificationReaderAgent("dummy-model")
# Mock tool call inside run_with_tools
async def mock_run(mission: str, tools: list) -> str:
# Simulate calling a tool
for t in tools:
if getattr(t, "__name__", "") == "process_issue":
t("meeks", "repo", 42, "reason")
return "response"
mock_run_with_tools.side_effect = mock_run
tools = NotificationTools()
res = await agent.decide_notification("mission", [], tools)
assert res == "response"
assert tools.tool_called is True
assert tools.action == "PROCESS_ISSUE"
@patch("core.notification_agent.NotificationReaderAgent.initialize")
@patch("core.notification_agent.NotificationReaderAgent.run_with_tools")
async def test_notification_reader_agent_no_tool_error(
mock_run_with_tools: MagicMock,
mock_initialize: MagicMock
) -> None:
mock_initialize.return_value = None
agent = NotificationReaderAgent("dummy-model")
mock_run_with_tools.return_value = "no tool called"
tools = NotificationTools()
with pytest.raises(NotificationNoToolCalledError):
await agent.decide_notification("mission", [], tools)
+16
View File
@@ -21,7 +21,9 @@ def temp_state_file(tmp_path: Path) -> Path:
@patch("core.orchestrator.AgentOrchestrator._get_state_file_path")
@patch("core.orchestrator.AgentDispatcher")
@patch("core.orchestrator.WorkspaceManager")
@patch("core.orchestrator.AgentFactory")
async def test_poll_and_dispatch_no_notifications(
mock_factory: MagicMock,
mock_workspace_class: MagicMock,
mock_dispatcher_class: MagicMock,
mock_get_path: MagicMock,
@@ -44,13 +46,27 @@ async def test_poll_and_dispatch_no_notifications(
@patch("core.orchestrator.AgentOrchestrator._get_state_file_path")
@patch("core.orchestrator.AgentDispatcher")
@patch("core.orchestrator.WorkspaceManager")
@patch("core.orchestrator.AgentFactory")
async def test_poll_and_dispatch_with_notifications(
mock_factory: 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_factory.create_notification_reader_agent.return_value = mock_reader
mock_client = MagicMock(spec=GiteaClient)
mock_tools = MagicMock(spec=GiteaTools)