This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.474 補 PChome near-threshold matcher / feeder 下一階段:新增 HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 的窄範圍 total-price exact 回收;Recipe Box 可撕式水性兒童指甲油只進 identity_review,不自動寫正式價差;Pavaruni 蠟燭 vs 精油、DASHING DIVA 不同款式仍維持 veto/低信心。known-id refresh 現在會對 hard-veto 舊候選執行 fresh search recovery,missing known-id 若 fresh search 只找到低分候選也會保留 best candidate + diagnostics,而非落成 `refresh_no_result`;正式覆寫保護新增 stronger existing guard,避免較弱新候選以高分覆蓋既有強正式配對。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
|
||||
- V10.473 補背景 embedding host_health skip:`allow_111_fallback=false` 會讀最近 `host_health_probes`,跳過 runtime unhealthy 的 GCP 節點(預設 20 分鐘,DB fail-open),避免每筆任務都等待已知壞節點 timeout;路由安全不變,不把背景 embedding 落 111。
|
||||
- V10.472 補 GCP Ollama failover rootless 診斷:新增 `scripts/ops/diagnose_ollama_gcp_failover.sh`,可一鍵檢查 GCP-A direct、GCP-B direct、111、110:11435、110:11436 與 GCP-B `bge-m3` runtime;目前輸出確認 GCP-A `11434` refused、GCP-B direct/embed OK、110:11435 502、110:11436 OK。110 無免密 sudo、GCP-A 22 refused、GCP-B SSH key denied,因此 primary 修復需 GCP/SSH 或 110 root 權限;應用層維持 GCP-A → GCP-B → 111,不把背景 embedding 落 111。
|
||||
- V10.471 依 GCP-B `bge-m3` 實測 latency 調整 embedding timeout,已部署正式環境並確認 `/health=V10.471`:GCP-B `/api/embed` 三次實測約 6.4s / 7.3s / 23.5s,原本 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `8s` 會誤殺慢但成功的 embedding;已將背景 embedding cap 與 host health model probe timeout 預設調為 30s。正式 smoke 顯示容器內 embedding 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。路由安全不變:背景 embedding 仍只跑 GCP-A/GCP-B,不落 111。
|
||||
|
||||
@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.473"
|
||||
SYSTEM_VERSION = "V10.474"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- 2026-05-25 13:38 CST 狀態:已部署 `V10.471` 到 188,正式 `/health` 為 `V10.471`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、首頁 / daily / growth / host_health / ppt_audit_history / PChome review queue HTTP 200。GCP-B `bge-m3` `/api/embed` 直接實測約 6.4s、7.3s、23.5s,原 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `OLLAMA_HOST_HEALTH_EMBED_TIMEOUT=8` 會誤殺慢但成功的 embedding;預設改為 30s。正式容器內 embedding smoke 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。背景 embedding 路由安全不變:GCP-A → GCP-B,不落 111。
|
||||
- 2026-05-25 14:10 CST 起,`V10.472` 補 rootless GCP Ollama failover 診斷腳本與 DevOps SOP:`scripts/ops/diagnose_ollama_gcp_failover.sh` 會檢查 direct GCP-A/GCP-B/111、110 proxy `11435/11436` 與 GCP-B `bge-m3` runtime。現況輸出:GCP-A direct `/api/version` failed/refused、GCP-B direct OK、111 OK、110:11435 502、110:11436 OK、GCP-B embed OK;110 無免密 sudo,`ssh gcp-a` 22 refused、`ssh gcp-b` publickey denied,因此 primary 修復需 GCP/SSH 或 110 root 權限。
|
||||
- 2026-05-25 14:12 CST 起,`V10.473` 進行背景 embedding host_health skip:`allow_111_fallback=false` 的背景 embedding 會讀最近 `host_health_probes`,若 GCP-A/GCP-B runtime 已被標 unhealthy,直接跳過該節點並開 GCP circuit,不等待 30 秒 timeout、不落 111;DB 讀取失敗 fail-open。
|
||||
- 2026-05-25 14:45 CST 起,`V10.474` 補 PChome near-threshold matcher / feeder 下一階段:HOOOME 白色經典香氛暖燈、Gdesign Aroma Lava 2.0 進 total-price exact;Recipe Box 可撕式水性兒童指甲油保留 identity_review,不自動寫正式價差;Pavaruni 蠟燭 vs 精油與 DASHING DIVA 不同款式仍不放行。known-id refresh 會對 hard-veto 舊候選跑 fresh search recovery;missing known-id 若 fresh search 只有低分候選,也保留 best candidate + diagnostics,不再只記 `refresh_no_result`;正式覆寫保護新增 stronger existing guard。
|
||||
- 2026-05-25 12:05 CST 狀態:`main` 已部署到 188,正式 `/health` 為 `V10.467`,待推 Gitea。兩段變更已合併驗證:V10.466 rescore duplicate 改看 latest-state,7 筆 SKU 最新 attempt 全為 `rescore_accepted_current`,`competitor_prices` / `competitor_price_history` 目標計數未變;V10.467 focused exact matcher 在容器內回 `exact / total_price / price_alert_exact`。本輪 recreate `momo-app`、`scheduler`、`telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、PChome rescore queue API HTTP 200、Gemini 24 小時無 provider 紀錄、Ollama env 順序維持 GCP-A → GCP-B → 111、3 分鐘三容器 log 未見 Traceback / ERROR / CRITICAL / IntegrityError。
|
||||
|
||||
## 1. MOMO / PChome 核心比價準確率
|
||||
@@ -64,6 +65,7 @@
|
||||
- 2026-05-25 11:55 CST 起,rescore audit duplicate 判斷只看最新 attempt;若歷史已有 accepted 但後續 crawler 又追加低信心列,可重新 materialize 成最新 `rescore_accepted_current`。Production pilot 已將 SKU `14756069`、`11159042`、`13842560`、`8394210`、`15192547`、`10509765`、`10603780` 入人工覆核隊列;正式 `competitor_prices` / `competitor_price_history` 未寫入或改變。
|
||||
- 2026-05-25 12:20 CST 起,matcher 新增 `focused_exact_total_price_safe` 窄範圍通道;目前只覆蓋 3W CLINIC 粉底液 2入、花美水凝膠 3支、The Ordinary 咖啡因 EGCG 30ml、KUSSEN 屁屁膏 3入、Bone 擴香禮盒、1990 融燭燈白色款與 CANMAKE 淚袋盤等已確認同款樣本。這讓高信心 `exact/manual_review` 能轉為 `exact/total_price` 供 rescore pilot 入人工覆核;DASHING DIVA、唇彩、香味、色號/款式敏感商品仍不放行。
|
||||
- 2026-05-25 12:25 CST production pilot:SKU `6101639`、`10074951`、`7760902`、`TP00074980000005`、`14774766`、`10142589`、`10262470`、`10262471`、`11308520` 已從 `true_low_confidence` materialize 為 `rescore_accepted_current`,全數 `exact/total_price/price_alert_exact` 且理由含 `focused_exact_total_price_safe`。SKU `6101784` 因「即期品」商業條件不同,刻意保留在 `true_low_confidence`,不納入本輪自動入隊。
|
||||
- 2026-05-25 14:45 CST 起,matcher 擴充至香氛/精油近門檻安全 cohort:HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 可進 `exact/total_price/price_alert_exact`;Recipe Box 可撕式水性兒童指甲油只進 `identity_review`,因兒童指甲油仍可能藏色款/款式。DASHING DIVA 與 Pavaruni cross-type 負例已補測試,避免跨款式、跨劑型誤配。
|
||||
|
||||
## 3. 12 Agent 決策信封整合
|
||||
|
||||
|
||||
@@ -1371,6 +1371,13 @@ class CompetitorPriceFeeder:
|
||||
"replace_same_identity_better_score="
|
||||
f"{existing_score:.3f}->{match_score:.3f}",
|
||||
)
|
||||
if existing_score >= match_score:
|
||||
return (
|
||||
False,
|
||||
f"existing_match_conflict;stronger_existing;existing_id={existing_id};"
|
||||
f"incoming_id={incoming_id};existing_score={existing_score:.3f};"
|
||||
f"incoming_score={match_score:.3f}",
|
||||
)
|
||||
if match_score >= REPLACE_DIFFERENT_PRODUCT_SCORE:
|
||||
return True, f"replace_high_confidence_score={match_score:.3f}"
|
||||
return (
|
||||
@@ -1937,6 +1944,35 @@ class CompetitorPriceFeeder:
|
||||
attempts_written += 1
|
||||
continue
|
||||
|
||||
attempt_status = _classify_low_score_attempt(score, diagnostics)
|
||||
if (
|
||||
attempt_status == "recoverable_low_score"
|
||||
and _has_variant_selection_gap(momo_name, [(best_product, score, diagnostics)], score)
|
||||
):
|
||||
attempt_status = "true_low_confidence"
|
||||
attempt_terms = search_terms + [term for term in recovery_terms if term not in search_terms]
|
||||
self._record_match_attempt(
|
||||
sku,
|
||||
momo_name,
|
||||
momo_product_id=momo_product_id,
|
||||
momo_price=momo_price,
|
||||
search_terms=attempt_terms,
|
||||
candidate_count=max(1, recovery_candidate_count),
|
||||
attempt_status=attempt_status,
|
||||
best_product=best_product,
|
||||
best_score=score,
|
||||
diagnostics=diagnostics,
|
||||
error_message=(
|
||||
"missing_known_product_id_fresh_search_low_confidence; "
|
||||
f"PChome product_id not returned: {competitor_product_id}; "
|
||||
f"{_format_match_diagnostics(diagnostics)}"
|
||||
),
|
||||
source=source,
|
||||
)
|
||||
skipped_low += 1
|
||||
attempts_written += 1
|
||||
continue
|
||||
|
||||
self._record_match_attempt(
|
||||
sku,
|
||||
momo_name,
|
||||
@@ -1991,17 +2027,20 @@ class CompetitorPriceFeeder:
|
||||
if score < MIN_MATCH_SCORE:
|
||||
recovery_terms: list[str] = []
|
||||
recovery_candidate_count = 0
|
||||
if not getattr(diagnostics, "hard_veto", False):
|
||||
recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search(
|
||||
crawler,
|
||||
momo_name,
|
||||
momo_price=momo_price,
|
||||
existing_product_id=competitor_product_id,
|
||||
)
|
||||
if recovered:
|
||||
recovered_product, recovered_score, recovered_diagnostics = recovered
|
||||
if recovered_score > score:
|
||||
best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics
|
||||
recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search(
|
||||
crawler,
|
||||
momo_name,
|
||||
momo_price=momo_price,
|
||||
existing_product_id=competitor_product_id,
|
||||
)
|
||||
if recovered:
|
||||
recovered_product, recovered_score, recovered_diagnostics = recovered
|
||||
if (
|
||||
recovered_score > score
|
||||
or getattr(diagnostics, "hard_veto", False)
|
||||
and not getattr(recovered_diagnostics, "hard_veto", False)
|
||||
):
|
||||
best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics
|
||||
|
||||
if score >= MIN_MATCH_SCORE:
|
||||
extras = ["refresh_known_identity"]
|
||||
|
||||
@@ -365,6 +365,9 @@ SEARCH_IDENTITY_ANCHORS = (
|
||||
"天然植物香氛精油",
|
||||
"爆水擦澡濕巾",
|
||||
"嬰兒潤膚乳",
|
||||
"可撕式水性兒童指甲油",
|
||||
"aroma lava 解憂放鬆緩緩燈",
|
||||
"經典款香氛蠟燭暖燈",
|
||||
"我愛超磁妝定妝噴霧",
|
||||
"全天候超完美定妝噴霧",
|
||||
"怪獸級持色唇膏",
|
||||
@@ -475,6 +478,7 @@ FOCUSED_IDENTITY_REVIEW_ONLY_REASONS = {
|
||||
"kate_monster_lipstick_catalog",
|
||||
"opi_gel_polish_series_catalog",
|
||||
"romand_juicy_lip_tint_2_catalog",
|
||||
"recipe_box_peelable_child_polish_catalog",
|
||||
"solone_longlasting_eyeliner",
|
||||
"shu_auto_hard_formula_refill_catalog",
|
||||
"summer_eve_full_skin_wash_2pack",
|
||||
@@ -497,6 +501,8 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = {
|
||||
"selection1990_half_dome_wax_lamp_white",
|
||||
"selection1990_bendable_wax_lamp_white",
|
||||
"canmake_tear_bag_palette",
|
||||
"gdesign_aroma_lava_lamp_2",
|
||||
"hooome_classic_white_wax_lamp_bulbs_giftbox",
|
||||
}
|
||||
|
||||
SEARCH_BROAD_ANCHORS = {
|
||||
@@ -3512,6 +3518,38 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
|
||||
and "淚袋眼影盤" in right_text
|
||||
):
|
||||
return "canmake_tear_bag_palette"
|
||||
if (
|
||||
{"recipe", "box"} <= brand_tokens
|
||||
and "可撕式水性兒童指甲油" in left_text
|
||||
and "可撕式水性兒童指甲油" in right_text
|
||||
):
|
||||
return "recipe_box_peelable_child_polish_catalog"
|
||||
if (
|
||||
"gdesign" in (left.brand_tokens & right.brand_tokens)
|
||||
and "aroma" in left_text
|
||||
and "aroma" in right_text
|
||||
and "lava" in left_text
|
||||
and "lava" in right_text
|
||||
and "解憂放鬆緩緩燈2.0" in left_text
|
||||
and "解憂放鬆緩緩燈2.0" in right_text
|
||||
and "熔岩燈" in left_text
|
||||
and "熔岩燈" in right_text
|
||||
and "精油擴香" in left_text
|
||||
and "精油擴香" in right_text
|
||||
):
|
||||
return "gdesign_aroma_lava_lamp_2"
|
||||
if (
|
||||
"hooome" in (left.brand_tokens & right.brand_tokens)
|
||||
and "白色" in left_text
|
||||
and "白色" in right_text
|
||||
and "香氛蠟燭暖燈" in left_text
|
||||
and "香氛蠟燭暖燈" in right_text
|
||||
and "兩顆燈泡" in left_text
|
||||
and "兩顆燈泡" in right_text
|
||||
and "禮盒" in left_text
|
||||
and "禮盒" in right_text
|
||||
):
|
||||
return "hooome_classic_white_wax_lamp_bulbs_giftbox"
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -1658,6 +1658,251 @@ def test_competitor_feeder_refresh_recovers_when_known_id_missing(monkeypatch):
|
||||
assert attempts[0]["attempt_status"] == "matched"
|
||||
|
||||
|
||||
def test_competitor_feeder_records_missing_known_id_low_score_candidate(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
candidate = PChomeProduct(
|
||||
product_id="DDAB01-LOW",
|
||||
name="Recipe Box 韓國 recipebox 可撕式水性兒童指甲油",
|
||||
price=299,
|
||||
original_price=350,
|
||||
discount=15,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-LOW",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.8,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def fetch_product_details(self, product_ids, batch_size=20):
|
||||
assert product_ids == ["DDAB01-MISSING"]
|
||||
return True, "ok", []
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", [candidate]
|
||||
|
||||
def fake_score(*_args, **_kwargs):
|
||||
return SimpleNamespace(
|
||||
score=0.742,
|
||||
brand_score=1.0,
|
||||
token_score=0.66,
|
||||
spec_score=0.55,
|
||||
sequence_score=0.60,
|
||||
type_score=0.55,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=("shared_identity_anchor_variant_safe",),
|
||||
comparison_mode="exact_identity",
|
||||
match_type="no_match",
|
||||
price_basis="none",
|
||||
alert_tier="suppress",
|
||||
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_known_identity_refresh_items([{
|
||||
"sku": "TP00018610000157",
|
||||
"name": "韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)",
|
||||
"product_id": 1,
|
||||
"momo_price": 299,
|
||||
"competitor_product_id": "DDAB01-MISSING",
|
||||
}])
|
||||
|
||||
assert result.matched == 0
|
||||
assert result.skipped_low_score == 1
|
||||
assert attempts[0]["attempt_status"] == "recoverable_low_score"
|
||||
assert attempts[0]["best_product"].product_id == "DDAB01-LOW"
|
||||
assert "missing_known_product_id_fresh_search_low_confidence" in attempts[0]["error_message"]
|
||||
|
||||
|
||||
def test_competitor_feeder_refresh_recovers_when_known_id_is_hard_veto(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
stale = PChomeProduct(
|
||||
product_id="DDAB01-STALE",
|
||||
name="Pavaruni 香氛蠟燭500g",
|
||||
price=980,
|
||||
original_price=1200,
|
||||
discount=18,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-STALE",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
recovered = PChomeProduct(
|
||||
product_id="DDAB01-RECOVERED",
|
||||
name="Pavaruni 天然植物香氛精油40種香味10ml",
|
||||
price=399,
|
||||
original_price=499,
|
||||
discount=20,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-RECOVERED",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.8,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def fetch_product_details(self, product_ids, batch_size=20):
|
||||
assert product_ids == ["DDAB01-STALE"]
|
||||
return True, "ok", [stale]
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", [stale, recovered]
|
||||
|
||||
def fake_score(_momo_name, competitor_name, **_kwargs):
|
||||
if "RECOVERED" in competitor_name or "天然植物香氛精油" in competitor_name:
|
||||
return SimpleNamespace(
|
||||
score=0.84,
|
||||
brand_score=1.0,
|
||||
token_score=0.78,
|
||||
spec_score=1.0,
|
||||
sequence_score=0.70,
|
||||
type_score=1.0,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=("shared_identity_anchor",),
|
||||
comparison_mode="exact_identity",
|
||||
match_type="exact",
|
||||
price_basis="total_price",
|
||||
alert_tier="price_alert_exact",
|
||||
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
|
||||
)
|
||||
return SimpleNamespace(
|
||||
score=0.32,
|
||||
brand_score=1.0,
|
||||
token_score=0.20,
|
||||
spec_score=0.0,
|
||||
sequence_score=0.20,
|
||||
type_score=0.0,
|
||||
price_penalty=0.0,
|
||||
hard_veto=True,
|
||||
reasons=("type_conflict",),
|
||||
comparison_mode="not_comparable",
|
||||
match_type="no_match",
|
||||
price_basis="none",
|
||||
alert_tier="suppress",
|
||||
tags=["identity_v2", "identity_veto"],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
|
||||
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
|
||||
feeder = CompetitorPriceFeeder(engine=object())
|
||||
attempts = []
|
||||
writes = []
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_should_upsert_competitor_price",
|
||||
lambda *_args, **_kwargs: (True, "same_or_empty_existing"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_upsert_competitor_price",
|
||||
lambda sku, product, score, tags, **kwargs: writes.append({
|
||||
"sku": sku,
|
||||
"product_id": product.product_id,
|
||||
"score": score,
|
||||
"tags": tags,
|
||||
**kwargs,
|
||||
}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_record_match_attempt",
|
||||
lambda *args, **kwargs: attempts.append(kwargs),
|
||||
)
|
||||
|
||||
result = feeder._run_known_identity_refresh_items([{
|
||||
"sku": "PAVARUNI-OIL",
|
||||
"name": "【Pavaruni】天然植物香氛精油40種香味10ml",
|
||||
"product_id": 1,
|
||||
"momo_price": 399,
|
||||
"competitor_product_id": "DDAB01-STALE",
|
||||
}])
|
||||
|
||||
assert result.matched == 1
|
||||
assert writes[0]["product_id"] == "DDAB01-RECOVERED"
|
||||
assert "fresh_search_recovery" in writes[0]["tags"]
|
||||
assert attempts[0]["attempt_status"] == "matched"
|
||||
|
||||
|
||||
def test_should_upsert_protects_stronger_existing_identity_candidate():
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE competitor_prices (
|
||||
sku TEXT,
|
||||
source TEXT,
|
||||
competitor_product_id TEXT,
|
||||
competitor_product_name TEXT,
|
||||
match_score REAL,
|
||||
tags TEXT
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT INTO competitor_prices (
|
||||
sku, source, competitor_product_id, competitor_product_name, match_score, tags
|
||||
) VALUES (
|
||||
'14133077',
|
||||
'pchome',
|
||||
'DDAB01-STRONG',
|
||||
'PONY EFFECT 絕對持久定妝噴霧 100ml',
|
||||
0.950,
|
||||
'["identity_v2","comparison_exact_identity","brand_match"]'
|
||||
)
|
||||
"""))
|
||||
|
||||
feeder = CompetitorPriceFeeder(engine=engine)
|
||||
product = SimpleNamespace(
|
||||
product_id="DDAB01-WEAKER",
|
||||
name="PONY EFFECT 絕對持久定妝噴霧",
|
||||
)
|
||||
|
||||
should_write, reason = feeder._should_upsert_competitor_price(
|
||||
"14133077",
|
||||
product,
|
||||
0.850,
|
||||
source="pchome",
|
||||
)
|
||||
|
||||
assert should_write is False
|
||||
assert "stronger_existing" in reason
|
||||
assert "existing_score=0.950" in reason
|
||||
assert "incoming_score=0.850" in reason
|
||||
|
||||
|
||||
def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
@@ -2125,6 +2125,68 @@ def test_marketplace_matcher_promotes_lactacyd_private_wash_exact_line():
|
||||
assert "shared_identity_anchor" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_safe_aroma_lamps_to_total_price():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
hooome = score_marketplace_match(
|
||||
"【HOOOME】白色 經典款香氛蠟燭暖燈 薰香燈 蠟燭燈 香氛燈 燭燈 融蠟燈(可調光+附兩顆燈泡+精美禮盒包裝 /",
|
||||
"【HOOOME】白色 經典款香氛蠟燭暖燈 蠟燭燈 香氛燈 融燭燈 融蠟燈 (可調光+附兩顆燈泡+禮盒包裝)",
|
||||
momo_price=1290,
|
||||
competitor_price=1290,
|
||||
)
|
||||
gdesign = score_marketplace_match(
|
||||
"【Gdesign】全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)",
|
||||
"全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)",
|
||||
momo_price=1980,
|
||||
competitor_price=1980,
|
||||
)
|
||||
|
||||
for diagnostics in (hooome, gdesign):
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
assert diagnostics.match_type == "exact"
|
||||
assert diagnostics.price_basis == "total_price"
|
||||
assert diagnostics.alert_tier == "price_alert_exact"
|
||||
|
||||
assert "focused_exact_identity_hooome_classic_white_wax_lamp_bulbs_giftbox" in hooome.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_keeps_recipe_box_child_polish_catalog_in_review():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)",
|
||||
"【Recipe Box】韓國 recipebox 可撕式水性兒童指甲油",
|
||||
momo_price=299,
|
||||
competitor_price=299,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
assert diagnostics.price_basis == "manual_review"
|
||||
assert diagnostics.alert_tier == "identity_review"
|
||||
assert "variant_selection_review" in diagnostics.reasons
|
||||
assert "focused_exact_identity_recipe_box_peelable_child_polish_catalog" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_keeps_aroma_and_nail_variant_gaps_blocked():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
pavaruni_cross_type = score_marketplace_match(
|
||||
"【Pavaruni】美國香氛蠟燭20種香味500g(大豆蠟燭禮盒/天然植物精油/花香/木質香/果香)",
|
||||
"【美國Pavaruni】天然植物香氛精油40種香味10ml 多款任選",
|
||||
)
|
||||
dashing_cross_style = score_marketplace_match(
|
||||
"【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-輕躍裸粉(貓眼 漸層)",
|
||||
"Dashing Diva/F 時尚潮流美甲片-流光裸色 30片/盒 MDF5F015CG",
|
||||
)
|
||||
|
||||
assert pavaruni_cross_type.hard_veto is True
|
||||
assert pavaruni_cross_type.score < 0.76
|
||||
assert dashing_cross_style.score < 0.76
|
||||
assert "variant_descriptor_conflict" in dashing_cross_style.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
|
||||
Reference in New Issue
Block a user