- git mv stock-lab/ → stock/ - docker-compose.yml: 서비스 키 + container_name + build.context + frontend.depends_on + agent-office STOCK_LAB_URL → STOCK_URL - agent-office/app: config.py, service_proxy.py, agents/stock.py, tests/ STOCK_LAB_URL → STOCK_URL - nginx/default.conf: proxy_pass http://stock-lab → http://stock (3 lines) - CLAUDE.md / README.md / STATUS.md / scripts/ 문구 갱신 - stock/ 내부 자기 참조 갱신 lab 네이밍 정책 (feedback_lab_naming.md) graduation. API URL / Python import / DB 파일명 변경 없음.
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
import json
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from app.screener.ai_news import analyzer
|
|
|
|
|
|
def _mk_llm(content_text: str, in_tokens: int = 100, out_tokens: int = 20):
|
|
llm = AsyncMock()
|
|
resp = MagicMock()
|
|
block = MagicMock()
|
|
block.text = content_text
|
|
resp.content = [block]
|
|
resp.usage = MagicMock(input_tokens=in_tokens, output_tokens=out_tokens)
|
|
llm.messages = MagicMock()
|
|
llm.messages.create = AsyncMock(return_value=resp)
|
|
return llm
|
|
|
|
|
|
NEWS = [
|
|
{"title": "삼성전자, HBM 양산", "summary": "1분기 영업이익 사상 최대", "pub_date": "2026-05-14"},
|
|
{"title": "메모리 가격 반등", "summary": "", "pub_date": "2026-05-14"},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_score_sentiment_success_parses_json():
|
|
llm = _mk_llm(json.dumps({"score": 7.5, "reason": "HBM 호재"}))
|
|
out = await analyzer.score_sentiment(llm, "005930", NEWS, name="삼성전자")
|
|
assert out["ticker"] == "005930"
|
|
assert out["score_raw"] == 7.5
|
|
assert out["reason"] == "HBM 호재"
|
|
assert out["news_count"] == 2
|
|
assert out["tokens_input"] == 100
|
|
assert out["tokens_output"] == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_score_sentiment_json_parse_fail_returns_zero():
|
|
llm = _mk_llm("not valid json")
|
|
out = await analyzer.score_sentiment(llm, "005930", NEWS)
|
|
assert out["score_raw"] == 0.0
|
|
assert "parse fail" in out["reason"]
|
|
assert out["tokens_input"] == 100 # 호출은 발생했음
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_score_sentiment_clamps_out_of_range():
|
|
llm = _mk_llm(json.dumps({"score": 15.0, "reason": "초강세"}))
|
|
out = await analyzer.score_sentiment(llm, "005930", NEWS)
|
|
assert out["score_raw"] == 10.0 # +10 클램프
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_score_sentiment_clamps_negative_out_of_range():
|
|
llm = _mk_llm(json.dumps({"score": -42.0, "reason": "초악재"}))
|
|
out = await analyzer.score_sentiment(llm, "005930", NEWS)
|
|
assert out["score_raw"] == -10.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_score_sentiment_includes_summary_in_prompt():
|
|
"""summary 가 있으면 prompt 에 포함, 없으면 title 만."""
|
|
llm = _mk_llm(json.dumps({"score": 5.0, "reason": "ok"}))
|
|
await analyzer.score_sentiment(llm, "005930", NEWS, name="삼성전자")
|
|
call = llm.messages.create.call_args
|
|
user_msg = call.kwargs["messages"][0]["content"]
|
|
assert "1분기 영업이익 사상 최대" in user_msg # summary 포함
|
|
assert "삼성전자, HBM 양산" in user_msg # title 포함
|
|
assert "2026-05-14" in user_msg # pub_date 포함
|