Files
ewoooc/tests/test_frontend_v2_assets.py
OoO bb8c29e56d
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
V10.590 修正 PChome 副標去重與比價覆核入口
2026-06-04 22:00:32 +08:00

765 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
dashboard_css = (ROOT / "web/static/css/page-dashboard-v2.css").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/<sku>/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 "overview.catalog_identity_review_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 "review_status='catalog_identity_review'" in dashboard
assert "身份採用待核" in dashboard
assert "grid-template-columns: repeat(5, minmax(0, 1fr))" in dashboard_css
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 "<!DOCTYPE html>" 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/<int:product_id>')" 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 '<span class="dashboard-focus-chip is-neutral">待比對</span>' 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 "page_cap = max(1, int(max_pages or SEARCH_MAX_PAGES))" in feeder_source
assert "search_limit = SEARCH_LIMIT * page_cap" in feeder_source
assert "bounded_search=True" in feeder_source
assert "crawler.search_products(keyword, limit=search_limit, max_pages=page_cap)" 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:30pchome_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/<path: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/<int:stockout_id>'", 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/<vendor_code>', 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