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") constitution = (ROOT / "CONSTITUTION.md").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 assert "前端文案與工作溝通隔離" in constitution assert "不得搬到使用者可見頁面" in constitution forbidden_markers = [ "mockProducts", "mockData", "fake", "假商品", "假 KPI", ] combined = shell + "\n" + base assert all(marker not in combined for marker in forbidden_markers) def test_high_visibility_pages_use_traditional_chinese_labels(): page_paths = [ "templates/code_review.html", "templates/ai_automation_smoke.html", "templates/admin/ppt_audit_history.html", "templates/admin/host_health.html", "templates/dashboard_v2.html", "templates/components/_navbar.html", "templates/cicd_dashboard.html", ] combined = "\n".join((ROOT / path).read_text(encoding="utf-8") for path in page_paths) assert "AI 程式碼審查" in combined assert "等待程式碼審查完成" in combined assert "AI 自動化健康檢查" in combined assert "四 Agent 控制面" in combined assert "產線健康度" in combined assert "工作隊列" in combined assert "覆蓋率流程" in combined assert "NemoTron · 派遣器" in combined assert "同步部署" in combined assert "部署監控" in combined assert "最新部署流程" in combined assert "執行環境正常" in combined assert "視覺檢查" in combined forbidden_visible_text = [ "AI Code Review", "Smoke Dashboard", "FOUR-AGENT CONTROL PLANE", "WarningCommit", "Branch", "${f.severity}", "ea.priority.toUpperCase", "CI/CD Dashboard", ">最新 Pipeline<", "暫無 Pipeline", "開啟 GitLab Pipelines", ">Runtime 狀態:<", "Docker Compose runtime", "Runtime 正常", ">Runtime<", "Vision QA", ] for marker in forbidden_visible_text: assert marker not in combined def test_topbar_observability_indicator_is_cached_and_timeout_bounded(): base_js = (ROOT / "web/static/js/ewoooc-base.js").read_text(encoding="utf-8") observability_route = (ROOT / "routes/admin_observability_routes.py").read_text(encoding="utf-8") assert "momoObsHealthIndicator:v1" in base_js assert "sessionStorage.getItem(cacheKey)" in base_js assert "sessionStorage.setItem(cacheKey" in base_js assert "const cacheTtlMs = 60000" in base_js assert "new AbortController()" in base_js assert "setTimeout(() => controller.abort(), 2500)" in base_js assert "setInterval(() => refresh(false), 60000)" in base_js assert "_HEALTH_INDICATOR_CACHE_LOCK" in observability_route assert "_HEALTH_INDICATOR_CACHE_TTL_SECONDS = 30" in observability_route assert "return jsonify(dict(cached_payload))" in observability_route def test_market_intel_disabled_page_stays_lightweight_and_action_oriented(): template_path = ROOT / "templates/market_intel/disabled.html" template = template_path.read_text(encoding="utf-8") assert template_path.stat().st_size < 40000 assert "市場情報入口" in template assert "比價覆核" in template assert "PChome 爬蟲" in template assert "AI 觀測台" in template assert "system_version" not in template assert "V10." not in template assert "hotfix" not in template.lower() assert "Gitea" not in template assert "Codex" not in template assert "Claude" not in template assert "Runtime Status" not in template assert "Decision Flow" not in template assert "模組待啟用" not in template assert "停用中的試驗流程" not in template assert "data-market-intel-preview" not in template assert "/api/market_intel/" not in template assert "讀取候選預覽中" not in template 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 "def _load_pchome_growth_command_center(session)" in route_source assert "build_pchome_growth_opportunities(engine, limit=16)" in route_source assert "pchome_growth_command_center=pchome_growth_command_center" 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 "PChome 業績成長作戰台" in dashboard assert "先看業績,再決定調價、曝光與組合" in dashboard assert "growth-command-kpis" in dashboard assert "MOMO 比價後怎麼做" in dashboard assert "高業績商品作戰清單" in dashboard assert "業績 × MOMO 價格 × 下一步" in dashboard assert "growth.mapping_rate" in dashboard assert "growth.top_opportunities" 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 ".growth-command-center" in dashboard_css assert ".growth-strategy-grid" in dashboard_css assert ".growth-opportunity-table" 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") page_guard_css = (ROOT / "web/static/css/ewoooc-v3-page-guard.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 "'detail': attempt.get('error_message')" not in route_source assert "需檢查 matcher diagnostics" not in route_source assert "匯出覆核" in dashboard assert "商品 / MOMO" in dashboard assert "PChome 候選" in dashboard assert "覆核判讀" in dashboard assert "dashboard-review-workbench-row" in dashboard assert "dashboard-review-candidate-title" in dashboard assert "dashboard-review-next-step" in dashboard assert "is-review-wrap" in dashboard assert "6 if current_filter == 'pchome_review'" in dashboard assert "review.review_bucket" not in dashboard assert "HITL" not in dashboard assert "stored_status" not in dashboard assert "stored_score" not in dashboard assert "matcher_rescore" not in dashboard assert 'title="{{ envelope.decision_id }}"' not in dashboard assert 'title="決策追蹤"' in dashboard assert "優先 {{ envelope.severity" in dashboard assert "證據完整" in dashboard 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-workbench-row" in dashboard_css assert ".dashboard-review-candidate-title" in dashboard_css assert ".dashboard-review-next-step" in dashboard_css assert ".dashboard-table.is-review" in dashboard_css assert "min-width: 1540px" in dashboard_css assert ".dashboard-table-wrap.is-review-wrap" in page_guard_css assert "width: max(100%, 1540px) !important" in page_guard_css assert 'content: "商品/MOMO"' in page_guard_css assert 'content: "PChome候選"' in page_guard_css 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 assert "grid-template-columns: repeat(auto-fit, minmax(128px, 1fr))" 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 "MOMO 外部價格參考" in template assert "今日重點總覽" in template assert "今日任務摘要" in template assert "growth-executive-strip" in template assert "growthExecTask" in template assert "renderGrowthExecutiveSummary" in template assert "商品處理進度" in template assert "外部價格來源" in template assert "nextActionTitle" in template assert "renderNextAction" in template assert "今天先做:補齊" in template assert "今天先做:檢查價格風險" in template assert "價格風險分佈" in template assert "備援資料檢查" in template assert "外部報價預檢" not in template assert "鎖定商品" in template assert "data-label=\"PChome\"" in template assert "data-action=\"backfill\"" in template assert "data-action=\"generate-picks\"" in template assert "data-action=\"trigger-analysis\"" in template assert "setActionBusy" in template assert "showToast" in template assert "readJsonResponse" in template assert "msg.textContent" in template assert "msg.innerHTML" not in template assert "focusPriceTable" in template assert "priceRiskBoard" in template assert "growth-ops-table" in template assert "資料可信度" in template assert "PChome 貴" in template assert "PChome 便宜" in template assert "data-label=\"資料可信度\"" in template assert "renderOpsCommandDashboard" in template assert "renderPriceRiskBoard" in template assert "需檢查價格" in template assert "留意價差" in template assert "PChome 便宜" in template assert "compSourceSummary" in template assert "'external_offers':" in template assert "'自動同步'" in template assert "price_basis_label" in template assert "商品總價" in template assert "/api/ai/pchome-growth/opportunities" in template assert "最近處理紀錄" in template assert "處理紀錄" in template assert "作戰建議紀錄" not 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 "僅顯示已確認同款的商品" in template assert "tagMap[t] || ['bg-light text-dark', t]" not in template assert "\"match_type_exact\":[" not in template assert "'match_type_exact':[" in template assert "'同款確認'" 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 "FROM external_offers eo" in route_source assert "自動同步資料層" in route_source assert "competitor_data_source_counts" in route_source assert "competitor_prices" in route_source assert "ai_price_recommendations" in route_source assert "_ICAIM_DASHBOARD_TTL_SECONDS" in route_source assert "_ICAIM_DB_STATEMENT_TIMEOUT_MS" in route_source assert "JOIN LATERAL" in route_source assert "DISTINCT ON (cp.sku)" in route_source assert "_get_cached_icaim_dashboard_payload(allow_stale=True)" in route_source def test_price_comparison_page_is_action_oriented_and_plain_chinese(): template = (ROOT / "templates/price_comparison.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/price_comparison_routes.py").read_text(encoding="utf-8") assert "{% extends \"ewoooc_base.html\" %}" in template assert "PChome 商品比價決策台" in template assert "price-hero-kpis" in template assert "priceDecisionGrid" in template assert "檢查範圍" in template assert "比價流程" in template assert "price-workflow-strip" in template assert "price-result-summary-grid" in template assert "priceResultHeadline" in template assert "renderPriceDecisionCards" in template assert "renderPriceWorkflow" in template assert "今天先做:選擇要檢查的商品範圍" in template assert "資料準備狀態" in template assert "priceNextActionButton" in template assert "renderPriceCommandDashboard" in template assert "runPriceNextAction" in template assert "fetchTargetedMomoBtn" in template assert "自動找 MOMO 候選" in template assert "fetchTargetedMomoCandidates" in template assert "momoUnitCompareCandidates" in template assert "momoUnitCompareCount" in template assert "renderMomoUnitComparePanel" in template assert "自動單位價比較" in template assert "查看單位價" in template assert "sync_external_offers: true" in template assert "已同步 ${syncedCount} 筆到作戰清單" in template assert "renderMomoReviewPanel" in template assert "/api/price_comparison/fetch_momo_for_pchome" in template assert "MOMO 候選待確認" in template assert "確認 MOMO 單品/組合候選" in template assert "比價結果判讀" in template assert "需檢查價格" in template assert "可主推曝光" in template assert "價格接近" in template assert "檢查售價" in template assert "主推曝光" in template assert "PChome 貴" in template assert "PChome 便宜" in template assert "resetComparisonResult" in template assert "showToast" in template assert "text.textContent = message" in template assert "toast.innerHTML" not in template assert "Step 1" not in template assert "Step 2" not in template assert "Step 3" not in template assert "開始比價" not in template assert "PChome vs MOMO 比價" not in template assert "爬取 PChome..." not in template assert "匹配 ${comparisonResult.matched_count}" not in template assert "@price_comparison_bp.route('/price_comparison')" in route_source assert "@price_comparison_bp.route('/api/price_comparison/fetch_momo_for_pchome', methods=['POST'])" in route_source assert "render_template('price_comparison.html', active_page='price_comparison')" 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") page_js = (ROOT / "web/static/js/page-ai-recommend.js").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 "Ollama 主路徑" in template assert "Gemini 備援" in template assert "Gemini 備援(系統自動,不可手動選)" in template assert "disabled>☁️ Gemini 備援" in template assert "權杖:" in template assert "Ollama 主路徑" in page_js assert "Gemini 備援" in page_js assert "搜尋失敗:" in page_js assert "分析失敗:" in page_js forbidden_visible_text = [ "🖥️ Ollama (本地)", "☁️ Gemini (雲端)", "Web Search 功能", "渲染 Web Search", "整合 Web Search", "Token:", "費用:", "生成失敗:", "發生錯誤:", "搜尋失敗:", "分析失敗:", ] combined = template + "\n" + page_js for marker in forbidden_visible_text: assert marker not in combined 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 "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-growth/backfill-momo-candidates', 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 "挑品數" in dashboard_template assert "平均信心" in dashboard_template assert "證據完整度" in dashboard_template assert "平均價差" in dashboard_template assert "最大價差" in dashboard_template assert "待補證據" in dashboard_template assert "PICK COUNT" not in dashboard_template assert "AVG CONFIDENCE" not in dashboard_template assert "EVIDENCE GAP" not in dashboard_template assert "AVG GAP" not in dashboard_template assert "BEST GAP" not 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 "def run_pchome_growth_momo_backfill_task" in scheduler_source assert "_save_stats('pchome_match_backfill'" in scheduler_source assert "_save_stats('pchome_growth_momo_backfill'" in scheduler_source assert "retryable_candidate_revalidation_total" in scheduler_source assert "run_pchome_match_backfill_task" in run_scheduler_source assert "run_pchome_growth_momo_backfill_task" in run_scheduler_source assert "每日 10:30:pchome_match_backfill" in run_scheduler_source assert "每日 10:45:pchome_growth_momo_backfill" in run_scheduler_source assert '"run_pchome_match_backfill_task"' in agent_actions_source assert '"run_pchome_growth_momo_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 比價補強" in dashboard_template assert "data-pchome-growth-backfill-trigger" in dashboard_template assert "data-pchome-growth-backfill-status" in dashboard_template assert "每日 10:45 自動補對應" in dashboard_template assert "PCHOME MATCH BACKFILL" not in dashboard_template assert ">ACTIVE<" not in dashboard_template assert "目前 ACTIVE 商品" not 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 "/api/ai/pchome-growth/backfill-momo-candidates" in dashboard_js assert "backfillPchomeGrowthMomoCandidates" in dashboard_js 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