Files
intl_news/tests/test_report_db.py
T
simon d4a55bcaaa feat: M9 日报结构化入库(HTML 改为写入 MySQL news_report/news_event,report_type=intl)
- 新增 report_db/ 包(models/schema/db,复用 news 项目实现,幂等 upsert)
- reporter.py: _build_report_data + generate_report 写库返回 report_id
- pipeline/cli 适配 report_id 返回值;HTML 渲染/上传保留 deprecated
- 新增 tests/test_report_db.py;.env.example 增加 NEWS_DB_* 配置
- 已部署 pi5 并验证:真实生成 report_id=184(12 事件)+ 幂等覆盖
2026-08-04 11:09:17 +08:00

180 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""日报结构化入库:模型 + _build_report_data 组装(纯逻辑,不连 DB)。"""
from __future__ import annotations
import json
from collections import Counter
from datetime import date, datetime
import pytest
from pydantic import ValidationError
from report_db.models import EventRow, ReportData
from scheduler.reporter import _build_report_data
# --------------------------------------------------------------------------- #
# 模型默认值
# --------------------------------------------------------------------------- #
class TestEventRow:
def test_minimal(self) -> None:
ev = EventRow(section="intl", rank=1, title="标题")
assert ev.importance is None
assert ev.sentiment is None
def test_full(self) -> None:
ev = EventRow(
section="intl", rank=2, importance=4, event_type="地缘政治",
title="t", summary="s", sentiment="negative", source="InvestingLive",
url="https://x.com/1",
)
assert ev.sentiment == "negative"
def test_missing_title_raises(self) -> None:
with pytest.raises(ValidationError):
EventRow(section="intl", rank=1) # type: ignore[call-arg]
class TestReportData:
def test_defaults(self) -> None:
r = ReportData(
report_date=date(2026, 8, 4),
report_type="intl",
generated_at=datetime(2026, 8, 4, 8, 0),
)
assert r.file_name == ""
assert r.stats == {}
assert r.events == []
def test_with_events(self) -> None:
r = ReportData(
report_date=date(2026, 8, 4),
report_type="intl",
generated_at=datetime(2026, 8, 4, 8, 0),
ai_summary="摘要",
events=[EventRow(section="intl", rank=1, title="t")],
)
assert len(r.events) == 1
# --------------------------------------------------------------------------- #
# _build_report_dataintl 结构)
# --------------------------------------------------------------------------- #
def _fake_high_event(title_zh: str, importance: int, *,
event_type: str = "其他", sentiment: str = "neutral",
summary_zh: str = "摘要", source_id: str = "investinglive",
url: str = "https://investinglive.com/news/1") -> dict:
"""构造 _dedup_events 之后的事件结构(含 article 属性)。"""
return {
"importance": importance,
"sentiment": sentiment,
"summary_zh": summary_zh,
"event_type": event_type,
"stock_codes": [],
"article": {
"title": f"EN {title_zh}",
"title_zh": title_zh,
"url": url,
"source_id": source_id,
},
}
class TestBuildReportData:
def test_sections_and_ranks(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
high = [
_fake_high_event("新闻A", 5),
_fake_high_event("新闻B", 4),
_fake_high_event("新闻C", 3),
]
r = _build_report_data(
now, {"raw_total": 100, "proc": 90}, high,
Counter(), Counter(), Counter(), Counter(), "AI摘要",
)
assert r.report_date == date(2026, 8, 4)
assert r.report_type == "intl"
assert r.file_name == ""
assert r.ai_summary == "AI摘要"
assert [(e.section, e.rank) for e in r.events] == [
("intl", 1), ("intl", 2), ("intl", 3),
]
def test_field_mapping(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
high = [_fake_high_event("英伟达财报超预期", 5, event_type="公司财报",
sentiment="positive", summary_zh="营收超预期",
source_id="investinglive",
url="https://investinglive.com/news/42")]
r = _build_report_data(now, {}, high, Counter(), Counter(), Counter(),
Counter(), "")
ev = r.events[0]
assert ev.title == "英伟达财报超预期" # title_zh 优先
assert ev.importance == 5
assert ev.event_type == "公司财报"
assert ev.summary == "营收超预期"
assert ev.sentiment == "positive"
assert ev.url == "https://investinglive.com/news/42"
assert ev.source == "InvestingLive" # 域名 → 源展示名
def test_source_fallback_without_url(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
ev = _fake_high_event("无URL事件", 4, url="")
r = _build_report_data(now, {}, [ev], Counter(), Counter(), Counter(),
Counter(), "")
assert r.events[0].url is None
# url 为空时回退 source_id → sources.yaml 展示名
assert r.events[0].source == "InvestingLive"
def test_stats_snapshot(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
high = [
_fake_high_event("A", 5, event_type="宏观", sentiment="positive"),
_fake_high_event("B", 4, event_type="宏观", sentiment="negative"),
]
r = _build_report_data(
now,
{"raw_total": 120, "proc": 100, "deduped": 90,
"emb_count": 80, "qdrant_count": 75},
high,
Counter({"positive": 1, "negative": 1}),
Counter({5: 1, 4: 1}),
Counter({"宏观": 2}),
Counter({"investinglive": 2}),
"摘要",
)
# stats 可 JSON 序列化(入库时 json.dumps
json.dumps(r.stats, ensure_ascii=False)
assert r.stats["pipeline"]["raw_total"] == 120
assert r.stats["sentiment"] == {"positive": 1, "negative": 1}
assert r.stats["importance"] == [
{"importance": 4, "count": 1}, {"importance": 5, "count": 1},
]
assert r.stats["event_types"] == [{"event_type": "宏观", "count": 2}]
assert r.stats["source_dist"] == [{"source": "InvestingLive", "count": 2}]
def test_title_truncated(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
ev = _fake_high_event("长" * 600, 4)
r = _build_report_data(now, {}, [ev], Counter(), Counter(), Counter(),
Counter(), "")
assert len(r.events[0].title) == 512
def test_empty_events(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
r = _build_report_data(now, {}, [], Counter(), Counter(), Counter(),
Counter(), "")
assert r.events == []
assert r.ai_summary is None
def test_question_mark_sentiment_normalized(self) -> None:
now = datetime(2026, 8, 4, 8, 0, 0)
ev = _fake_high_event("未分类情绪", 4, sentiment="?")
r = _build_report_data(now, {}, [ev], Counter(), Counter(), Counter(),
Counter(), "")
# "?" 不写入 DB,留 None
assert r.events[0].sentiment is None