[V10.359] 導入 browse.sh 診斷與色號防錯配
This commit is contained in:
81
tests/test_browse_sh_tool.py
Normal file
81
tests/test_browse_sh_tool.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def test_browse_sh_availability_reports_missing_cli(monkeypatch):
|
||||
from services import browse_sh_tool
|
||||
from services.browse_sh_tool import BrowseShTool
|
||||
|
||||
monkeypatch.delenv("BROWSE_SH_CLI", raising=False)
|
||||
monkeypatch.setattr(browse_sh_tool.shutil, "which", lambda _name: None)
|
||||
|
||||
availability = BrowseShTool().availability()
|
||||
|
||||
assert availability.available is False
|
||||
assert availability.command == tuple()
|
||||
assert "未安裝" in availability.reason
|
||||
|
||||
|
||||
def test_browse_sh_build_command_prefers_env_override(monkeypatch):
|
||||
from services import browse_sh_tool
|
||||
from services.browse_sh_tool import BrowseShTool
|
||||
|
||||
monkeypatch.setenv("BROWSE_SH_CLI", "/opt/bin/browse")
|
||||
monkeypatch.setattr(browse_sh_tool.shutil, "which", lambda _name: "/usr/local/bin/browse")
|
||||
|
||||
assert BrowseShTool().build_command(("skills", "list")) == (
|
||||
"/opt/bin/browse",
|
||||
"skills",
|
||||
"list",
|
||||
)
|
||||
|
||||
|
||||
def test_browse_sh_run_returns_success(monkeypatch):
|
||||
from services.browse_sh_tool import BrowseShTool
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_run(command, **kwargs):
|
||||
calls.append((command, kwargs))
|
||||
return SimpleNamespace(returncode=0, stdout="ok", stderr="")
|
||||
|
||||
monkeypatch.setattr("services.browse_sh_tool.subprocess.run", fake_run)
|
||||
|
||||
result = BrowseShTool(cli_path="/bin/browse").run(("skills", "list"), require_available=False)
|
||||
|
||||
assert result.ok is True
|
||||
assert result.command == ("/bin/browse", "skills", "list")
|
||||
assert result.stdout == "ok"
|
||||
assert calls[0][0] == ("/bin/browse", "skills", "list")
|
||||
|
||||
|
||||
def test_browse_sh_run_converts_timeout(monkeypatch):
|
||||
from services.browse_sh_tool import BrowseShTool
|
||||
|
||||
def fake_run(command, **_kwargs):
|
||||
raise subprocess.TimeoutExpired(command, timeout=1, output="partial", stderr="late")
|
||||
|
||||
monkeypatch.setattr("services.browse_sh_tool.subprocess.run", fake_run)
|
||||
|
||||
result = BrowseShTool(cli_path="/bin/browse").run(("screenshot",), require_available=False)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.timed_out is True
|
||||
assert result.stdout == "partial"
|
||||
assert result.stderr == "late"
|
||||
|
||||
|
||||
def test_browse_sh_run_surfaces_broken_cli(monkeypatch):
|
||||
from services.browse_sh_tool import BrowseShTool
|
||||
|
||||
def fake_run(command, **kwargs):
|
||||
if command[-1] == "--version":
|
||||
return SimpleNamespace(returncode=1, stdout="", stderr="node dyld missing icu4c")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr("services.browse_sh_tool.subprocess.run", fake_run)
|
||||
|
||||
result = BrowseShTool(cli_path="/bin/browse").run(("skills", "list"))
|
||||
|
||||
assert result.ok is False
|
||||
assert "icu4c" in result.unavailable_reason
|
||||
@@ -724,6 +724,54 @@ def test_search_candidates_does_not_stop_on_merely_acceptable_match(monkeypatch)
|
||||
assert [candidate.product_id for candidate in candidates] == ["DDAB01-FIRST", "DDAB01-SECOND"]
|
||||
|
||||
|
||||
def test_search_candidates_adds_variant_recall_sorts_for_dashing_diva(monkeypatch):
|
||||
from services.competitor_price_feeder import _search_pchome_candidates
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
candidate = PChomeProduct(
|
||||
product_id="DDBH8E-A900JMCJZ",
|
||||
name="Dashing Diva/F 時尚潮流美甲片-月光銀影 MDF5F010AG",
|
||||
price=420,
|
||||
original_price=520,
|
||||
discount=19,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDBH8E-A900JMCJZ",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def search_products(self, keyword, **kwargs):
|
||||
self.calls.append((keyword, kwargs.get("sort")))
|
||||
return True, "ok", [candidate]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"services.marketplace_product_matcher.score_marketplace_match",
|
||||
lambda *_args, **_kwargs: SimpleNamespace(score=0.72),
|
||||
)
|
||||
|
||||
crawler = FakeCrawler()
|
||||
_search_pchome_candidates(
|
||||
crawler,
|
||||
"【DASHING DIVA】MAGICPRESS時尚潮流美甲片_極光之藍",
|
||||
keywords=["dashing diva 時尚潮流美甲片 極光之藍"],
|
||||
momo_price=331,
|
||||
)
|
||||
|
||||
assert crawler.calls == [
|
||||
("dashing diva 時尚潮流美甲片 極光之藍", None),
|
||||
("dashing diva 時尚潮流美甲片", "sale/dc"),
|
||||
("dashing diva 時尚潮流美甲片", "new/dc"),
|
||||
]
|
||||
|
||||
|
||||
def test_competitor_feeder_logs_keyword_parser_fallback(monkeypatch, caplog):
|
||||
from services import competitor_price_feeder
|
||||
from services import marketplace_product_matcher
|
||||
|
||||
@@ -591,6 +591,45 @@ def test_marketplace_matcher_promotes_variant_safe_exact_option():
|
||||
assert "shared_identity_anchor_variant_safe" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_explicit_shade_option_mismatch():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
lipstick = score_marketplace_match(
|
||||
"【Maybelline 媚比琳】超持久水光鎖吻唇釉#62 4.2ml",
|
||||
"MAYBELLINE 媚比琳 超持久水光鎖吻唇釉 #120 4.2ml",
|
||||
momo_price=399,
|
||||
competitor_price=399,
|
||||
)
|
||||
primer = score_marketplace_match(
|
||||
"【植村秀】水凝光透妝前防護乳 紫色 30ml",
|
||||
"植村秀 水凝光透妝前防護乳 粉色 30ml",
|
||||
momo_price=1200,
|
||||
competitor_price=1100,
|
||||
)
|
||||
|
||||
for diagnostics in (lipstick, primer):
|
||||
assert diagnostics.hard_veto is True
|
||||
assert diagnostics.comparison_mode == "not_comparable"
|
||||
assert diagnostics.score < 0.76
|
||||
assert "variant_option_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_accepts_same_explicit_shade_option():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【Maybelline 媚比琳】超持久水光鎖吻唇釉#62 4.2ml",
|
||||
"MAYBELLINE 媚比琳 超持久水光鎖吻唇釉 #62 4.2ml",
|
||||
momo_price=399,
|
||||
competitor_price=399,
|
||||
)
|
||||
|
||||
assert diagnostics.hard_veto is False
|
||||
assert diagnostics.comparison_mode == "exact_identity"
|
||||
assert diagnostics.score >= 0.76
|
||||
assert "variant_option_conflict" not in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_shared_identity_anchor_near_threshold():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
@@ -881,6 +920,18 @@ def test_marketplace_search_terms_prefer_exact_identity_for_nail_foam_and_foot_m
|
||||
assert "枚入" not in " ".join(kameria_terms[:3])
|
||||
|
||||
|
||||
def test_marketplace_search_terms_preserve_decimal_spec_and_shade_option():
|
||||
from services.marketplace_product_matcher import build_search_terms
|
||||
|
||||
terms = build_search_terms(
|
||||
"【Maybelline 媚比琳】超持久水光鎖吻唇釉#62 4.2ml",
|
||||
max_terms=5,
|
||||
)
|
||||
|
||||
assert terms[0] == "媚比琳 超持久水光鎖吻唇釉 62 4.2ml"
|
||||
assert "4 2ml" not in " ".join(terms)
|
||||
|
||||
|
||||
def test_marketplace_search_terms_prefer_specific_line_over_generic_usage_words():
|
||||
from services.marketplace_product_matcher import build_search_terms
|
||||
|
||||
|
||||
Reference in New Issue
Block a user