FE가 detail(객체)을 그대로 렌더해 React #31 크래시 → 안전한 텍스트 필드 제공. condition_reason(조건→한줄근거), agent-office telegram_trade._COND_REASON과 동일 유지.
97 lines
4.5 KiB
Python
97 lines
4.5 KiB
Python
import pytest
|
|
from unittest.mock import patch
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def client(monkeypatch, tmp_path):
|
|
from app import db as _db
|
|
monkeypatch.setattr(_db, "DB_PATH", str(tmp_path / "stock.db"))
|
|
_db.init_db()
|
|
monkeypatch.setenv("WEBAI_API_KEY", "k")
|
|
from app.main import app
|
|
return TestClient(app)
|
|
|
|
|
|
def _report(client, firing):
|
|
return client.post("/api/webai/trade-alert/report",
|
|
headers={"X-WebAI-Key": "k"},
|
|
json={"as_of": "2026-07-02T09:01:00+09:00", "firing": firing})
|
|
|
|
|
|
def test_report_new_edge_sends_and_persists(client):
|
|
firing = [{"ticker": "005930", "name": "삼성전자", "kind": "buy",
|
|
"condition": "buy_breakout", "price": 71500, "detail": {"vol": 2.0}}]
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True) as m:
|
|
r1 = _report(client, firing)
|
|
assert r1.json()["new_alerts"] == 1
|
|
assert m.called
|
|
# 2번째 동일 firing → 유지, 신규 0
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
|
r2 = _report(client, firing)
|
|
assert r2.json()["new_alerts"] == 0
|
|
# 이력 1건
|
|
assert len(client.get("/api/stock/trade-alerts?days=1").json()["alerts"]) == 1
|
|
|
|
|
|
def test_report_send_failure_does_not_persist(client):
|
|
firing = [{"ticker": "005930", "name": "삼성전자", "kind": "buy",
|
|
"condition": "buy_breakout", "price": 71500, "detail": {}}]
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=False):
|
|
r = _report(client, firing)
|
|
assert r.json()["new_alerts"] == 0 # 전송 실패 → 미채택
|
|
# 다음 사이클(전송 성공) 재시도되어 알림
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
|
r2 = _report(client, firing)
|
|
assert r2.json()["new_alerts"] == 1
|
|
|
|
|
|
def test_report_cooldown_suppresses_immediate_refire(client):
|
|
"""같은 종목·조건이 해제됐다 곧바로 재발화해도 쿨다운(기본 6h) 내면 재알림 억제."""
|
|
firing = [{"ticker": "005930", "name": "삼성", "kind": "buy",
|
|
"condition": "buy_breakout", "price": 71500, "detail": {}}]
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
|
assert _report(client, firing).json()["new_alerts"] == 1 # 최초 알림
|
|
_report(client, []) # 해제
|
|
r = _report(client, firing) # 즉시 재발화 → 쿨다운 억제
|
|
assert r.json()["new_alerts"] == 0
|
|
assert r.json()["suppressed"] == 1
|
|
|
|
|
|
def test_report_refire_after_cooldown_alerts(client, monkeypatch):
|
|
"""쿨다운=0이면 해제 후 재발화 시 재알림."""
|
|
monkeypatch.setenv("TRADE_ALERT_COOLDOWN_HOURS", "0")
|
|
firing = [{"ticker": "005930", "name": "삼성", "kind": "buy",
|
|
"condition": "buy_breakout", "price": 71500, "detail": {}}]
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
|
_report(client, firing)
|
|
_report(client, [])
|
|
r = _report(client, firing)
|
|
assert r.json()["new_alerts"] == 1
|
|
|
|
|
|
def test_trade_alerts_history_includes_reason_string(client):
|
|
"""이력 API가 조건별 사람이 읽는 근거(reason) 문자열을 함께 반환한다(FE가 detail 객체 대신 렌더)."""
|
|
firing = [{"ticker": "005930", "name": "삼성", "kind": "sell",
|
|
"condition": "sell_ma_break", "price": 60000,
|
|
"detail": {"ma50": 1, "ma200": 2, "severity": "high"}}]
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
|
_report(client, firing)
|
|
alerts = client.get("/api/stock/trade-alerts?days=1").json()["alerts"]
|
|
assert isinstance(alerts[0]["reason"], str) and len(alerts[0]["reason"]) > 3
|
|
|
|
|
|
def test_report_resolves_stock_name_from_watchlist(client):
|
|
"""워커 firing에 name이 없어도 NAS가 종목명을 해석해 알림에 포함한다."""
|
|
from app import db
|
|
db.add_watchlist("000660", "SK하이닉스")
|
|
firing = [{"ticker": "000660", "kind": "buy", "condition": "buy_breakout",
|
|
"price": 180000, "detail": {}}] # name 없음
|
|
with patch("app.trade_alerts.notify_agent_office", return_value=True) as m:
|
|
_report(client, firing)
|
|
sent_alert = m.call_args[0][0][0]
|
|
assert sent_alert["name"] == "SK하이닉스"
|
|
# 이력에도 종목명 기록
|
|
alerts = client.get("/api/stock/trade-alerts?days=1").json()["alerts"]
|
|
assert alerts[0]["name"] == "SK하이닉스"
|