221 lines
9.4 KiB
Python
221 lines
9.4 KiB
Python
from services import webcrumbs_host_data_service as svc
|
|
|
|
|
|
def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
|
engine = object()
|
|
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"fetch_top_competitor_risks",
|
|
lambda passed_engine, limit: [
|
|
{
|
|
"sku": "SKU-REVIEW",
|
|
"name": "需人工覆核的非直接告警候選",
|
|
"momo_price": 999,
|
|
"pchome_price": 500,
|
|
"gap_pct": 99.8,
|
|
"match_score": 0.95,
|
|
"alert_tier": "identity_review",
|
|
"match_type": "exact",
|
|
"price_basis": "total_price",
|
|
},
|
|
{
|
|
"sku": "SKU-1",
|
|
"name": "Derma Angel 護妍天使 集中抗痘精華",
|
|
"momo_price": 420,
|
|
"pchome_price": 250,
|
|
"gap_pct": 68.0,
|
|
"match_score": 0.91,
|
|
"alert_tier": "price_alert_exact",
|
|
"match_type": "exact",
|
|
"price_basis": "total_price",
|
|
"pchome_id": "DABC123",
|
|
"pchome_name": "Derma Angel 集中抗痘精華",
|
|
"crawled_at": "05/31 20:50",
|
|
}
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"fetch_competitor_coverage",
|
|
lambda passed_engine: {
|
|
"valid_matches": 88,
|
|
"match_rate": 12.3,
|
|
"fresh_matches": 70,
|
|
"fresh_match_rate": 79.5,
|
|
"decision_ready_matches": 70,
|
|
"decision_ready_rate": 9.8,
|
|
"decision_support_count": 105,
|
|
"decision_support_rate": 14.7,
|
|
"decision_support_non_exact_count": 35,
|
|
"catalog_comparable_count": 12,
|
|
"catalog_comparable_rate": 1.7,
|
|
"catalog_variant_review_count": 7,
|
|
"catalog_unit_review_count": 3,
|
|
"catalog_identity_review_count": 2,
|
|
"catalog_review_plan": {
|
|
"variant_review": 7,
|
|
"unit_review": 3,
|
|
"identity_review": 2,
|
|
"total": 12,
|
|
},
|
|
"unit_comparable_count": 23,
|
|
"stale_matches": 18,
|
|
"pending": 612,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"fetch_competitor_review_queue",
|
|
lambda passed_engine, limit: [
|
|
{
|
|
"sku": "SKU-REVIEW",
|
|
"name": "人工覆核精華液",
|
|
"decision_envelope": {
|
|
"decision_id": "review_queue:SKU-REVIEW",
|
|
"severity": "P2",
|
|
"subject": {
|
|
"sku": "SKU-REVIEW",
|
|
"name": "人工覆核精華液",
|
|
"momo_price": 399,
|
|
"competitor_price": 329,
|
|
"competitor_product_id": "PCH-REVIEW",
|
|
},
|
|
"recommended_action": {
|
|
"action": "review_accept_identity",
|
|
"requires_hitl": True,
|
|
},
|
|
"expected_impact": {"candidate_gap_pct": 21.3},
|
|
"guardrails": {
|
|
"data_quality": "complete",
|
|
"can_auto_execute": False,
|
|
},
|
|
"evidence": [
|
|
{
|
|
"metric": "match_score",
|
|
"value": 0.91,
|
|
"basis": "exact/total_price/identity_review",
|
|
}
|
|
],
|
|
},
|
|
}
|
|
],
|
|
)
|
|
|
|
payload = svc.build_webcrumbs_marketplace_host_data(engine=engine, limit=5)
|
|
|
|
assert payload["marketSnapshot"][0]["name"].startswith("SKU-1")
|
|
assert payload["marketSnapshot"][0]["price"] == 250
|
|
assert payload["marketSnapshot"][0]["change_pct"] == 68.0
|
|
assert payload["aiCandidate"]["ticker"] == "SKU-1"
|
|
assert payload["aiCandidate"]["confidence_score"] == 0.91
|
|
assert "MOMO NT$420 vs PChome NT$250" in payload["aiCandidate"]["thesis"]
|
|
assert payload["aiCandidate"]["release_status"] == "review_required"
|
|
assert payload["metadata"]["writes_database"] is False
|
|
assert payload["metadata"]["calls_llm"] is False
|
|
assert payload["metadata"]["fetches_external"] is False
|
|
assert payload["metadata"]["matched_count"] == 88
|
|
assert payload["metadata"]["coverage_rate"] == 12.3
|
|
assert payload["metadata"]["identity_coverage_rate"] == 12.3
|
|
assert payload["metadata"]["decision_ready_count"] == 70
|
|
assert payload["metadata"]["decision_ready_rate"] == 9.8
|
|
assert payload["metadata"]["decision_support_count"] == 105
|
|
assert payload["metadata"]["decision_support_rate"] == 14.7
|
|
assert payload["metadata"]["decision_support_non_exact_count"] == 35
|
|
assert payload["metadata"]["catalog_comparable_count"] == 12
|
|
assert payload["metadata"]["catalog_comparable_rate"] == 1.7
|
|
assert payload["metadata"]["catalog_variant_review_count"] == 7
|
|
assert payload["metadata"]["catalog_unit_review_count"] == 3
|
|
assert payload["metadata"]["catalog_identity_review_count"] == 2
|
|
assert payload["metadata"]["catalog_review_plan"]["variant_review"] == 7
|
|
assert payload["metadata"]["catalog_review_plan"]["unit_review"] == 3
|
|
assert payload["metadata"]["catalog_review_plan"]["identity_review"] == 2
|
|
assert payload["metadata"]["unit_comparable_count"] == 23
|
|
assert payload["metadata"]["fresh_match_count"] == 70
|
|
assert payload["metadata"]["fresh_match_rate"] == 79.5
|
|
assert payload["metadata"]["stale_match_count"] == 18
|
|
assert payload["metadata"]["pending_match_count"] == 612
|
|
assert payload["metadata"]["review_queue_count"] == 1
|
|
assert payload["metadata"]["hitl_count"] == 1
|
|
assert payload["metadata"]["auto_execute_blocked_count"] == 1
|
|
assert payload["metadata"]["decision_envelope_source"] == "competitor_intel_repository"
|
|
assert payload["reviewDecisionBrief"]["items"][0]["sku"] == "SKU-REVIEW"
|
|
assert payload["reviewDecisionBrief"]["items"][0]["can_auto_execute"] is False
|
|
assert "review_accept_identity" in payload["reviewDecisionBrief"]["items"][0]["action"]
|
|
assert all(row["freshness_status"] == "price_alert_exact" for row in payload["marketSnapshot"])
|
|
|
|
|
|
def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
|
|
monkeypatch.setattr(svc, "fetch_top_competitor_risks", lambda engine, limit: [])
|
|
monkeypatch.setattr(svc, "fetch_competitor_review_queue", lambda engine, limit: [])
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"fetch_competitor_coverage",
|
|
lambda engine: {
|
|
"valid_matches": 3,
|
|
"fresh_matches": 1,
|
|
"decision_support_count": 4,
|
|
"catalog_comparable_count": 2,
|
|
"catalog_variant_review_count": 1,
|
|
"catalog_unit_review_count": 1,
|
|
"catalog_identity_review_count": 0,
|
|
"catalog_review_plan": {
|
|
"variant_review": 1,
|
|
"unit_review": 1,
|
|
"identity_review": 0,
|
|
"total": 2,
|
|
},
|
|
"unit_comparable_count": 1,
|
|
"stale_matches": 2,
|
|
"pending": 9,
|
|
},
|
|
)
|
|
|
|
payload = svc.build_webcrumbs_marketplace_host_data(engine=object(), limit=5)
|
|
|
|
assert payload["marketSnapshot"][0]["freshness_status"] == "no_current_exact_risk"
|
|
assert payload["aiCandidate"]["release_status"] == "blocked"
|
|
assert payload["metadata"]["matched_count"] == 3
|
|
assert payload["metadata"]["decision_ready_count"] == 1
|
|
assert payload["metadata"]["decision_support_count"] == 4
|
|
assert payload["metadata"]["catalog_comparable_count"] == 2
|
|
assert payload["metadata"]["catalog_variant_review_count"] == 1
|
|
assert payload["metadata"]["catalog_unit_review_count"] == 1
|
|
assert payload["metadata"]["catalog_identity_review_count"] == 0
|
|
assert payload["metadata"]["catalog_review_plan"]["total"] == 2
|
|
assert payload["metadata"]["unit_comparable_count"] == 1
|
|
assert payload["metadata"]["fresh_match_count"] == 1
|
|
assert payload["metadata"]["stale_match_count"] == 2
|
|
assert payload["metadata"]["pending_match_count"] == 9
|
|
assert payload["metadata"]["review_queue_count"] == 0
|
|
assert payload["reviewDecisionBrief"]["text"] == "(目前沒有待覆核決策信封)"
|
|
assert "非同款、單位價或變體候選" in payload["aiCandidate"]["thesis"]
|
|
|
|
|
|
def test_webcrumbs_seed_data_fallback_does_not_expose_demo_values(monkeypatch):
|
|
from services import external_tool_payload_service as payload_service
|
|
|
|
def boom(limit):
|
|
raise RuntimeError("db unavailable")
|
|
|
|
monkeypatch.setattr(payload_service, "build_webcrumbs_marketplace_host_data", boom)
|
|
|
|
payload = payload_service.build_webcrumbs_seed_data(limit=5)
|
|
|
|
assert payload["marketSnapshot"][0]["freshness_status"] == "diagnostic_unavailable"
|
|
assert payload["aiCandidate"]["release_status"] == "blocked"
|
|
assert "fallback demo" in payload["aiCandidate"]["thesis"]
|
|
assert "TAIEX" not in str(payload)
|
|
|
|
|
|
def test_webcrumbs_auth_required_seed_data_is_non_sensitive():
|
|
from services.external_tool_payload_service import build_webcrumbs_auth_required_seed_data
|
|
|
|
payload = build_webcrumbs_auth_required_seed_data()
|
|
|
|
assert payload["marketSnapshot"][0]["freshness_status"] == "auth_required"
|
|
assert payload["aiCandidate"]["release_status"] == "blocked"
|
|
assert payload["metadata"]["source"] == "auth_required"
|
|
assert "MOMO NT$" not in str(payload)
|
|
assert "PChome NT$" not in str(payload)
|