feat(agent-office): drop daily realestate cron + bookmark callback routing
- scheduler.py: remove _run_realestate_schedule() and its 09:15 cron job
- service_proxy.py: add realestate_bookmark_toggle() helper (PATCH bookmark endpoint)
- webhook.py: add _handle_realestate_bookmark() dispatcher before DB-lookup path;
realestate_bookmark_{id} callbacks are handled inline without a DB entry
- tests/test_realestate_callback.py: 4 new unit tests covering happy path,
invalid id, proxy error, and regression that approve/reject still uses DB path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,6 @@ async def _run_stock_schedule():
|
|||||||
if agent:
|
if agent:
|
||||||
await agent.on_schedule()
|
await agent.on_schedule()
|
||||||
|
|
||||||
async def _run_realestate_schedule():
|
|
||||||
agent = AGENT_REGISTRY.get("realestate")
|
|
||||||
if agent:
|
|
||||||
await agent.on_schedule()
|
|
||||||
|
|
||||||
async def _run_blog_schedule():
|
async def _run_blog_schedule():
|
||||||
agent = AGENT_REGISTRY.get("blog")
|
agent = AGENT_REGISTRY.get("blog")
|
||||||
if agent:
|
if agent:
|
||||||
@@ -31,7 +26,6 @@ async def _run_lotto_schedule():
|
|||||||
|
|
||||||
def init_scheduler():
|
def init_scheduler():
|
||||||
scheduler.add_job(_run_stock_schedule, "cron", hour=7, minute=30, id="stock_news")
|
scheduler.add_job(_run_stock_schedule, "cron", hour=7, minute=30, id="stock_news")
|
||||||
scheduler.add_job(_run_realestate_schedule, "cron", hour=9, minute=15, id="realestate_report")
|
|
||||||
scheduler.add_job(_run_blog_schedule, "cron", hour=10, minute=0, id="blog_pipeline")
|
scheduler.add_job(_run_blog_schedule, "cron", hour=10, minute=0, id="blog_pipeline")
|
||||||
scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=7, minute=0, id="lotto_curate")
|
scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=7, minute=0, id="lotto_curate")
|
||||||
scheduler.add_job(_check_idle_breaks, "interval", seconds=60, id="idle_check")
|
scheduler.add_job(_check_idle_breaks, "interval", seconds=60, id="idle_check")
|
||||||
|
|||||||
@@ -133,6 +133,16 @@ async def realestate_mark_read(match_id: int) -> Dict[str, Any]:
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def realestate_bookmark_toggle(announcement_id: int) -> Dict[str, Any]:
|
||||||
|
"""realestate-lab의 PATCH /api/realestate/announcements/{id}/bookmark 호출."""
|
||||||
|
async with httpx.AsyncClient(timeout=10) as client:
|
||||||
|
resp = await client.patch(
|
||||||
|
f"{REALESTATE_LAB_URL}/api/realestate/announcements/{announcement_id}/bookmark"
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
# --- lotto-backend ---
|
# --- lotto-backend ---
|
||||||
|
|
||||||
async def lotto_candidates(n: int = 20) -> Dict[str, Any]:
|
async def lotto_candidates(n: int = 20) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ async def handle_webhook(data: dict, agent_dispatcher=None) -> Optional[dict]:
|
|||||||
|
|
||||||
|
|
||||||
async def _handle_callback(callback_query: dict) -> Optional[dict]:
|
async def _handle_callback(callback_query: dict) -> Optional[dict]:
|
||||||
"""기존 승인/거절 콜백 처리 로직."""
|
"""승인/거절 및 realestate 북마크 콜백 처리."""
|
||||||
callback_id = callback_query.get("data", "")
|
callback_id = callback_query.get("data", "")
|
||||||
|
|
||||||
|
# realestate 북마크 토글 콜백 — DB 조회 없이 직접 처리
|
||||||
|
if callback_id.startswith("realestate_bookmark_"):
|
||||||
|
return await _handle_realestate_bookmark(callback_query, callback_id)
|
||||||
|
|
||||||
cb = get_telegram_callback(callback_id)
|
cb = get_telegram_callback(callback_id)
|
||||||
if not cb:
|
if not cb:
|
||||||
return None
|
return None
|
||||||
@@ -60,6 +65,38 @@ async def _handle_callback(callback_query: dict) -> Optional[dict]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_realestate_bookmark(callback_query: dict, callback_id: str) -> dict:
|
||||||
|
"""realestate_bookmark_{announcement_id} 콜백 처리."""
|
||||||
|
from .. import service_proxy
|
||||||
|
from .messaging import send_raw
|
||||||
|
|
||||||
|
# answerCallbackQuery 먼저 — 텔레그램 로딩 스피너 해제
|
||||||
|
await api_call(
|
||||||
|
"answerCallbackQuery",
|
||||||
|
{"callback_query_id": callback_query["id"], "text": "처리 중..."},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ann_id = int(callback_id.removeprefix("realestate_bookmark_"))
|
||||||
|
except ValueError:
|
||||||
|
await send_raw("⚠️ 잘못된 북마크 콜백 데이터")
|
||||||
|
return {"ok": False, "error": "invalid_callback_data"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await service_proxy.realestate_bookmark_toggle(ann_id)
|
||||||
|
bookmarked = result.get("bookmarked", None)
|
||||||
|
if bookmarked is True:
|
||||||
|
await send_raw(f"🔖 북마크 추가 완료 (#{ann_id})")
|
||||||
|
elif bookmarked is False:
|
||||||
|
await send_raw(f"🔖 북마크 해제 완료 (#{ann_id})")
|
||||||
|
else:
|
||||||
|
await send_raw(f"🔖 북마크 토글 완료 (#{ann_id})")
|
||||||
|
return {"ok": True, "announcement_id": ann_id}
|
||||||
|
except Exception as e:
|
||||||
|
await send_raw(f"⚠️ 북마크 처리 실패: {e}")
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
async def _handle_message(message: dict, agent_dispatcher) -> Optional[dict]:
|
async def _handle_message(message: dict, agent_dispatcher) -> Optional[dict]:
|
||||||
"""슬래시 명령 메시지 처리."""
|
"""슬래시 명령 메시지 처리."""
|
||||||
from .router import parse_command, resolve_agent_command, HELP_TEXT
|
from .router import parse_command, resolve_agent_command, HELP_TEXT
|
||||||
|
|||||||
128
agent-office/tests/test_realestate_callback.py
Normal file
128
agent-office/tests/test_realestate_callback.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import gc
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
_TMP = tempfile.mktemp(suffix=".db")
|
||||||
|
os.environ["AGENT_OFFICE_DB_PATH"] = _TMP
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _init_db():
|
||||||
|
gc.collect()
|
||||||
|
if os.path.exists(_TMP):
|
||||||
|
try:
|
||||||
|
os.remove(_TMP)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
from app.db import init_db
|
||||||
|
init_db()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_callback_realestate_bookmark_calls_proxy():
|
||||||
|
"""callback_data 'realestate_bookmark_42' 가 service_proxy.realestate_bookmark_toggle(42) 를 호출."""
|
||||||
|
from app import service_proxy
|
||||||
|
from app.telegram import webhook
|
||||||
|
|
||||||
|
fake_toggle = AsyncMock(return_value={"bookmarked": True})
|
||||||
|
fake_send = AsyncMock(return_value={"ok": True})
|
||||||
|
fake_api_call = AsyncMock(return_value={"ok": True})
|
||||||
|
|
||||||
|
update = {
|
||||||
|
"callback_query": {
|
||||||
|
"id": "cb1",
|
||||||
|
"from": {"id": 1},
|
||||||
|
"data": "realestate_bookmark_42",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(service_proxy, "realestate_bookmark_toggle", fake_toggle), \
|
||||||
|
patch("app.telegram.messaging.send_raw", fake_send), \
|
||||||
|
patch("app.telegram.webhook.api_call", fake_api_call):
|
||||||
|
result = asyncio.run(webhook.handle_webhook(update))
|
||||||
|
|
||||||
|
fake_toggle.assert_awaited_once_with(42)
|
||||||
|
assert result == {"ok": True, "announcement_id": 42}
|
||||||
|
|
||||||
|
|
||||||
|
def test_callback_realestate_bookmark_invalid_id():
|
||||||
|
"""callback_data 'realestate_bookmark_abc' 는 ValueError를 처리하고 에러 응답 반환."""
|
||||||
|
from app import service_proxy
|
||||||
|
from app.telegram import webhook
|
||||||
|
|
||||||
|
fake_toggle = AsyncMock(return_value={"bookmarked": True})
|
||||||
|
fake_send = AsyncMock(return_value={"ok": True})
|
||||||
|
fake_api_call = AsyncMock(return_value={"ok": True})
|
||||||
|
|
||||||
|
update = {
|
||||||
|
"callback_query": {
|
||||||
|
"id": "cb2",
|
||||||
|
"from": {"id": 1},
|
||||||
|
"data": "realestate_bookmark_abc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(service_proxy, "realestate_bookmark_toggle", fake_toggle), \
|
||||||
|
patch("app.telegram.messaging.send_raw", fake_send), \
|
||||||
|
patch("app.telegram.webhook.api_call", fake_api_call):
|
||||||
|
result = asyncio.run(webhook.handle_webhook(update))
|
||||||
|
|
||||||
|
fake_toggle.assert_not_awaited()
|
||||||
|
assert result is not None
|
||||||
|
assert result.get("ok") is False
|
||||||
|
assert result.get("error") == "invalid_callback_data"
|
||||||
|
|
||||||
|
|
||||||
|
def test_callback_realestate_bookmark_proxy_error():
|
||||||
|
"""service_proxy 가 예외를 던질 때 에러 응답 반환."""
|
||||||
|
from app import service_proxy
|
||||||
|
from app.telegram import webhook
|
||||||
|
|
||||||
|
fake_toggle = AsyncMock(side_effect=Exception("connection refused"))
|
||||||
|
fake_send = AsyncMock(return_value={"ok": True})
|
||||||
|
fake_api_call = AsyncMock(return_value={"ok": True})
|
||||||
|
|
||||||
|
update = {
|
||||||
|
"callback_query": {
|
||||||
|
"id": "cb3",
|
||||||
|
"from": {"id": 1},
|
||||||
|
"data": "realestate_bookmark_99",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(service_proxy, "realestate_bookmark_toggle", fake_toggle), \
|
||||||
|
patch("app.telegram.messaging.send_raw", fake_send), \
|
||||||
|
patch("app.telegram.webhook.api_call", fake_api_call):
|
||||||
|
result = asyncio.run(webhook.handle_webhook(update))
|
||||||
|
|
||||||
|
fake_toggle.assert_awaited_once_with(99)
|
||||||
|
assert result is not None
|
||||||
|
assert result.get("ok") is False
|
||||||
|
assert "connection refused" in result.get("error", "")
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_realestate_callback_uses_db_path():
|
||||||
|
"""approve_*/reject_* 콜백은 기존 DB 조회 경로를 사용 (realestate 분기를 타지 않음)."""
|
||||||
|
from app.telegram import webhook
|
||||||
|
|
||||||
|
fake_api_call = AsyncMock(return_value={"ok": True})
|
||||||
|
|
||||||
|
update = {
|
||||||
|
"callback_query": {
|
||||||
|
"id": "cb4",
|
||||||
|
"from": {"id": 1},
|
||||||
|
"data": "approve_abcd1234",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# DB에 등록되지 않은 콜백이므로 None 반환 — 기존 로직 진입 확인
|
||||||
|
with patch("app.telegram.webhook.api_call", fake_api_call):
|
||||||
|
result = asyncio.run(webhook.handle_webhook(update))
|
||||||
|
|
||||||
|
assert result is None # DB에 없으면 None 반환 (기존 동작 유지)
|
||||||
Reference in New Issue
Block a user