This commit is contained in:
@@ -62,7 +62,7 @@ def test_fetch_candidates_falls_back_without_sales_on_sqlite_snapshot():
|
||||
"competitor_product_id, competitor_product_name, tags, crawled_at"
|
||||
") VALUES ("
|
||||
"'SKU-1', 'pchome', NULL, 0.9, 999, 1200, 16.75, "
|
||||
"'PCH-1', '測試商品 PChome', '[\"on_sale\"]', '2026-05-13 10:00:00'"
|
||||
"'PCH-1', '測試商品 PChome', '[\"identity_v2\", \"on_sale\"]', '2026-05-13 10:00:00'"
|
||||
")"
|
||||
))
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
assert 'attempt_status="no_result"' in source
|
||||
assert 'attempt_status="no_match"' in source
|
||||
assert 'attempt_status="error"' in source
|
||||
assert "_search_pchome_candidates(crawler, momo_name, search_terms)" in source
|
||||
assert "_search_pchome_candidates(crawler, momo_name, search_terms, momo_price=momo_price)" in source
|
||||
assert 'attempt_status="needs_review"' in source
|
||||
assert "_should_upsert_competitor_price" in source
|
||||
assert "replace_legacy_unverified" in source
|
||||
assert "identity_v2" in source
|
||||
|
||||
assert "CREATE TABLE IF NOT EXISTS competitor_match_attempts" in migration
|
||||
assert "attempt_status" in migration
|
||||
@@ -31,12 +35,12 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
|
||||
def test_competitor_feeder_logs_keyword_parser_fallback(monkeypatch, caplog):
|
||||
from services import competitor_price_feeder
|
||||
from services.price_comparison import ProductNameParser
|
||||
from services import marketplace_product_matcher
|
||||
|
||||
def broken_parse(self, *_args, **_kwargs):
|
||||
raise RuntimeError("parser unavailable")
|
||||
def broken_build_search_terms(*_args, **_kwargs):
|
||||
raise RuntimeError("matcher unavailable")
|
||||
|
||||
monkeypatch.setattr(ProductNameParser, "parse", broken_parse)
|
||||
monkeypatch.setattr(marketplace_product_matcher, "build_search_terms", broken_build_search_terms)
|
||||
caplog.set_level(logging.DEBUG, logger="services.competitor_price_feeder")
|
||||
|
||||
terms = competitor_price_feeder._build_search_keywords("理膚寶水 B5 修復霜 40ml")
|
||||
|
||||
@@ -322,7 +322,10 @@ def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
|
||||
assert "competitor.product_url" in dashboard
|
||||
assert "dashboard-competition-badge" in dashboard
|
||||
assert "decision.summary" in dashboard
|
||||
assert "PChome 待比對" in dashboard
|
||||
assert "PChome {{ match_status.label" in dashboard
|
||||
assert "_load_pchome_match_attempt_map" in route_source
|
||||
assert "低信心待審" in route_source
|
||||
assert "規格衝突待審" in route_source
|
||||
assert "series.pchome" in dashboard
|
||||
assert "label: 'PChome'" in dashboard
|
||||
assert "含 PChome 歷史快照" in dashboard
|
||||
@@ -348,7 +351,9 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
|
||||
route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")
|
||||
template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")
|
||||
|
||||
assert "MIN_MATCH_SCORE = 0.42" in feeder_source
|
||||
assert "MIN_MATCH_SCORE = 0.62" in feeder_source
|
||||
assert "REPLACE_DIFFERENT_PRODUCT_SCORE" in feeder_source
|
||||
assert "marketplace_product_matcher" in feeder_source
|
||||
assert "MAX_SEARCH_TERMS" in feeder_source
|
||||
assert "_build_search_keywords" in feeder_source
|
||||
assert "_search_pchome_candidates" in feeder_source
|
||||
|
||||
87
tests/test_marketplace_product_matcher.py
Normal file
87
tests/test_marketplace_product_matcher.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
|
||||
def test_marketplace_matcher_accepts_same_product_identity():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"理膚寶水 B5 修復霜 40ml",
|
||||
"理膚寶水 全面修復霜 B5 40ml",
|
||||
momo_price=699,
|
||||
competitor_price=679,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.9
|
||||
assert diagnostics.hard_veto is False
|
||||
assert "brand_match" in diagnostics.tags
|
||||
assert "spec_match" in diagnostics.tags
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_brand_conflict_even_when_volume_matches():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【蘭蔻】官方直營 玫瑰霜60ml+玫瑰精露150ml",
|
||||
"LOREAL Paris 巴黎萊雅 金致臻顏花蜜奢養膠原輕盈乳霜_60ml",
|
||||
momo_price=18765,
|
||||
competitor_price=1249,
|
||||
)
|
||||
|
||||
assert diagnostics.score < 0.62
|
||||
assert diagnostics.hard_veto is True
|
||||
assert "brand_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_volume_conflict_for_same_brand():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【CLARINS 克蘭詩】黃金亮眼萃20mlX2囤貨組",
|
||||
"【CLARINS克蘭詩】黃金雙萃精華 75ml",
|
||||
momo_price=4500,
|
||||
competitor_price=3549,
|
||||
)
|
||||
|
||||
assert diagnostics.score < 0.62
|
||||
assert diagnostics.hard_veto is True
|
||||
assert "volume_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_handles_bundle_piece_count():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【Relove】馬甲纖纖飲-莓果風味X3盒 共72包",
|
||||
"【3入超值組】Relove 馬甲纖纖飲 24包/7克",
|
||||
momo_price=4590,
|
||||
competitor_price=4590,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.62
|
||||
assert diagnostics.hard_veto is False
|
||||
|
||||
|
||||
def test_batch_compare_top_uses_latest_momo_price_not_revenue(monkeypatch):
|
||||
from services import pchome_crawler
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, i_code TEXT)"))
|
||||
conn.execute(text("CREATE TABLE daily_sales (product_id INTEGER, revenue NUMERIC, date TEXT)"))
|
||||
conn.execute(text("CREATE TABLE price_records (id INTEGER PRIMARY KEY, product_id INTEGER, price NUMERIC, timestamp TEXT)"))
|
||||
conn.execute(text("INSERT INTO products VALUES (1, '理膚寶水 B5 修復霜 40ml', 'SKU001')"))
|
||||
conn.execute(text("INSERT INTO daily_sales VALUES (1, 999999, '2026-05-19')"))
|
||||
conn.execute(text("INSERT INTO price_records VALUES (1, 1, 699, '2026-05-19 10:00:00')"))
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_compare_product(name, momo_price, momo_icode=""):
|
||||
calls.append((name, momo_price, momo_icode))
|
||||
return {"found": True, "momo_price": momo_price}
|
||||
|
||||
monkeypatch.setattr(pchome_crawler, "compare_product", fake_compare_product)
|
||||
|
||||
results = pchome_crawler.batch_compare_top(engine, top_n=1)
|
||||
|
||||
assert results == [{"found": True, "momo_price": 699.0}]
|
||||
assert calls == [("理膚寶水 B5 修復霜 40ml", 699.0, "SKU001")]
|
||||
Reference in New Issue
Block a user