289 lines
14 KiB
Python
289 lines
14 KiB
Python
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")
|
||
|
||
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 'name="ui" value="v2"' not in base
|
||
|
||
forbidden_markers = [
|
||
"mockProducts",
|
||
"mockData",
|
||
"fake",
|
||
"假商品",
|
||
"假 KPI",
|
||
]
|
||
combined = shell + "\n" + base
|
||
assert all(marker not in combined for marker in forbidden_markers)
|
||
|
||
|
||
def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||
route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
|
||
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
|
||
|
||
assert "request.args.get('ui') == 'legacy'" in route_source
|
||
assert "template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'" in route_source
|
||
assert "get_full_dashboard_data()" in route_source
|
||
assert "MockRecord" not in route_source
|
||
assert "{% for item in items %}" in dashboard
|
||
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_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")
|
||
|
||
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 "https://cdn.jsdelivr.net/npm/chart.js" in dashboard
|
||
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 "onclick=\"event.stopPropagation(); showHistory(this.dataset.productId, this.dataset.productName);\"" in dashboard
|
||
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 dashboard
|
||
assert "priceChartInstance = new Chart" in dashboard
|
||
assert "目前沒有可顯示的歷史價格紀錄" in dashboard
|
||
|
||
|
||
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")
|
||
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 待比對" in dashboard
|
||
assert "series.pchome" in dashboard
|
||
assert "label: 'PChome'" in dashboard
|
||
assert "含 PChome 歷史快照" in dashboard
|
||
|
||
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")
|
||
template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")
|
||
|
||
assert "MIN_MATCH_SCORE = 0.42" 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 "crawler.search_products(keyword, limit=SEARCH_LIMIT)" in feeder_source
|
||
assert "_fetch_unmatched_priority_skus" in feeder_source
|
||
assert "run_unmatched_priority" in feeder_source
|
||
|
||
assert "generate_product_pick_list" in agent_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 "{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 "run_unmatched_priority(limit=limit)" in route_source
|
||
assert "generate_product_pick_list(engine, limit=30)" in route_source
|
||
assert "完成後會重算 AI 挑品清單" in route_source
|
||
assert "match_rate" in route_source
|
||
assert "product_pick_count" in route_source
|
||
|
||
scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
|
||
run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8")
|
||
agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8")
|
||
assert "def run_pchome_match_backfill_task" in scheduler_source
|
||
assert "_save_stats('pchome_match_backfill'" in scheduler_source
|
||
assert "run_pchome_match_backfill_task" in run_scheduler_source
|
||
assert "每日 10:30:pchome_match_backfill" in run_scheduler_source
|
||
assert '"run_pchome_match_backfill_task"' in agent_actions_source
|
||
|
||
assert "產生挑品清單" in template
|
||
assert "補抓待比對" in template
|
||
assert "generatePickList" in template
|
||
assert "backfillPchomeMatches" in template
|
||
assert "/api/ai/product-picks/generate" in template
|
||
assert "/api/ai/pchome-match/backfill" in template
|
||
assert "'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")
|
||
|
||
assert route_source.count("request.args.get('ui') == 'legacy'") == 5
|
||
assert "template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'" in route_source
|
||
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(this.dataset.iCode, this.dataset.productName)" in template
|
||
assert "fetch(`/api/history/i-code/${encodeURIComponent(iCode)}?range=${activeCampaignHistoryRange}`)" in template
|
||
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 template
|
||
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 "request.args.get('ui') == 'legacy'" in route_source
|
||
assert not (ROOT / "web/templates/vendor_stockout_index_v2.html").exists()
|
||
assert "vendor_stockout_index_v2.html" in route_source
|
||
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_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
|