Files
intl_news/tests/test_report_db.py
T
simon c72a5ed13a feat: AI 模型按场景独立配置(llm_scenes)+ 去重多来源合并
- system.yaml 新增 llm_scenes(translation / daily_report,含用途/方法/模型要求说明)
- load_llm_config(scene=...) 场景覆盖;日报摘要 temperature 0.3 硬编码 → 配置
- M3 去重:唯一篇记录 source_ids(跨源重复合并,首个来源为 source_id)
- source_ids 经翻译透传至 events,日报事件 source 多来源拼接展示(≤3 个)
- 已部署 pi5:merge 实证 10 源合并;日报 report_id=224 正常入库
2026-08-12 09:56:55 +08:00

202 lines
8.1 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_data(intl 结构)
# --------------------------------------------------------------------------- #
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
def test_multi_source_label(self) -> None:
"""去重合并后的多来源 → source 拼接展示(Reuters, CNBC)。"""
now = datetime(2026, 8, 4, 8, 0, 0)
ev = _fake_high_event("多来源事件", 5, source_id="reuters",
url="https://reuters.com/news/9")
# 模拟 M3 去重合并:source_ids 含两个来源
ev["article"]["source_ids"] = ["reuters", "cnbc"]
r = _build_report_data(now, {}, [ev], Counter(), Counter(), Counter(),
Counter(), "")
assert r.events[0].source == "Reuters, CNBC"
assert r.events[0].url == "https://reuters.com/news/9"
def test_single_source_falls_back(self) -> None:
"""source_ids 为空/单一时回退单源逻辑(不拼接)。"""
now = datetime(2026, 8, 4, 8, 0, 0)
ev = _fake_high_event("单来源事件", 4, source_id="investinglive",
url="https://investinglive.com/news/3")
ev["article"]["source_ids"] = ["investinglive"]
r = _build_report_data(now, {}, [ev], Counter(), Counter(), Counter(),
Counter(), "")
assert r.events[0].source == "InvestingLive"