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)