from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def test_frontend_v2_shell_assets_exist_and_are_indexed(): assert (ROOT / "web/static/css/ewoooc-tokens.css").exists() assert (ROOT / "web/static/css/ewoooc-shell.css").exists() assert (ROOT / "templates/ewoooc_base.html").exists() assert (ROOT / "templates/components/_ewoooc_shell.html").exists() agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") constitution = (ROOT / "CONSTITUTION.md").read_text(encoding="utf-8") roadmap = (ROOT / "docs/guides/frontend_upgrade_roadmap.md").read_text(encoding="utf-8") assert "docs/guides/frontend_upgrade_roadmap.md" in agents assert "前端 V2 視覺基準" in constitution assert "禁止用 mock data" in roadmap def test_frontend_v2_shell_uses_real_runtime_context(): shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8") base = (ROOT / "templates/ewoooc_base.html").read_text(encoding="utf-8") app_source = (ROOT / "app.py").read_text(encoding="utf-8") config_source = (ROOT / "config.py").read_text(encoding="utf-8") assert "scheduler_stats" in shell assert "session.get('username')" in shell assert "next_run|default(None)" in shell assert "components/_ewoooc_shell.html" in base assert "'webcrumbs_config': {" in app_source assert "WEBCRUMBS_RUNTIME_URL" in config_source assert "/webcrumbs-assets/loader/webcrumbs-compatible-loader.js" in config_source assert "WEBCRUMBS_ASSET_UPSTREAM_URL" in config_source assert "data-webcrumbs-runtime" in base assert 'name="ui" value="v2"' not in base forbidden_markers = [ "mockProducts", "mockData", "fake", "假商品", "假 KPI", ] combined = shell + "\n" + base assert all(marker not in combined for marker in forbidden_markers) def test_frontend_v2_syncs_latest_momo_pro_prototype_tokens_and_shell(): tokens = (ROOT / "web/static/css/ewoooc-tokens.css").read_text(encoding="utf-8") shell = (ROOT / "web/static/css/ewoooc-shell.css").read_text(encoding="utf-8") assert "MOMO Pro × Nothing × Claude" in tokens assert "--momo-warm-caramel" in tokens assert "--momo-tag-honey-bg" in tokens assert "--momo-text-body" in tokens assert "--momo-accent-strong" in tokens assert "--momo-border-strong" in tokens assert "--momo-sidebar-width" in tokens assert ".momo-app button:not(.btn):not(.btn-close)" in tokens assert "background: #1f1a14;" in shell assert "rgba(250, 247, 240, 0.08)" in shell assert "var(--momo-text-inverse)" in shell def test_legacy_navbar_uses_warm_token_accent_aliases(): navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8") assert "--momo-nav-accent: var(--momo-page-accent, #c96442)" in navbar assert "--momo-nav-accent-dark: var(--momo-page-accent-dark, #8f4530)" in navbar assert "--momo-nav-accent-soft: var(--momo-page-accent-soft, rgba(201, 100, 66, 0.12))" in navbar forbidden_fragments = [ "--momo-legacy-accent", "#d96f52", "#a95846", "#9f4f3e", ] assert all(fragment not in navbar for fragment in forbidden_fragments) def test_campaign_v2_uses_latest_warm_hero_without_fake_data(): template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8") style = (ROOT / "web/static/css/page-edm-v2.css").read_text(encoding="utf-8") combined = template + "\n" + style assert "css/page-edm-v2.css" in template assert "--campaign-accent: var(--momo-warm-caramel)" in style assert "--campaign-accent: var(--momo-warm-honey)" in style assert "--campaign-accent: var(--momo-warm-rust)" in style assert "background: var(--momo-bg-paper)" in style assert "radial-gradient(circle, rgba(42, 37, 32, 0.12) 1px, transparent 1px)" in style assert ".campaign-hero::after" in style assert "font-family: var(--momo-font-display)" in style assert "linear-gradient(160deg" not in combined assert "#7c3aed" not in combined assert "#0891b2" not in combined assert "mock" not in combined.lower() assert "假商品" not in combined def test_campaign_v2_product_table_keeps_real_operations_columns(): template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8") page_js = (ROOT / "web/static/js/page-edm-v2.js").read_text(encoding="utf-8") assert "分類 / 狀態" in template assert "銷售 / 庫存" in template assert "追蹤資訊" in template assert "data-campaign-copy=\"{{ item.i_code }}\"" in template assert "js/page-edm-v2.js" in template assert "copyCampaignProductId" in page_js assert "campaign-change-pct" in template assert "item.total_sold" in template assert "item.days_on_shelf" in template assert "item.qty_history" in template assert "當日銷售歷程" in template assert "item.crawled_at.strftime('%m/%d %H:%M')" in template assert "data-bs-toggle=\"tooltip\"" in template assert "item.total_sold = total_sold_map.get(item.i_code, 0)" in route_source assert "item.days_on_shelf = days_on_shelf_map.get(item.i_code, 1)" in route_source assert "item.qty_history = history_map.get((item.i_code, item.time_slot), [])" in route_source def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") assert "template_name = 'dashboard_v2.html'" in route_source assert "template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'" not in route_source assert "get_full_dashboard_data()" in route_source assert "_load_shared_full_dashboard_cache(now)" in route_source assert "_load_stale_full_dashboard_cache(now)" in route_source assert "_write_shared_full_dashboard_cache(full_data)" in route_source assert "warm_full_dashboard_cache" in route_source assert "force_rebuild=False" in route_source assert "def _load_competitor_decision_overview(session, latest_items=None)" in route_source assert "fetch_competitor_review_queue" in route_source assert "fetch_competitor_review_queue_page" in route_source assert "_load_competitor_review_page(" in route_source assert "def _render_pchome_review_dashboard(" in route_source assert "return _render_pchome_review_dashboard(" in route_source assert "_build_review_dashboard_items(session, review_queue, today_start_db)" in route_source assert "_load_cached_competitor_overview_for_review(" in route_source assert "_load_competitor_decision_overview(session)" not in route_source assert "review_status != 'all'" in route_source assert "or bool(search_query)" in route_source assert "or bool(_normalize_dashboard_category_filter(category_filter))" in route_source assert "count_total=count_total" in route_source assert "review_total_is_estimated = True" in route_source assert "review_total_is_estimated=review_total_is_estimated" in route_source assert "只替 PChome 覆核當頁建立商品列" in route_source assert "_load_competitor_decision_overview(session, unique_items)" in route_source assert "item_map = {}" in route_source assert "competitor_map = _load_pchome_competitor_map(session, item_map.keys())" in route_source assert "ai_price_recommendations" in route_source assert "pending_match_count" in route_source assert "stale_match_count" in route_source assert "review_queue_count" in route_source assert "unit_comparable_count" in route_source assert "decision_support_rate" in route_source assert "catalog_comparable_count" in route_source assert "'catalog_comparable'" in route_source assert "型錄可比" in route_source assert "'catalog_variant_review'" in route_source assert "選項待核" in route_source assert "'catalog_unit_review'" in route_source assert "入數待核" in route_source assert "'catalog_identity_review'" in route_source assert "身份待核" in route_source assert "rescore_accepted_count" in route_source assert "filter_type == 'pchome_review'" in route_source assert "total_items = review_queue_total" in route_source assert "REVIEW_STATUS_OPTIONS" in route_source assert "current_review_status" in route_source assert "review_status = 'legacy_low_score'" in route_source assert "@dashboard_bp.route('/api/pchome-review/queue')" in route_source assert "def get_pchome_review_queue_api" in route_source assert "'total_is_estimated': total < 0" in route_source assert "review_status != 'all'" in route_source assert "@dashboard_bp.route('/api/pchome-review//decision', methods=['POST'])" in route_source assert "record_competitor_match_review" in route_source assert "clear_competitor_intel_cache()" in route_source assert "_extract_match_diagnostic_reasons" in route_source assert "妝效質地不同" in route_source assert "多款任選待確認" in route_source assert "MockRecord" not in route_source assert "{% for item in items %}" in dashboard assert "比價監控總覽" in dashboard assert "決策支援覆蓋率" in dashboard assert "overview.decision_support_rate" in dashboard assert "overview.catalog_comparable_count" in dashboard assert "比價決策焦點" in dashboard assert "overview.match_rate" in dashboard assert "overview.stale_match_count" in dashboard assert "待刷新 {{ overview.stale_match_count" in dashboard assert "overview.top_picks" in dashboard assert "overview.top_momo_threats" in dashboard assert "overview.pending_priority" in dashboard assert "overview.review_queue" in dashboard assert "需單位價 {{ overview.unit_comparable_count" in dashboard assert "重算待覆核 {{ overview.rescore_accepted_count" in dashboard assert "{% if review_total_is_estimated %}約 {% endif %}" in dashboard assert "filter='ai_picks'" in dashboard assert "filter='pchome_review'" in dashboard assert "review_status=option.key" in dashboard assert "需單位價" in dashboard assert "近門檻可救" in route_source assert "證據不足" in route_source assert "低信心舊候選" in route_source assert "'legacy_low_score'" in route_source assert "dashboard-review-segments" in dashboard assert "data-pchome-review-action" in dashboard assert "採用同款" in dashboard assert "否決候選" in dashboard assert "標記單位價" in dashboard assert "補搜尋" in dashboard assert "覆核建議:" in dashboard assert "review.catalog_review_guidance.action_hint" in dashboard assert 'data-review-action="needs_research"' in dashboard assert "人工閉環" in route_source assert "AI 挑品清單" in dashboard assert "比價覆核隊列" in dashboard assert "覆核動作" in dashboard assert "dashboard-ai-summary-grid" in dashboard assert "AI 建議" in dashboard assert "/api/export/excel/ai-picks" in dashboard assert "匯出 AI 挑品" in dashboard assert "item.ai_pick.reason" in dashboard assert "_summarize_ai_pick_selection(ai_pick_map)" in route_source assert "{{ ai_pick_list_limit }} 品" in dashboard assert "_load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT)" in route_source assert "PRODUCT_PICK_LIST_LIMIT = 50" in route_source assert "ui='v2'" not in dashboard assert 'name="ui" value="v2"' not in dashboard assert "mockProducts" not in dashboard assert "假商品" not in dashboard def test_ai_pick_export_uses_real_recommendation_data(): export_source = (ROOT / "routes/export_routes.py").read_text(encoding="utf-8") assert "@export_bp.route('/api/export/excel/ai-picks')" in export_source assert "ai_price_recommendations ar" in export_source assert "competitor_prices cp" in export_source assert "LEFT JOIN products p ON p.i_code = ar.sku" in export_source assert "ROW_NUMBER() OVER" in export_source assert "LIMIT 50" in export_source assert "MOMO商品ID" in export_source assert "PChome商品ID" in export_source assert "AI建議理由" in export_source assert "pd.ExcelWriter" in export_source def test_pchome_review_export_and_diagnostics_use_real_queue_data(): export_source = (ROOT / "routes/export_routes.py").read_text(encoding="utf-8") route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") repository_source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") dashboard_css = (ROOT / "web/static/css/page-dashboard-v2.css").read_text(encoding="utf-8") assert "@export_bp.route('/api/export/excel/pchome-review')" in export_source assert "fetch_competitor_review_queue_page" in export_source assert "診斷原因" in export_source assert "_flatten_review_decision_envelope" in export_source assert "決策信封ID" in export_source assert "自動執行允許" in export_source assert "決策證據摘要" in export_source assert "原始診斷" in export_source assert "PChome比價覆核_" in export_source assert "MATCH_DIAGNOSTIC_REASON_LABELS" in repository_source assert "diagnostic_reasons" in repository_source assert "商品線不符" in repository_source assert "容量差異" in repository_source assert "妝效質地不同" in repository_source assert "工具功能不同" in repository_source assert "多款任選待確認" in repository_source assert "妝效質地不同" in route_source assert "_extract_match_diagnostic_reasons" in route_source assert "匯出覆核" in dashboard assert "review.decision_envelope" in dashboard assert "dashboard-review-envelope" in dashboard assert "review.diagnostic_reasons" in dashboard assert "item.pchome_match_attempt.diagnostic_reasons" in dashboard assert "dashboard-review-reasons" in dashboard assert "dashboard-review-actions" in dashboard assert ".dashboard-review-reasons" in dashboard_css assert ".dashboard-review-envelope" in dashboard_css assert ".dashboard-review-actions" in dashboard_css assert ".dashboard-review-action.is-research" in dashboard_css def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis(): template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8") assert "{% extends 'ewoooc_base.html' %}" in template assert "{% block ewooo_content %}" in template assert "ai-intel-hero" in template assert "ai-status-badge" in template assert "PChome 競品比價監控" in template assert "AI 決策日誌" in template assert "fetch('/api/ai/icaim/dashboard')" in template assert "fetch('/api/ai/product-picks/generate'" in template assert "fetch('/api/ai/pchome-match/backfill'" in template assert "JSON.stringify({ limit: 50 })" in template assert "mock" not in template.lower() assert "假商品" not in template assert "@ai_bp.route('/ai_intelligence')" in route_source assert "render_template('ai_intelligence.html', active_page='ai_intelligence')" in route_source assert "@ai_bp.route('/api/ai/icaim/dashboard')" in route_source assert "competitor_prices" in route_source assert "ai_price_recommendations" in route_source def test_ai_history_uses_v2_shell_and_real_history_apis(): template = (ROOT / "templates/ai_history.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8") assert "{% extends 'ewoooc_base.html' %}" in template assert "{% block ewooo_content %}" in template assert "{% block extra_css %}" in template assert "ai-history-hero" in template assert "ai-history-panel" in template assert "fetch('/api/ai/statistics?days=30')" in template assert "fetch(`/api/ai/history?${params}`)" in template assert "fetch(`/api/ai/history/${id}`" in template assert "fetch('/api/ai/history/batch'" in template assert "mock" not in template.lower() assert "假商品" not in template assert "@ai_bp.route('/ai_history')" in route_source assert "render_template('ai_history.html', active_page='ai_history')" in route_source assert "@ai_bp.route('/api/ai/history')" in route_source assert "ai_history_service.get_history_list" in route_source assert "ai_history_service.get_statistics" in route_source def test_ai_recommend_uses_v2_shell_and_runtime_category_data(): template = (ROOT / "templates/ai_recommend.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8") assert "{% extends 'ewoooc_base.html' %}" in template assert "{% block ewooo_content %}" in template assert "{% block extra_css %}" in template assert "ai-recommend-hero" in template assert "ai-recommend-page" in template assert "{% for category in product_categories[:4] %}" in template assert "quickWebSearch({{ category|tojson }})" in template assert "quickWebSearch('保濕面膜')" not in template assert "fetch('/api/ai/generate_copy'" in template assert "fetch('/api/ai/web_search'" in template assert "fetch('/api/ai/product_insights'" in template assert "fetch('/api/ai/gemini_usage?days=30')" in template assert "mock" not in template.lower() assert "假商品" not in template assert "@ai_bp.route('/ai_recommend')" in route_source assert "render_template('ai_recommend.html'" in route_source assert "active_page='ai_recommend'" in route_source assert "product_categories=product_categories" in route_source assert "@ai_bp.route('/api/ai/generate_copy'" in route_source assert "@ai_bp.route('/api/ai/web_search'" in route_source assert "@ai_bp.route('/api/ai/product_insights'" in route_source def test_monthly_summary_analysis_uses_v2_shell_and_real_monthly_api(): template = (ROOT / "templates/monthly_summary_analysis.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/monthly_routes.py").read_text(encoding="utf-8") script = (ROOT / "web/static/js/page-monthly-summary.js").read_text(encoding="utf-8") assert "{% extends 'ewoooc_base.html' %}" in template assert "{% block ewooo_content %}" in template assert "{% block extra_css %}" in template assert "{% block extra_js %}" in template assert "components/_navbar.html" not in template assert "" not in template assert "monthly-analysis-hero" in template assert "monthly-analysis-page" in template assert "/api/monthly_summary_data" in template assert "/api/monthly_summary_trend" in route_source assert "/api/monthly_summary_trend" in script assert "area_name=${encodeURIComponent(area)}&limit=1" not in script assert "monthly_summary_analysis" in route_source assert "active_page='monthly'" in route_source assert "MonthlySummaryAnalysis" in route_source assert "mock" not in template.lower() assert "假商品" not in template def test_dashboard_v2_restores_real_price_history_chart(): route_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") page_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8") assert "@api_bp.route('/api/history/')" in route_source assert "PRICE_HISTORY_RANGES" in route_source assert "'week': {'days': 7" in route_source assert "'month': {'days': 30" in route_source assert "'quarter': {'days': 90" in route_source assert "'year': {'days': 365" in route_source assert "session.query(PriceRecord)" in route_source assert "PriceRecord.product_id == product.id" in route_source assert "js/page-dashboard-v2.js" in dashboard assert "https://cdn.jsdelivr.net/npm/chart.js" in page_js assert 'id="historyModal"' in dashboard assert 'id="priceChart"' in dashboard assert "data-product-id=\"{{ product.id }}\"" in dashboard assert "data-history-trigger" in dashboard assert "document.querySelectorAll('[data-history-trigger]')" in page_js assert "event.stopPropagation();" in page_js assert "showHistory(button.dataset.productId, button.dataset.productName);" in page_js assert "data-history-range=\"week\"" in dashboard assert "data-history-range=\"month\"" in dashboard assert "data-history-range=\"quarter\"" in dashboard assert "data-history-range=\"year\"" in dashboard assert "fetch(`/api/history/${productId}?range=${activeHistoryRange}&format=v2`)" in page_js assert "priceChartInstance = new Chart" in page_js assert "目前沒有可顯示的歷史價格紀錄" in page_js def test_dashboard_v2_shows_pchome_competitor_pricing_and_links(): route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") api_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8") feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8") scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") page_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8") migration = (ROOT / "migrations/022_competitor_price_history.sql").read_text(encoding="utf-8") assert "_load_pchome_competitor_map(" in route_source assert "FROM competitor_prices" in route_source assert "competitor_product_id" in route_source assert "https://24h.pchome.com.tw/prod/" in route_source assert "_build_competitor_decision(" in route_source assert "PChome 價格壓力" in route_source assert "MOMO 價格優勢" in route_source assert "item['pchome_competitor']" in route_source assert "item['competitor_decision']" in route_source assert "MOMO 價格" in dashboard assert "PChome 價格" in dashboard assert "競價判讀" in dashboard assert "MOMO {{ product.i_code }}" in dashboard assert "PChome {{ competitor.product_id }}" in dashboard assert "competitor.product_url" in dashboard assert "dashboard-competition-badge" in dashboard assert "decision.summary" in dashboard assert "PChome {{ match_status.label" in dashboard assert "候選:{{ item.pchome_match_attempt.best_competitor_product_name }}" in dashboard assert "候選價,需單位換算" in dashboard assert "尚未進入 PChome 補抓" in dashboard assert '待比對' not in dashboard assert "PChome 比價補強產線" in dashboard assert "待比對補抓產線" not in dashboard assert "_load_pchome_match_attempt_map" in route_source assert "低信心待補強" in route_source assert "未找到可信同款" in route_source assert "規格不符已排除" in route_source assert "dashboard-review-reasons" in dashboard assert "series.pchome" in page_js assert "label: 'PChome'" in page_js assert "含 PChome 歷史快照" in page_js assert "competitor_price_history" in migration assert "momo_price" in migration assert "competitor_product_id" in migration assert "CREATE INDEX IF NOT EXISTS idx_comp_price_history_sku_source_time" in migration assert "_ensure_competitor_price_history_table" in feeder_source assert "INSERT INTO competitor_price_history" in feeder_source assert "history_written" in feeder_source assert "momo_price" in feeder_source assert "competitor_price_history" in api_source assert "'series': {" in api_source assert "'pchome': competitor_data" in api_source assert "history_written" in scheduler_source def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8") agent_source = (ROOT / "services/ai_product_pick_agent.py").read_text(encoding="utf-8") route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8") scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8") template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8") assert "MIN_MATCH_SCORE = 0.76" in feeder_source assert "REPLACE_DIFFERENT_PRODUCT_SCORE" in feeder_source assert "marketplace_product_matcher" in feeder_source assert "MAX_SEARCH_TERMS" in feeder_source assert "_build_search_keywords" in feeder_source assert "_search_pchome_candidates" in feeder_source assert "search_limit = SEARCH_LIMIT * max(1, SEARCH_MAX_PAGES)" in feeder_source assert "crawler.search_products(keyword, limit=search_limit, max_pages=SEARCH_MAX_PAGES)" in feeder_source assert "_fetch_unmatched_priority_skus" in feeder_source assert "_fetch_expired_identity_skus" in feeder_source assert "run_expired_identity_refresh" in feeder_source assert "fetch_product_details(requested_ids" in feeder_source assert "run_unmatched_priority" in feeder_source assert "PCHOME_BACKFILL_EXPIRED_REFRESH_LIMIT" in scheduler_source assert "run_expired_identity_refresh(limit=expired_refresh_limit)" in scheduler_source assert "generate_product_pick_list" in agent_source assert "clear_dashboard_cache()" in route_source assert "get_full_dashboard_data()" in route_source assert "dashboard_cache_warmed" in route_source assert "competitor_prices" in agent_source assert "competitor_price_history" in agent_source assert "daily_sales_snapshot" in agent_source assert "ai_price_recommendations" in agent_source assert "'product_pick'" in agent_source assert "PChomeProductPickAgent" in agent_source assert "PChome 價格優勢" in agent_source assert "_daily_sales_columns" in agent_source assert '"總業績"' in agent_source assert '"毛利"' in agent_source assert '"總成本"' in agent_source assert "evidence_quality" in agent_source assert "opportunity_score" in agent_source assert "margin_rate" in agent_source assert "missing_evidence" in agent_source assert "confidence_band" in agent_source assert "_supersede_old_picks" in agent_source assert "status = 'superseded'" in agent_source assert "{date_col}::date" in agent_source assert "conn.rollback()" in agent_source assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source assert "generate_product_pick_list(engine" in route_source assert "@ai_bp.route('/api/ai/pchome-match/backfill', methods=['POST'])" in route_source assert "@ai_bp.route('/api/ai/pchome-match/refresh-stale', methods=['POST'])" in route_source assert "@ai_bp.route('/api/ai/pchome-match/recover-stale', methods=['POST'])" in route_source assert 'PCHOME_STALE_RECOVERY_ENABLED' in route_source assert "@ai_bp.route('/api/ai/pchome-match/backfill/status', methods=['GET'])" in route_source assert "_build_pchome_backfill_coverage_payload" in route_source assert "_build_pchome_revalidation_preview_payload" in route_source assert "_build_pchome_stale_recovery_preview_payload" in route_source assert "fetch_competitor_coverage" in route_source assert "'catalog_variant_review_count': int(coverage.get('catalog_variant_review_count') or 0)" in route_source assert "'catalog_unit_review_count': int(coverage.get('catalog_unit_review_count') or 0)" in route_source assert "'catalog_identity_review_count': int(coverage.get('catalog_identity_review_count') or 0)" in route_source assert "'catalog_review_plan': coverage.get('catalog_review_plan') or {}" in route_source assert "preview_retryable_candidate_revalidation" in route_source assert "preview_expired_identity_recovery" in route_source assert "revalidation_preview" in route_source assert "stale_recovery_preview" in route_source assert "_build_pchome_operation_backlog" in route_source assert "'catalog_variant_review': {" in route_source assert "'catalog_unit_review': {" in route_source assert "'catalog_identity_review': {" in route_source assert "_pick_pchome_recommended_next_action" in route_source assert "'operation_backlog': operation_backlog" in route_source assert "'recommended_next_action': _pick_pchome_recommended_next_action(operation_backlog)" in route_source assert "status['coverage'] = _build_pchome_backfill_coverage_payload()" in route_source assert "run_unmatched_priority(limit=unmatched_limit)" in route_source assert "stale_refresh_limit = max(5, min(40, max(5, limit // 3)))" in route_source assert "stale_refresh_result = feeder.run_expired_identity_refresh(limit=stale_refresh_limit)" in route_source assert "stale_identity_refresh" in route_source assert "run_expired_identity_refresh(limit=limit)" in route_source assert "run_expired_identity_search_recovery(limit=limit)" in route_source assert "stage='refreshing_stale'" in route_source assert "stage='recovering_stale'" in route_source assert "run_retryable_candidate_revalidation" in route_source assert "generate_product_pick_list(engine, limit=50)" in route_source assert "start_pchome_backfill_run" in route_source assert "finish_pchome_backfill_run" in route_source assert "payload.get('limit', 50)" in route_source assert "JSON.stringify({ limit: 50 })" in template assert "完成後會重算 AI 挑品清單" in route_source assert "match_rate" in route_source assert "decision_ready_rate" in route_source assert "product_pick_count" in route_source dashboard_route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") dashboard_template = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") assert "model_footprint" in dashboard_route_source assert "_ai_pick_evidence_fields" in dashboard_route_source assert "avg_evidence_quality" in dashboard_route_source assert "top_missing_evidence" in dashboard_route_source assert "證據 {{ item.ai_pick.evidence_quality" in dashboard_template assert "dashboard-ai-evidence-chip" in dashboard_template assert "EVIDENCE GAP" in dashboard_template scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8") run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8") agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8") assert "def run_pchome_match_backfill_task" in scheduler_source assert "_save_stats('pchome_match_backfill'" in scheduler_source assert "retryable_candidate_revalidation_total" in scheduler_source assert "run_pchome_match_backfill_task" in run_scheduler_source assert "每日 10:30:pchome_match_backfill" in run_scheduler_source assert '"run_pchome_match_backfill_task"' in agent_actions_source assert "產生挑品清單" in template assert "補抓未搜尋" in template assert "generatePickList" in template assert "backfillPchomeMatches" in template assert "/api/ai/product-picks/generate" in template assert "/api/ai/pchome-match/backfill" in template assert "/api/ai/pchome-match/refresh-stale" in dashboard_template assert "/api/ai/pchome-match/recover-stale" not in dashboard_template assert "/api/ai/pchome-match/backfill/status" in dashboard_template assert "PCHOME MATCH BACKFILL" in dashboard_template assert "data-pchome-backfill-trigger" in dashboard_template assert "data-pchome-refresh-stale-trigger" in dashboard_template assert "data-pchome-recover-stale-trigger" not in dashboard_template assert "刷新過期 120 筆" in dashboard_template assert "救援過期 40 筆" not in dashboard_template dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8") assert "loadPchomeBackfillStatus" in dashboard_js assert "window.backfillPchomeMatches" in dashboard_js assert "window.refreshStalePchomeMatches" in dashboard_js assert "window.recoverStalePchomeMatches" not in dashboard_js assert "formatBackfillCoverageSummary" in dashboard_js assert "formatBackfillStageSummary" in dashboard_js assert "補強 60 筆" in dashboard_js assert "啟動 PChome 比價補強" in dashboard_js assert "刷新', result.stale_identity_refresh" in dashboard_js assert "formatBackfillLimitedCount" in dashboard_js assert "status.coverage" in dashboard_js assert "coverage.recommended_next_action" in dashboard_js assert "建議 ${recommended.label}" in dashboard_js assert "決策支援 ${formatBackfillRate(coverage.decision_support_rate || coverage.decision_ready_rate)}" in dashboard_js assert "精準可用 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js assert "型錄可比 ${formatBackfillCount(coverage.catalog_comparable_count)}" in dashboard_js assert "待刷新 ${formatBackfillCount(coverage.stale_matches)}" in dashboard_js assert "待補抓 ${formatBackfillCount(coverage.pending)}" in dashboard_js assert "可重評 ${formatBackfillLimitedCount(preview.candidate_count" in dashboard_js assert "可救援 ${formatBackfillLimitedCount(staleRecovery.candidate_count" in dashboard_js assert "'product_pick':['bg-success'" in template assert "kpiMatchRate" in template def test_edm_dashboard_v2_is_production_default_and_uses_real_campaign_data(): route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8") template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8") page_js = (ROOT / "web/static/js/page-edm-v2.js").read_text(encoding="utf-8") assert "request.args.get('ui') == 'legacy'" not in route_source assert route_source.count("template_name = 'edm_dashboard_v2.html'") == 5 assert "{% for slot, stats in slot_stats.items() %}" in template assert "{% for item in items %}" in template assert "scheduler_stats.get(task_key, [])" in template assert "@api_bp.route('/api/history/i-code/')" in (ROOT / "routes/api_routes.py").read_text(encoding="utf-8") assert "data-campaign-filter=\"new\"" in template assert "data-campaign-filter=\"up\"" in template assert "data-campaign-filter=\"down\"" in template assert "data-campaign-filter=\"delisted\"" in template assert "data-campaign-history-trigger" in template assert "showCampaignHistory(button.dataset.iCode, button.dataset.productName)" in page_js assert "fetch(`/api/history/i-code/${encodeURIComponent(iCode)}?range=${activeCampaignHistoryRange}`)" in page_js assert "data-campaign-history-range=\"week\"" in template assert "data-campaign-history-range=\"month\"" in template assert "data-campaign-history-range=\"quarter\"" in template assert "data-campaign-history-range=\"year\"" in template assert "applyCampaignFilter(card, button.dataset.campaignFilter)" in page_js assert "?ui=v2" not in template assert "ui='v2'" not in template assert "mock" not in template.lower() assert "假商品" not in template def test_vendor_stockout_v2_is_production_default_and_uses_real_vendor_data(): route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8") template = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8") assert "vendor_stockout_index_v2.html" in route_source assert "request.args.get('ui') == 'legacy'" not in route_source assert not (ROOT / "web/templates/vendor_stockout_index_v2.html").exists() assert "get_vendor_dashboard_stats(vendor_db)" in route_source assert "def get_vendor_dashboard_stats(vendor_db)" in service_source assert "session.query(VendorStockout).count()" in service_source assert "EmailSendLog" in service_source assert "stats.pending_stockouts" in template assert "stats.email_success_rate" in template assert "ui='v2'" not in template assert "mock" not in template.lower() assert "假" not in template def test_vendor_stockout_list_v2_is_production_default_and_uses_real_stockout_rows(): route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8") template = (ROOT / "templates/vendor_stockout_list_v2.html").read_text(encoding="utf-8") assert "vendor_stockout_list_v2.html" in route_source assert "get_vendor_stockout_list_context(" in route_source assert "request.args.get('page', 1, type=int)" in route_source assert "def get_vendor_stockout_list_context(" in service_source assert "_apply_stockout_filters(" in service_source assert "session.query(VendorStockout)" in service_source assert "VendorStockout.status == 'pending'" in service_source assert "VendorStockout.batch_id == batch_id" in service_source assert "from flask" not in service_source assert "{% for record in records %}" in template assert "{% for batch in batches %}" in template assert "stats.pending" in template assert "record.product_code" in template assert "record.product_name" in template assert "record.vendor_name" in template assert "ui='v2'" not in template assert "mock" not in template.lower() assert "假" not in template def test_vendor_stockout_api_queries_are_extracted_to_service(): route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8") api_section = route_source.split("def api_get_stockout_list():", 1)[1].split("@vendor_bp.route('/api/stockout/'", 1)[0] assert "get_stockout_api_list_payload(" in api_section assert "get_stockout_batches_payload(vendor_db)" in api_section assert "session.query(VendorStockout)" not in api_section assert "def get_stockout_api_list_payload(" in service_source assert "def get_stockout_batches_payload(vendor_db)" in service_source assert "'batch_number': record.batch_id" in service_source assert "'send_status': record.status or 'pending'" in service_source assert "'latest_date': batch.latest_date.isoformat()" in service_source assert "from flask" not in service_source def test_vendor_management_queries_are_extracted_to_service(): route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8") api_section = route_source.split("def api_get_vendor_list():", 1)[1].split("@vendor_bp.route('/api/vendor', methods=['POST'])", 1)[0] detail_section = route_source.split("def api_get_vendor(vendor_code):", 1)[1].split("@vendor_bp.route('/api/vendor/', methods=['PUT'])", 1)[0] assert "get_vendor_list_payload(" in api_section assert "get_vendor_detail_payload(vendor_db, vendor_code)" in detail_section assert "session.query(VendorList)" not in api_section assert "session.query(VendorEmail)" not in detail_section assert "def get_vendor_list_payload(" in service_source assert "def get_vendor_detail_payload(vendor_db, vendor_code)" in service_source assert "'vendor_code': vendor.vendor_code" in service_source assert "'email_count': len(emails)" in service_source assert "from flask" not in service_source def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_rows(): route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") template = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8") template_function = route_source.split("def api_import_template():", 1)[1].split("@vendor_bp.route('/api/stockout/list'", 1)[0] assert "vendor_stockout_import_v2.html" in route_source assert "template_columns = [" in template_function assert "pd.DataFrame(columns=template_columns)" in template_function assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template assert "vendor_stockout" in template assert "範例" not in template_function assert "ui='v2'" not in template assert "mock" not in template.lower() assert "假" not in template