[V10.359] 導入 browse.sh 診斷與色號防錯配

This commit is contained in:
OoO
2026-05-21 11:57:18 +08:00
parent 2efec5fb15
commit d5b41ec11b
15 changed files with 587 additions and 13 deletions

View 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

View File

@@ -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

View File

@@ -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