This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
@@ -42,6 +43,7 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8")
|
||||
migration = (ROOT / "migrations/023_competitor_match_attempts.sql").read_text(encoding="utf-8")
|
||||
diagnostics_migration = (ROOT / "migrations/041_competitor_match_diagnostics.sql").read_text(encoding="utf-8")
|
||||
browse_migration = (ROOT / "migrations/042_add_browse_diagnostics_to_match_attempts.sql").read_text(encoding="utf-8")
|
||||
|
||||
assert "attempts_written" in source
|
||||
assert "_ensure_competitor_match_attempts_table" in source
|
||||
@@ -56,6 +58,9 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
assert 'attempt_status="no_match"' in source
|
||||
assert 'attempt_status="error"' in source
|
||||
assert "_search_pchome_candidates(crawler, momo_name, search_terms, momo_price=momo_price)" in source
|
||||
assert "_prepare_browse_diagnostic" in source
|
||||
assert "browse_diagnostic_json" in source
|
||||
assert "PCHOME_FEEDER_BROWSE_SH_EXECUTE_ENABLED" in source
|
||||
assert 'attempt_status="protected_existing_match"' in source
|
||||
assert "_should_upsert_competitor_price" in source
|
||||
assert "_classify_low_score_attempt" in source
|
||||
@@ -88,6 +93,8 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
assert "match_diagnostic_json" in diagnostics_migration
|
||||
assert "comparison_mode" in diagnostics_migration
|
||||
assert "diagnostic_codes" in diagnostics_migration
|
||||
assert "browse_diagnostic_json" in browse_migration
|
||||
assert "idx_comp_match_attempts_browse_diag_time" in browse_migration
|
||||
assert "competitor_product_url" in source
|
||||
assert "competitor_image_url" in source
|
||||
assert "competitor_stock" in source
|
||||
@@ -95,6 +102,74 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
|
||||
assert "idx_comp_match_attempts_sku_source_time" in migration
|
||||
|
||||
|
||||
def test_competitor_feeder_records_browse_sh_plan_for_no_result(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", []
|
||||
|
||||
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
|
||||
feeder = CompetitorPriceFeeder(engine=object())
|
||||
attempts = []
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_record_match_attempt",
|
||||
lambda *args, **kwargs: attempts.append(kwargs),
|
||||
)
|
||||
|
||||
result = feeder._run_sku_items([{
|
||||
"sku": "BROWSE001",
|
||||
"name": "MOMO 稀有專櫃組合 50ml",
|
||||
"product_id": 901,
|
||||
"momo_price": 1280,
|
||||
}])
|
||||
|
||||
assert result.matched == 0
|
||||
assert result.skipped_no_result == 1
|
||||
browse_plan = attempts[0]["browse_diagnostic"]
|
||||
assert browse_plan["tool"] == "browse.sh"
|
||||
assert browse_plan["mode"] == "plan_only"
|
||||
assert browse_plan["execute_enabled"] is False
|
||||
assert browse_plan["reason"] == "no_result"
|
||||
assert browse_plan["execution"]["status"] == "disabled"
|
||||
assert browse_plan["suggested_commands"][0]["args"][0] == "get"
|
||||
assert "ecshweb.pchome.com.tw/search" in browse_plan["urls"][0]
|
||||
|
||||
|
||||
def test_competitor_match_attempt_persists_browse_diagnostic_json():
|
||||
from sqlalchemy import create_engine, text
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
feeder = CompetitorPriceFeeder(engine=engine)
|
||||
feeder._record_match_attempt(
|
||||
sku="BROWSE002",
|
||||
momo_name="MOMO 取證測試商品",
|
||||
search_terms=["取證 測試"],
|
||||
attempt_status="no_result",
|
||||
browse_diagnostic={
|
||||
"tool": "browse.sh",
|
||||
"mode": "plan_only",
|
||||
"urls": ["https://ecshweb.pchome.com.tw/search/v3.3/?q=test"],
|
||||
},
|
||||
)
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(text("""
|
||||
SELECT browse_diagnostic_json
|
||||
FROM competitor_match_attempts
|
||||
WHERE sku = 'BROWSE002'
|
||||
""")).scalar_one()
|
||||
|
||||
payload = json.loads(row)
|
||||
assert payload["tool"] == "browse.sh"
|
||||
assert payload["mode"] == "plan_only"
|
||||
|
||||
|
||||
def test_match_diagnostics_payload_carries_professional_match_lanes():
|
||||
from services.competitor_price_feeder import _match_diagnostics_payload, _extend_match_tags
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
@@ -591,6 +666,166 @@ def test_competitor_feeder_marks_weak_identity_as_true_low_confidence(monkeypatc
|
||||
assert attempts[0]["attempt_status"] == "true_low_confidence"
|
||||
|
||||
|
||||
def test_competitor_feeder_downgrades_variant_selection_gap_from_recoverable(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
products = [
|
||||
PChomeProduct(
|
||||
product_id="DDAB01-08",
|
||||
name="PERIPERA 雙頭旋轉極細眉筆 08深杏色 0.05g",
|
||||
price=180,
|
||||
original_price=220,
|
||||
discount=18,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-08",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
),
|
||||
PChomeProduct(
|
||||
product_id="DDAB01-09",
|
||||
name="PERIPERA 雙頭旋轉極細眉筆 09灰褐棕 0.05g",
|
||||
price=180,
|
||||
original_price=220,
|
||||
discount=18,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-09",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
),
|
||||
PChomeProduct(
|
||||
product_id="DDAB01-11",
|
||||
name="PERIPERA 雙頭旋轉極細眉筆 11摩卡灰褐 0.05g",
|
||||
price=180,
|
||||
original_price=220,
|
||||
discount=18,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-11",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
),
|
||||
]
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", products
|
||||
|
||||
def fake_score(_momo_name, competitor_name, **_kwargs):
|
||||
return SimpleNamespace(
|
||||
score=0.734 if "09灰褐棕" in competitor_name else 0.733,
|
||||
brand_score=1.0,
|
||||
token_score=0.74,
|
||||
spec_score=0.55,
|
||||
sequence_score=0.66,
|
||||
type_score=0.55,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=("shared_identity_anchor_packaging_variant",),
|
||||
comparison_mode="exact_identity",
|
||||
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
|
||||
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
|
||||
feeder = CompetitorPriceFeeder(engine=object())
|
||||
attempts = []
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_record_match_attempt",
|
||||
lambda *args, **kwargs: attempts.append(kwargs),
|
||||
)
|
||||
|
||||
result = feeder._run_sku_items([{
|
||||
"sku": "P001",
|
||||
"name": "【peripera官方直營】雙頭旋轉極細眉筆_多色任選(1.5mm極細筆頭)",
|
||||
"product_id": 11,
|
||||
"momo_price": 180,
|
||||
}])
|
||||
|
||||
assert result.matched == 0
|
||||
assert result.skipped_low_score == 1
|
||||
assert attempts[0]["attempt_status"] == "true_low_confidence"
|
||||
|
||||
|
||||
def test_competitor_feeder_treats_choose_one_offer_as_missing_variant_signal(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
product = PChomeProduct(
|
||||
product_id="DDAB01-YSL",
|
||||
name="【YSL聖羅蘭】恆久完美透膚煙染腮紅 6g ( #12/ #57/ #93)",
|
||||
price=1650,
|
||||
original_price=1780,
|
||||
discount=7,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-YSL",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", [product]
|
||||
|
||||
def fake_score(*_args, **_kwargs):
|
||||
return SimpleNamespace(
|
||||
score=0.735,
|
||||
brand_score=1.0,
|
||||
token_score=0.74,
|
||||
spec_score=0.55,
|
||||
sequence_score=0.66,
|
||||
type_score=1.0,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=("shared_identity_anchor_packaging_variant",),
|
||||
comparison_mode="exact_identity",
|
||||
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
|
||||
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
|
||||
feeder = CompetitorPriceFeeder(engine=object())
|
||||
attempts = []
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_record_match_attempt",
|
||||
lambda *args, **kwargs: attempts.append(kwargs),
|
||||
)
|
||||
|
||||
result = feeder._run_sku_items([{
|
||||
"sku": "Y001",
|
||||
"name": "【YSL】官方直營 恆久完美透膚煙染腮紅(腮紅/任選1款/新品上市)",
|
||||
"product_id": 12,
|
||||
"momo_price": 1650,
|
||||
}])
|
||||
|
||||
assert result.matched == 0
|
||||
assert result.skipped_low_score == 1
|
||||
assert attempts[0]["attempt_status"] == "true_low_confidence"
|
||||
|
||||
|
||||
def test_should_upsert_allows_same_identity_candidate_to_replace_lower_score():
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
|
||||
@@ -503,6 +503,49 @@ def test_marketplace_matcher_promotes_recipe_box_marketing_line_drift():
|
||||
assert "shared_identity_anchor_recipe_box_line" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_st_deodorizer_with_brand_alias_and_line_anchor():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【日本雞仔牌ST】室內消臭力智能光感應3段定時無線自動除臭芳香噴霧機(內贈芳香劑39ml 衛浴精油擴香瓶棒組)",
|
||||
"日本ST雞仔牌-室內消臭力智能光感應3段定時無線自動除臭芳香噴霧機1入(含芳香劑39ml)",
|
||||
momo_price=699,
|
||||
competitor_price=699,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert "shared_identity_anchor_exact_line" in diagnostics.reasons or "shared_identity_anchor_packaging_variant" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_nivea_dry_lotion_with_long_shared_anchor():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【NIVEA 妮維雅】男士無印乾爽止汗爽身乳液(無印止汗滾珠/德國妮維雅)",
|
||||
"【NIVEA 妮維雅】止汗爽身乳液 無印乾爽50ml",
|
||||
momo_price=129,
|
||||
competitor_price=129,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert "shared_identity_anchor_nivea_dry_lotion" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_refill_core_vs_case_only_pack():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【KATE 凱婷】3D造型眉彩餅補充芯(眉彩刷、眉餅盒分開販售)",
|
||||
"【KATE 凱婷】眉彩餅盒一入款(搭配3D造型眉彩餅補充芯)",
|
||||
momo_price=280,
|
||||
competitor_price=280,
|
||||
)
|
||||
|
||||
assert diagnostics.score < 0.76
|
||||
assert diagnostics.hard_veto is True
|
||||
assert "accessory_case_conflict" in diagnostics.reasons or "refill_pack_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_suppresses_wide_price_penalty_for_exact_lip_product():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
|
||||
Reference in New Issue
Block a user