feat: implement gitea_comment and discord_reply tools
This commit is contained in:
parent
b2ad726fc4
commit
e8983d8534
@ -1,5 +1,32 @@
|
|||||||
"""Discord message tool. Phase 2 implementation."""
|
"""Discord 채널/스레드 메시지 전송 도구."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent.utils.discord_client import get_discord_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def discord_reply(message: str) -> dict:
|
def discord_reply(message: str) -> dict[str, Any]:
|
||||||
raise NotImplementedError("Phase 2")
|
if not message.strip():
|
||||||
|
return {"success": False, "error": "빈 메시지는 전송할 수 없습니다."}
|
||||||
|
|
||||||
|
channel_id = os.environ.get("DISCORD_CHANNEL_ID", "")
|
||||||
|
if not channel_id:
|
||||||
|
return {"success": False, "error": "DISCORD_CHANNEL_ID가 설정되지 않았습니다."}
|
||||||
|
|
||||||
|
client = get_discord_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = asyncio.run(
|
||||||
|
client.send_message(channel_id=channel_id, content=message)
|
||||||
|
)
|
||||||
|
logger.info("Sent Discord message to channel %s", channel_id)
|
||||||
|
return {"success": True, "message_id": result.get("id")}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to send Discord message")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|||||||
@ -1,5 +1,40 @@
|
|||||||
"""Gitea issue/PR comment tool. Phase 2 implementation."""
|
"""Gitea 이슈/PR 코멘트 작성 도구."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent.utils.gitea_client import get_gitea_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def gitea_comment(message: str, issue_number: int) -> dict:
|
def _get_repo_info() -> tuple[str, str]:
|
||||||
raise NotImplementedError("Phase 2")
|
owner = os.environ.get("DEFAULT_REPO_OWNER", "quant")
|
||||||
|
repo = os.environ.get("DEFAULT_REPO_NAME", "galaxis-po")
|
||||||
|
return owner, repo
|
||||||
|
|
||||||
|
|
||||||
|
def gitea_comment(message: str, issue_number: int) -> dict[str, Any]:
|
||||||
|
if not issue_number or issue_number <= 0:
|
||||||
|
return {"success": False, "error": "유효한 issue_number가 필요합니다."}
|
||||||
|
|
||||||
|
if not message.strip():
|
||||||
|
return {"success": False, "error": "빈 메시지는 작성할 수 없습니다."}
|
||||||
|
|
||||||
|
owner, repo = _get_repo_info()
|
||||||
|
client = get_gitea_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = asyncio.run(
|
||||||
|
client.create_issue_comment(
|
||||||
|
owner=owner, repo=repo, issue_number=issue_number, body=message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("Posted comment on %s/%s#%d", owner, repo, issue_number)
|
||||||
|
return {"success": True, "comment_id": result.get("id")}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to post comment on %s/%s#%d", owner, repo, issue_number)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|||||||
@ -1,9 +1,45 @@
|
|||||||
"""Discord bot integration. Phase 2 implementation."""
|
"""Discord REST API 클라이언트."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DISCORD_API_BASE = "https://discord.com/api/v10"
|
||||||
|
|
||||||
|
|
||||||
class DiscordClient:
|
class DiscordClient:
|
||||||
async def send_message(self, channel_id: str, content: str) -> dict:
|
def __init__(self, token: str):
|
||||||
raise NotImplementedError("Phase 2")
|
self.token = token
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=DISCORD_API_BASE,
|
||||||
|
headers={"Authorization": f"Bot {self.token}"},
|
||||||
|
timeout=15.0,
|
||||||
|
)
|
||||||
|
|
||||||
async def send_thread_reply(self, channel_id, thread_id, content) -> dict:
|
async def send_message(self, channel_id: str, content: str) -> dict:
|
||||||
raise NotImplementedError("Phase 2")
|
resp = await self._client.post(
|
||||||
|
f"/channels/{channel_id}/messages",
|
||||||
|
json={"content": content},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
async def send_thread_reply(self, channel_id: str, thread_id: str, content: str) -> dict:
|
||||||
|
return await self.send_message(channel_id=thread_id, content=content)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
_client: DiscordClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_discord_client() -> DiscordClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = DiscordClient(token=os.environ.get("DISCORD_TOKEN", ""))
|
||||||
|
return _client
|
||||||
|
|||||||
48
tests/test_discord_reply.py
Normal file
48
tests/test_discord_reply.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def test_discord_reply_success():
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.send_message = AsyncMock(
|
||||||
|
return_value={"id": "123456", "content": "test message"}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"agent.tools.discord_reply.get_discord_client", return_value=mock_client
|
||||||
|
), patch.dict("os.environ", {"DISCORD_CHANNEL_ID": "999"}):
|
||||||
|
from agent.tools.discord_reply import discord_reply
|
||||||
|
result = discord_reply(message="test message")
|
||||||
|
assert result["success"] is True
|
||||||
|
mock_client.send_message.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_discord_reply_empty_message():
|
||||||
|
from agent.tools.discord_reply import discord_reply
|
||||||
|
result = discord_reply(message="")
|
||||||
|
assert result["success"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_discord_reply_no_channel_configured():
|
||||||
|
with patch.dict("os.environ", {"DISCORD_CHANNEL_ID": ""}, clear=False):
|
||||||
|
from agent.tools.discord_reply import discord_reply
|
||||||
|
result = discord_reply(message="test")
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "DISCORD" in result.get("error", "")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_discord_client_send_message():
|
||||||
|
import httpx
|
||||||
|
mock_resp = MagicMock(spec=httpx.Response)
|
||||||
|
mock_resp.status_code = 200
|
||||||
|
mock_resp.json.return_value = {"id": "msg123", "content": "hello"}
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
from agent.utils.discord_client import DiscordClient
|
||||||
|
client = DiscordClient(token="test-token")
|
||||||
|
client._client.post = AsyncMock(return_value=mock_resp)
|
||||||
|
|
||||||
|
result = await client.send_message(channel_id="999", content="hello")
|
||||||
|
assert result["id"] == "msg123"
|
||||||
|
client._client.post.assert_called_once()
|
||||||
48
tests/test_gitea_comment.py
Normal file
48
tests/test_gitea_comment.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_comment_success():
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.create_issue_comment = AsyncMock(
|
||||||
|
return_value={"id": 42, "body": "test comment"}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"agent.tools.gitea_comment.get_gitea_client", return_value=mock_client
|
||||||
|
), patch(
|
||||||
|
"agent.tools.gitea_comment._get_repo_info",
|
||||||
|
return_value=("quant", "galaxis-po"),
|
||||||
|
):
|
||||||
|
from agent.tools.gitea_comment import gitea_comment
|
||||||
|
result = gitea_comment(message="test comment", issue_number=1)
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["comment_id"] == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_comment_missing_issue_number():
|
||||||
|
from agent.tools.gitea_comment import gitea_comment
|
||||||
|
result = gitea_comment(message="test", issue_number=0)
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "issue_number" in result["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_comment_api_error():
|
||||||
|
import httpx
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.create_issue_comment = AsyncMock(
|
||||||
|
side_effect=httpx.HTTPStatusError(
|
||||||
|
"404", request=MagicMock(), response=MagicMock(status_code=404)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"agent.tools.gitea_comment.get_gitea_client", return_value=mock_client
|
||||||
|
), patch(
|
||||||
|
"agent.tools.gitea_comment._get_repo_info",
|
||||||
|
return_value=("quant", "galaxis-po"),
|
||||||
|
):
|
||||||
|
from agent.tools.gitea_comment import gitea_comment
|
||||||
|
result = gitea_comment(message="test", issue_number=999)
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "error" in result
|
||||||
Loading…
x
Reference in New Issue
Block a user