"""AlertHit[] → 飞书消息:分组 / 截断 / 含关键词(text + post 两种格式)。""" from __future__ import annotations from datetime import datetime from app.services.compare_alert import AlertHit from app.services.compare_alert_format import ALERT_KEYWORD, format_alert_message, format_alert_post def _hits(n, alert_type="T1"): return [ AlertHit( trace_id=f"t{i}", alert_type=alert_type, reason="比价过程出错", app_version="v0.3.4", created_at=None, trace_url=None, user_id=None, ) for i in range(n) ] # ---- format_alert_message(纯文本,保留原有测试) ---- def test_contains_keyword_and_count(): msg = format_alert_message( _hits(2), window_label="2026-08-04 08:00–08:30", max_detail_per_type=20, max_total=50, ) assert ALERT_KEYWORD in msg assert "本期触发 2 条" in msg assert "系统技术失败 2 条" in msg assert "t0" in msg and "v0.3.4" in msg def test_group_by_type(): hits = _hits(1, "T1") + _hits(1, "T6") + _hits(1, "T5") msg = format_alert_message(hits, window_label="w", max_detail_per_type=20, max_total=50) assert "系统技术失败 1 条" in msg assert "商品识别失败 1 条" in msg assert "深度放弃(cancelled) 1 条" in msg def test_per_type_truncation(): msg = format_alert_message(_hits(25), window_label="w", max_detail_per_type=20, max_total=50) assert msg.count("t0") == 1 assert "另有 5 条" in msg def test_total_truncation_counts_only(): msg = format_alert_message(_hits(60), window_label="w", max_detail_per_type=20, max_total=50) assert "系统技术失败 60 条" in msg assert "t0" not in msg assert "分析库" in msg # ---- format_alert_post(富文本 post) ---- def _hits_with_meta(n, alert_type="T1", *, user_id=None, trace_url=None, created_at=None): return [ AlertHit( trace_id=f"tr{i}", alert_type=alert_type, reason="比价过程出错", app_version="v1.2.3", created_at=created_at or datetime(2026, 8, 4, 10, 30), trace_url=trace_url, user_id=user_id, ) for i in range(n) ] def test_post_title_contains_keyword(): hits = _hits_with_meta(1) title, content = format_alert_post( hits, window_label="2026-08-04 10:00", phone_map={}, max_detail_per_type=20, max_total=50, ) assert ALERT_KEYWORD in title def test_post_content_is_list(): hits = _hits_with_meta(2) title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) assert isinstance(content, list) assert len(content) >= 1 # 每个段落是 list[dict] for para in content: assert isinstance(para, list) for elem in para: assert "tag" in elem def test_post_summary_count(): hits = _hits_with_meta(3, "T1") + _hits_with_meta(2, "T6") title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) # 摘要第一段含合计数和类型计数 first_para_text = "".join(e.get("text", "") for e in content[0]) assert "合计 5 条" in first_para_text assert "系统技术失败 3" in first_para_text assert "商品识别失败 2" in first_para_text def test_post_phone_map_applied(): hits = _hits_with_meta(1, user_id=42) title, content = format_alert_post( hits, window_label="w", phone_map={42: "13800138000"}, max_detail_per_type=20, max_total=50, ) # 找明细行(非摘要非表头)中含手机号 all_text = " ".join( e.get("text", "") for para in content for e in para ) assert "13800138000" in all_text def test_post_no_user_id_shows_dash(): hits = _hits_with_meta(1, user_id=None) title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) all_text = " ".join(e.get("text", "") for para in content for e in para) assert "| - |" in all_text def test_post_trace_url_becomes_a_element(): hits = _hits_with_meta(1, trace_url="https://trace.example.com/tr0") title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) # 找 tag=a 的元素 a_elements = [e for para in content for e in para if e.get("tag") == "a"] assert len(a_elements) == 1 assert a_elements[0]["href"] == "https://trace.example.com/tr0" assert a_elements[0]["text"] == "trace" def test_post_no_trace_url_shows_trace_id_prefix(): hits = _hits_with_meta(1, trace_url=None) title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) # 无 trace_url 时:tag=text, text=trace_id[:16] detail_texts = [e.get("text", "") for para in content for e in para if e.get("tag") == "text"] # trace_id 是 tr0,截 16 位 assert any("tr0" in t for t in detail_texts) def test_post_total_truncation_no_detail(): hits = _hits_with_meta(60) title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) # 超 max_total:只有摘要+截断提示,无表头、无明细 all_text = " ".join(e.get("text", "") for para in content for e in para) assert "合计 60 条" in all_text assert "时间 | 手机号" not in all_text assert "tr0" not in all_text assert "分析库" in all_text def test_post_per_type_truncation(): hits = _hits_with_meta(25) title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) all_text = " ".join(e.get("text", "") for para in content for e in para) assert "另有 5 条" in all_text def test_post_created_at_formatting(): hits = [ AlertHit( trace_id="tx1", alert_type="T1", reason="测试", app_version="v2.0", created_at=datetime(2026, 8, 4, 10, 30), trace_url=None, user_id=None, ) ] title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) all_text = " ".join(e.get("text", "") for para in content for e in para) assert "08-04 10:30" in all_text def test_post_no_created_at_shows_dash(): hits = [ AlertHit( trace_id="tx2", alert_type="T1", reason="测试", app_version=None, created_at=None, trace_url=None, user_id=None, ) ] title, content = format_alert_post( hits, window_label="w", phone_map={}, max_detail_per_type=20, max_total=50, ) all_text = " ".join(e.get("text", "") for para in content for e in para) # 无 created_at 时间显示 "-" assert "- |" in all_text