Files
ewoooc/tests/test_competitor_match_attempts_persistence.py
OoO 756b01af66
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
補齊 PChome 比價人工覆核閉環
2026-05-20 10:00:58 +08:00

213 lines
7.7 KiB
Python

from pathlib import Path
import logging
from datetime import datetime
ROOT = Path(__file__).resolve().parents[1]
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")
assert "attempts_written" in source
assert "_ensure_competitor_match_attempts_table" in source
assert "_record_match_attempt" in source
assert "INSERT INTO competitor_match_attempts" in source
assert "CAST(:search_terms AS jsonb)" in source
assert 'attempt_status="matched"' in source
assert 'attempt_status="low_score"' in source
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, 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 "_fetch_expired_identity_skus" in source
assert "run_expired_identity_refresh" in source
assert "refresh_known_identity" in source
assert 'attempt_status="unit_comparable"' in source
assert 'attempt_status="refresh_unit_comparable"' in source
assert "mode={getattr(diagnostics, 'comparison_mode'" in source
assert 'PCHOME_FEEDER_TIMEOUT", "12"' in source
assert "PChomeCrawler(timeout=REQUEST_TIMEOUT" in source
assert "CREATE TABLE IF NOT EXISTS competitor_match_attempts" in migration
assert "attempt_status" in migration
assert "search_terms" in migration
assert "best_match_score" in migration
assert "error_message" in migration
assert "idx_comp_match_attempts_sku_source_time" in migration
def test_competitor_match_review_service_closes_human_review_loop():
service_source = (ROOT / "services/competitor_match_review_service.py").read_text(encoding="utf-8")
migration = (ROOT / "migrations/039_create_competitor_match_reviews.sql").read_text(encoding="utf-8")
dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8")
assert "VALID_REVIEW_ACTIONS" in service_source
assert "accept_identity" in service_source
assert "reject_identity" in service_source
assert "unit_price_required" in service_source
assert "manual_accepted" in service_source
assert "manual_rejected" in service_source
assert "manual_unit_price_required" in service_source
assert "INSERT INTO competitor_match_reviews" in service_source
assert "INSERT INTO competitor_prices" in service_source
assert "INSERT INTO competitor_price_history" in service_source
assert "manual_review" in service_source
assert "manual_accept" in service_source
assert "CREATE TABLE IF NOT EXISTS competitor_match_reviews" in migration
assert "review_action" in migration
assert "reviewer_identity" in migration
assert "candidate_diagnostic" in migration
assert "idx_comp_match_reviews_sku_source_time" in migration
assert "runPchomeReviewDecision" in dashboard_js
assert "/api/pchome-review/" in dashboard_js
def test_competitor_feeder_logs_keyword_parser_fallback(monkeypatch, caplog):
from services import competitor_price_feeder
from services import marketplace_product_matcher
def broken_build_search_terms(*_args, **_kwargs):
raise RuntimeError("matcher unavailable")
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")
assert terms
assert "fallback to cleaned product name" in caplog.text
def test_competitor_feeder_refreshes_expired_identity_by_known_product_id(monkeypatch):
from services.competitor_price_feeder import CompetitorPriceFeeder
from services.pchome_crawler import PChomeProduct
requested = []
product = PChomeProduct(
product_id="DDAB01-1900ABCD",
name="舒特膚 AD 乳液 200ml",
price=899,
original_price=999,
discount=10,
image_url="",
product_url="https://24h.pchome.com.tw/prod/DDAB01-1900ABCD",
stock=50,
store="24h",
rating=4.8,
review_count=12,
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):
requested.extend(product_ids)
return True, "ok", [product]
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
feeder = CompetitorPriceFeeder(engine=object())
writes = []
attempts = []
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": "A001",
"name": "舒特膚 AD 乳液 200ml",
"product_id": 1,
"momo_price": 980,
"competitor_product_id": "DDAB01-1900ABCD",
}])
assert requested == ["DDAB01-1900ABCD"]
assert result.matched == 1
assert writes[0]["product_id"] == "DDAB01-1900ABCD"
assert "identity_v2" in writes[0]["tags"]
assert "refresh_known_identity" in writes[0]["tags"]
assert attempts[0]["attempt_status"] == "matched"
assert attempts[0]["search_terms"] == ["known_product_id:DDAB01-1900ABCD"]
def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch):
from services.competitor_price_feeder import CompetitorPriceFeeder
from services.pchome_crawler import PChomeProduct
product = PChomeProduct(
product_id="DDAB01-UNIT",
name="理膚寶水 全面修復霜 B5 40ml",
price=679,
original_price=799,
discount=15,
image_url="",
product_url="https://24h.pchome.com.tw/prod/DDAB01-UNIT",
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]
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
feeder = CompetitorPriceFeeder(engine=object())
attempts = []
writes = []
monkeypatch.setattr(
feeder,
"_record_match_attempt",
lambda *args, **kwargs: attempts.append(kwargs),
)
monkeypatch.setattr(
feeder,
"_upsert_competitor_price",
lambda *args, **kwargs: writes.append((args, kwargs)),
)
result = feeder._run_sku_items([{
"sku": "A002",
"name": "理膚寶水 B5 全面修復霜 40ml x2 超值組",
"product_id": 2,
"momo_price": 1199,
}])
assert result.matched == 0
assert result.skipped_low_score == 1
assert writes == []
assert attempts[0]["attempt_status"] == "unit_comparable"
assert "unit_comparable" in attempts[0]["error_message"]