1328 lines
65 KiB
Python
1328 lines
65 KiB
Python
import stat
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
def test_static_assets_are_container_readable():
|
||
unreadable = []
|
||
for asset in (ROOT / "web/static").rglob("*"):
|
||
if asset.is_file() and not (asset.stat().st_mode & stat.S_IROTH):
|
||
unreadable.append(str(asset.relative_to(ROOT)))
|
||
|
||
assert unreadable == []
|
||
|
||
|
||
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/login.html",
|
||
"templates/admin/budget.html",
|
||
"templates/admin/agent_orchestration.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 "部署守門與程式碼審查" in combined
|
||
assert "等待程式碼審查完成" in combined
|
||
assert "AI 自動化健康檢查" in combined
|
||
assert "AI 閉環守門" in combined
|
||
assert "產線健康度" in combined
|
||
assert "工作隊列" in combined
|
||
assert "覆蓋率流程" in combined
|
||
assert "同步部署" in combined
|
||
assert "服務更新監控" in combined
|
||
assert "最新更新流程" in combined
|
||
assert "執行環境正常" in combined
|
||
assert "視覺檢查" in combined
|
||
assert "下載檢查紀錄" in combined
|
||
assert "健康檢查服務" in combined
|
||
assert "資料服務" in combined
|
||
assert "Top 5 成本使用情境" in combined
|
||
assert "工具協作 × 使用情境" in combined
|
||
|
||
forbidden_visible_text = [
|
||
"工作視窗",
|
||
"Codex",
|
||
"Claude",
|
||
"推到 Gitea",
|
||
"本輪已完成",
|
||
"剛剛修正",
|
||
"後續 session",
|
||
"JSONL",
|
||
"健康檢查 API",
|
||
"JSON.stringify(item.details",
|
||
"PostgreSQL",
|
||
"CSRF 防護",
|
||
"Session 2h",
|
||
"燒錢呼叫端",
|
||
"<code>{{ m.caller }}</code>",
|
||
"<code>{{ m.server }}</code>",
|
||
"AI Code Review",
|
||
"Smoke Dashboard",
|
||
"FOUR-AGENT CONTROL PLANE",
|
||
"Warning</",
|
||
"Critical</",
|
||
"Generated</",
|
||
"Action Queue",
|
||
"Pipeline Health",
|
||
"Vision Findings",
|
||
"Production Command Center",
|
||
"COVERAGE WORKFLOW",
|
||
"OpenClaw · Gemini 2.5",
|
||
"等待 Code Review 完成",
|
||
"Pipeline 執行中",
|
||
"Pipeline 失敗",
|
||
"Step ${state.current_step}",
|
||
"AI 自動化 Smoke",
|
||
"NemoTron · Dispatcher",
|
||
"<b>Commit</b>",
|
||
"<b>Branch</b>",
|
||
"${f.severity}</span>",
|
||
"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_growth_workflow_pages_hide_raw_export_and_fallback_content():
|
||
pchome_crawler = (ROOT / "templates/pchome_crawler.html").read_text(encoding="utf-8")
|
||
market_intel = (ROOT / "templates/market_intel/disabled.html").read_text(encoding="utf-8")
|
||
settings = (ROOT / "templates/settings.html").read_text(encoding="utf-8")
|
||
navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8")
|
||
shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8")
|
||
dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8")
|
||
ai_recommend_js = (ROOT / "web/static/js/page-ai-recommend.js").read_text(encoding="utf-8")
|
||
|
||
assert "PChome 商品監控" in pchome_crawler
|
||
assert "商品清單" in pchome_crawler
|
||
assert "下載完整清單" in pchome_crawler
|
||
assert "匯出 JSON" not in pchome_crawler
|
||
assert "PChome 爬蟲" not in pchome_crawler
|
||
assert "爬蟲" not in pchome_crawler
|
||
|
||
assert "資料狀態" in market_intel
|
||
assert "PChome 商品監控" in market_intel
|
||
assert "來源規格" in market_intel
|
||
assert "手動整理" in market_intel
|
||
assert "DATA STATUS" not in market_intel
|
||
assert "Adapter" not in market_intel
|
||
assert "手動 Fetch" not in market_intel
|
||
assert "API 不執行推版" not in market_intel
|
||
assert "PChome 爬蟲" not in market_intel
|
||
|
||
assert "商品監控中心" in settings
|
||
assert "監控來源設定" in settings
|
||
assert "商品監控" in navbar
|
||
assert "商品監控狀態" in shell
|
||
assert "全站商品監控" in dashboard_js
|
||
for source in (settings, navbar, shell, dashboard_js):
|
||
assert "爬蟲" not in source
|
||
|
||
assert "外部訊號已取得,但尚未整理成可直接判斷的摘要" in ai_recommend_js
|
||
assert "商品判斷尚未整理成可執行摘要" in ai_recommend_js
|
||
assert "<pre class=\"mb-0 small\"" not in ai_recommend_js
|
||
assert "${escapeHtml(data.raw_content)}" not in ai_recommend_js
|
||
|
||
|
||
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 × minimal grid × operational clarity" in tokens
|
||
assert "Claude" not 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")
|
||
guard_css = (ROOT / "web/static/css/ewoooc-v3-page-guard.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/<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 "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 "商品ID {{ product.i_code }}" in dashboard
|
||
assert "開 MOMO 賣場" in dashboard
|
||
assert "開 PChome 賣場" in dashboard
|
||
assert "is-ai-picks-wrap" in dashboard
|
||
assert "dashboard-product-thumb-missing" in dashboard
|
||
assert "dashboard-product-identity" in dashboard_css
|
||
assert ".dashboard-table.is-ai-picks td:nth-child(6)" in dashboard_css
|
||
assert "position: sticky;" in dashboard_css
|
||
assert "min-width: 230px" in dashboard_css
|
||
assert ".dashboard-table-wrap.is-ai-picks-wrap" in guard_css
|
||
assert "min-width: 1660px !important" in guard_css
|
||
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 "待確認 ${momoReviewCandidates.length} 筆:先並排看兩家賣場" in template
|
||
assert "price-review-compare" in template
|
||
assert "price-review-store is-pchome" in template
|
||
assert "price-review-store is-momo" in template
|
||
assert "同時開兩家賣場" 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 "雙開賣場" in template
|
||
assert "openComparisonStores" in template
|
||
assert "data-pchome-url" in template
|
||
assert "data-momo-url" in template
|
||
assert "Excel 至少要有商品名稱與售價" in template
|
||
assert "貼上 MOMO 商品、售價與賣場連結" in template
|
||
assert "先選擇 MOMO 商品檔案。" in template
|
||
assert "先貼上商品、售價與賣場連結。" in template
|
||
assert "resetComparisonResult" in template
|
||
assert "showToast" in template
|
||
assert "text.textContent = message" in template
|
||
assert "toast.innerHTML" not in template
|
||
assert "格式說明" not in template
|
||
assert "商品名稱,價格" not in template
|
||
assert "欄位" not in template
|
||
assert "系統會整理成可比價清單" not in template
|
||
assert "請選擇檔案" not in template
|
||
assert "請輸入商品資料" 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_utility_pages_keep_operator_copy_professional():
|
||
ppt_history = (ROOT / "templates/admin/ppt_audit_history.html").read_text(encoding="utf-8")
|
||
ppt_preview = (ROOT / "templates/admin/ppt_audit_preview.html").read_text(encoding="utf-8")
|
||
auto_import = (ROOT / "templates/auto_import_index.html").read_text(encoding="utf-8")
|
||
stockout_import = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
|
||
vendor_import_js = (ROOT / "web/static/js/page-vendor-import.js").read_text(encoding="utf-8")
|
||
stockout_index = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8")
|
||
stockout_history = (ROOT / "templates/vendor_stockout_history_v2.html").read_text(encoding="utf-8")
|
||
ai_calls = (ROOT / "templates/admin/ai_calls_dashboard.html").read_text(encoding="utf-8")
|
||
observability_labels = (ROOT / "templates/admin/_observability_labels.html").read_text(encoding="utf-8")
|
||
host_health = (ROOT / "templates/admin/host_health.html").read_text(encoding="utf-8")
|
||
cicd_dashboard = (ROOT / "templates/cicd_dashboard.html").read_text(encoding="utf-8")
|
||
observability_js = (ROOT / "web/static/js/observability-charts.js").read_text(encoding="utf-8")
|
||
budget = (ROOT / "templates/admin/budget.html").read_text(encoding="utf-8")
|
||
agent_orchestration = (ROOT / "templates/admin/agent_orchestration.html").read_text(encoding="utf-8")
|
||
ai_automation = (ROOT / "templates/ai_automation_smoke.html").read_text(encoding="utf-8")
|
||
code_review = (ROOT / "templates/code_review.html").read_text(encoding="utf-8")
|
||
login = (ROOT / "templates/login.html").read_text(encoding="utf-8")
|
||
combined = "\n".join([
|
||
ppt_history,
|
||
ppt_preview,
|
||
auto_import,
|
||
stockout_import,
|
||
vendor_import_js,
|
||
stockout_index,
|
||
stockout_history,
|
||
ai_calls,
|
||
observability_labels,
|
||
host_health,
|
||
cicd_dashboard,
|
||
observability_js,
|
||
budget,
|
||
agent_orchestration,
|
||
ai_automation,
|
||
code_review,
|
||
login,
|
||
])
|
||
|
||
assert "簡報線上預覽" in ppt_preview
|
||
assert "下載簡報檔" in ppt_history
|
||
assert "送出後更新日報、成長分析與今日作戰清單" in auto_import
|
||
assert "等待系統更新任務狀態;若重複停在異常" in auto_import
|
||
assert "return raw ||" not in auto_import
|
||
assert "缺少必要資料時,會先停止匯入" in stockout_import
|
||
assert "先選擇供應商缺貨 Excel 檔。" in vendor_import_js
|
||
assert "使用情境" in ai_calls
|
||
assert "全部使用情境" in ai_calls
|
||
assert "情境 × 知識命中矩陣" in ai_calls
|
||
assert "服務更新監控" in cicd_dashboard
|
||
assert "正式服務更新與可用性監控" in cicd_dashboard
|
||
assert "供貨風險" in stockout_index
|
||
assert "無服務資料 / 未連線" in host_health
|
||
assert "部署檢查已排入背景處理" in observability_js
|
||
assert "Top 5 成本使用情境" in budget
|
||
assert "工具協作 × 使用情境" in agent_orchestration
|
||
assert "下載檢查紀錄" in ai_automation
|
||
assert "健康檢查服務" in ai_automation
|
||
assert "上線證據" in code_review
|
||
assert "資料服務" in login
|
||
system_settings = (ROOT / "templates/system_settings.html").read_text(encoding="utf-8")
|
||
assert "系統操作暫時沒有完成,請稍後重試" in system_settings
|
||
assert "return raw;" not in system_settings
|
||
|
||
forbidden = [
|
||
"工作視窗",
|
||
"Codex",
|
||
"Claude",
|
||
"推到 Gitea",
|
||
"本輪已完成",
|
||
"剛剛修正",
|
||
"後續 session",
|
||
"JSONL",
|
||
"健康檢查 API",
|
||
"JSON.stringify(item.details",
|
||
"PostgreSQL",
|
||
"CSRF 防護",
|
||
"Session 2h",
|
||
"燒錢呼叫端",
|
||
"<code>{{ m.caller }}</code>",
|
||
"<code>{{ m.server }}</code>",
|
||
"PPT Online Preview",
|
||
"Preview unavailable",
|
||
"原始 PPTX",
|
||
"原始:",
|
||
"無模型資料",
|
||
"請選擇 Excel 檔案",
|
||
"Vendor Stockout",
|
||
"VENDOR STOCKOUT",
|
||
"全部呼叫端",
|
||
"呼叫端 ×",
|
||
"<code>{{ c.caller }}</code>",
|
||
"NIM Elephant",
|
||
"OpenRouter",
|
||
"GitLab 部署紀錄",
|
||
"${p.ref} @ ${p.sha}",
|
||
"error_log.substring",
|
||
"${escapeHtml(env.error)}",
|
||
"系統會去重",
|
||
"系統會拒絕",
|
||
"管線 ID",
|
||
"Commit:",
|
||
"變更檔案:",
|
||
"查看系統日誌",
|
||
"could not locate runnable browser",
|
||
"daily_sales_snapshot",
|
||
"realtime_sales_monthly",
|
||
"snapshot_date",
|
||
"psycopg2",
|
||
]
|
||
for marker in forbidden:
|
||
assert marker not in combined
|
||
|
||
|
||
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 "setProduct({{ category|tojson }})" in template
|
||
assert "quickWebSearch('保濕面膜')" not in template
|
||
assert "fetch('/api/ai/generate_copy'" in page_js
|
||
assert "fetch('/api/ai/web_search'" in page_js
|
||
assert "fetch('/api/ai/product_insights'" in page_js
|
||
assert "fetch('/api/ai/gemini_usage?days=30')" in page_js
|
||
assert "renderBestsellerCard" in page_js
|
||
assert "ar-product-card__img" in page_js
|
||
assert "待補圖片" in page_js
|
||
assert "商品 ID" in page_js
|
||
assert "開賣場" in page_js
|
||
assert "product_id: el.dataset.productId" in page_js
|
||
assert "Object.assign(window" in page_js
|
||
assert "setProductFromCard" in page_js
|
||
assert "商品 ID、圖片與賣場連結可一眼確認" in template
|
||
assert 'id="platformMomo" value="momo">' in template
|
||
assert "mock" not in template.lower()
|
||
assert "假商品" not in template
|
||
assert "PChome 銷售建議" in template
|
||
assert "銷售動作生成" in template
|
||
assert "建議目的" in template
|
||
assert "處理順序" in template
|
||
assert "建議引擎" in template
|
||
assert "備援守門" in template
|
||
assert "ar-engine-settings" in template
|
||
assert "整理訊號" in page_js
|
||
assert "商品判斷暫時不可用" in page_js
|
||
|
||
forbidden_visible_text = [
|
||
"🖥️ Ollama (本地)",
|
||
"☁️ Gemini (雲端)",
|
||
"Ollama 主路徑",
|
||
"Gemini 備援",
|
||
"Gemini 備援(系統自動,不可手動選)",
|
||
"disabled>☁️ Gemini 備援",
|
||
"AI 模型主路徑",
|
||
"AI 路徑",
|
||
"分析模型",
|
||
"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
|
||
assert "'product_id': p.product_id" in (ROOT / "services/pchome_crawler.py").read_text(encoding="utf-8")
|
||
assert "'product_id': p.product_id" in (ROOT / "services/momo_crawler.py").read_text(encoding="utf-8")
|
||
|
||
|
||
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-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 "ai-intel-legacy-status" 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/<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/api/import/excel" in template
|
||
assert "範例" not in template_function
|
||
assert "ui='v2'" not in template
|
||
assert "mock" not in template.lower()
|
||
assert "假" not in template
|