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_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하이닉스"