market-assistant/backend/pipeline/tests/test_competitor_brief.py
hub-gif 614d631261 feat(pipeline): 竞品简报与策略管线、报告导出及 JD 前端联动
补充策略决策键、fixture 与相关单测;分析/营销包本地存储与任务状态;竞品简报 schema 与 LLM 章节生成、促销与导出调整;并纳入 Cursor 工程规则 user-dev-style。

Made-with: Cursor
2026-04-27 15:48:09 +08:00

175 lines
7.0 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.

"""结构化竞品摘要:空样本烟测(不依赖真实 run_dir CSV"""
from __future__ import annotations
import tempfile
from pathlib import Path
from django.test import SimpleTestCase
from pipeline.competitor_report import jd_report as jcr
from pipeline.competitor_report.comment_sentiment import (
_comment_sentiment_lexicon,
build_comment_sentiment_llm_payload,
)
from pipeline.csv.schema import infer_total_sales_from_sales_floor
from pipeline.reporting.charts import _cn_volume_int
class BuildCompetitorBriefTests(SimpleTestCase):
def test_empty_merged_json_safe(self) -> None:
with tempfile.TemporaryDirectory() as td:
run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True)
out = jcr.build_competitor_brief(
run_dir=run_dir,
keyword="测试",
merged_rows=[],
search_export_rows=[],
comment_rows=[],
meta=None,
)
self.assertEqual(out["schema_version"], 1)
self.assertEqual(out["scope"]["merged_sku_count"], 0)
self.assertIsInstance(out["strategy_hints"], list)
self.assertEqual(out["matrix_by_group"], [])
self.assertNotIn("comment_sentiment_lexicon", out)
import json
json.dumps(out)
def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None:
texts = ["口感软硬适中很好吃", "太差了不建议"]
attr = [f"【细类ASKU1品名x店铺y】{t}" for t in texts]
pl = build_comment_sentiment_llm_payload(
texts,
attributed_texts=attr,
shuffle_seed="unit-test-seed",
semantic_pool_max=10,
)
self.assertIn("sample_reviews_semantic_pool", pl)
self.assertNotIn("comment_sentiment_lexicon", pl)
self.assertNotIn("negative_lexeme_hits_top", pl)
self.assertGreaterEqual(len(pl["sample_reviews_semantic_pool"]), 1)
def test_comment_sentiment_score_then_lexeme(self) -> None:
texts = ["很好吃", "太差了", "一般般"]
scores = [5, 1, 3]
lex = _comment_sentiment_lexicon(texts, scores)
self.assertEqual(lex.get("method"), "score_then_lexeme")
self.assertEqual(lex.get("positive_only"), 1)
self.assertEqual(lex.get("negative_only"), 1)
self.assertEqual(lex.get("neutral_or_empty"), 1)
pl = build_comment_sentiment_llm_payload(texts, scores=scores)
dist = pl.get("star_rating_distribution") or {}
self.assertEqual(dist.get("score_1_2"), 1)
self.assertEqual(dist.get("score_3"), 1)
self.assertEqual(dist.get("score_4_5"), 1)
self.assertNotIn("comment_sentiment_lexicon", pl)
def test_comment_sentiment_all_scores_missing_falls_back_keyword(self) -> None:
texts = ["好吃推荐", "差评"]
scores = [None, None]
lex = _comment_sentiment_lexicon(texts, scores)
self.assertEqual(lex.get("method"), "keyword_lexicon")
def test_brief_omits_preset_comment_focus_keywords(self) -> None:
with tempfile.TemporaryDirectory() as td:
run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True)
out = jcr.build_competitor_brief(
run_dir=run_dir,
keyword="测试",
merged_rows=[],
search_export_rows=[],
comment_rows=[
{
"tagCommentContent": "自定义词阿尔法出现两次 自定义词阿尔法",
}
],
meta=None,
report_config={"comment_focus_words": ["自定义词阿尔法"]},
)
self.assertEqual(out["comment_focus_keywords"], [])
def test_matrix_groups_require_detail_category_path(self) -> None:
sku_h = "SKU(skuId)"
merged = [
{
sku_h: "111",
"detail_category_path": "食品饮料 > 休闲食品 > 饼干 > 粗粮饼干",
"标题(wareName)": "A",
},
{sku_h: "222", "标题(wareName)": "B"},
]
groups = jcr._merged_rows_grouped_for_matrix(merged)
self.assertEqual(len(groups), 1)
self.assertEqual(len(groups[0][1]), 1)
self.assertEqual(groups[0][1][0][sku_h], "111")
smap = jcr._sku_to_matrix_group_map(merged, sku_h)
self.assertEqual(smap.get("111"), "饼干")
self.assertNotIn("222", smap)
fb = jcr._consumer_feedback_by_matrix_group(
merged_rows=merged,
comment_rows=[
{"sku": "222", "tagCommentContent": "缺路径仍不应计入按细类统计"},
{"sku": "111", "tagCommentContent": "有路径进细类"},
],
sku_header=sku_h,
)
counts = {g: len(cr) for g, cr, _ in fb}
self.assertEqual(counts.get("饼干"), 1)
def test_comment_lines_with_product_context_prefix(self) -> None:
"""评价抽样须带细类/SKU/品名前缀,便于归因。"""
sku_h = "SKU(skuId)"
title_h = "标题(wareName)"
merged = [
{
sku_h: "100",
title_h: "低GI全麦饼干1kg",
"detail_brand": "B",
"detail_price_final": "29",
"detail_shop_name": "",
"detail_category_path": "休闲食品 > 饼干 > 粗粮饼干",
"detail_product_attributes": "x",
},
]
comments = [{"sku": "100", "tagCommentContent": "整体口感还差点意思"}]
lines = jcr._comment_lines_with_product_context(
comments, merged, sku_header=sku_h, title_h=title_h
)
self.assertEqual(len(lines), 1)
self.assertIn("【细类:", lines[0])
self.assertIn("SKU100", lines[0])
self.assertIn("品名:", lines[0])
self.assertIn("店铺:", lines[0])
self.assertIn("整体口感还差点意思", lines[0])
def test_cn_volume_int_parses_total_sales_trailer(self) -> None:
self.assertEqual(
_cn_volume_int("已售50万+ | good:99%好评"), 500_000
)
self.assertEqual(_cn_volume_int("2.5亿件"), 250_000_000)
def test_mix_top_remainder_sums_to_all_rows(self) -> None:
"""mix_top 各 count 之和须等于 strip 后可统计行数(与扇图同源)。"""
names = [f"{i}" for i in range(30)]
mix = jcr._counter_mix_top_rows_with_remainder(
names, top_n=24, remainder_label="(其余店铺)"
)
self.assertEqual(sum(v for _, v in mix), 30)
self.assertEqual(mix[-1][0], "(其余店铺)")
self.assertEqual(mix[-1][1], 6)
self.assertEqual(len(jcr._structure_names_for_pie_counter(names)), 30)
def test_infer_total_sales_from_sales_floor(self) -> None:
self.assertEqual(
infer_total_sales_from_sales_floor("good:99%好评 | 已售50万+"),
"已售50万+",
)
self.assertEqual(infer_total_sales_from_sales_floor(""), "")