Files
ewoooc/tests/test_pchome_revenue_growth_service.py
ogt 2144ef2102
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s
fix: lock pchome growth ui copy guardrails
2026-06-26 11:38:04 +08:00

958 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from sqlalchemy import create_engine, text
def _seed_growth_tables(engine):
with engine.begin() as conn:
conn.execute(text(
'CREATE TABLE daily_sales_snapshot ('
'"商品ID" TEXT, "商品名稱" TEXT, snapshot_date TEXT, "總業績" REAL, "數量" REAL, "商品館" TEXT)'
))
conn.execute(text(
"CREATE TABLE competitor_prices ("
"sku TEXT, source TEXT, competitor_product_id TEXT, competitor_product_name TEXT, "
"price REAL, match_score REAL, tags TEXT, crawled_at TEXT, expires_at TEXT)"
))
conn.execute(text(
"CREATE TABLE products ("
"id INTEGER PRIMARY KEY, i_code TEXT, name TEXT, status TEXT)"
))
conn.execute(text(
"CREATE TABLE price_records ("
"id INTEGER PRIMARY KEY, product_id INTEGER, price REAL, timestamp TEXT)"
))
conn.execute(text("""
INSERT INTO daily_sales_snapshot
("商品ID", "商品名稱", snapshot_date, "總業績", "數量", "商品館")
VALUES
('PCH-1', '高業績商品', '2026-06-14', 120000, 36, '保養'),
('PCH-1', '高業績商品', '2026-06-08', 180000, 50, '保養'),
('PCH-2', '待補對應商品', '2026-06-14', 90000, 18, '彩妝'),
('PCH-2', '待補對應商品', '2026-06-08', 30000, 8, '彩妝')
"""))
conn.execute(text(
"INSERT INTO products (id, i_code, name, status) "
"VALUES (1, 'MOMO-1', 'MOMO 參考商品', 'ACTIVE')"
))
conn.execute(text(
"INSERT INTO price_records (product_id, price, timestamp) "
"VALUES (1, 900, '2026-06-14 10:00:00')"
))
conn.execute(text("""
INSERT INTO competitor_prices
(sku, source, competitor_product_id, competitor_product_name,
price, match_score, tags, crawled_at, expires_at)
VALUES
('MOMO-1', 'pchome', 'PCH-1', '高業績商品',
1000, 0.91, '["identity_v2","match_type_exact"]', '2026-06-14 11:00:00', NULL)
"""))
def _seed_growth_external_offers(engine):
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE external_offers (
id INTEGER PRIMARY KEY,
source_code TEXT,
platform_code TEXT,
source_product_id TEXT,
source_offer_key TEXT,
title TEXT,
product_url TEXT,
price REAL,
observed_at TEXT,
expires_at TEXT,
ingestion_method TEXT,
pchome_product_id TEXT,
momo_sku TEXT,
match_status TEXT,
quality_score REAL,
data_quality_status TEXT,
quality_notes_json TEXT,
raw_payload_json TEXT
)
"""))
conn.execute(text("""
INSERT INTO external_offers (
id, source_code, platform_code, source_product_id, source_offer_key,
title, product_url, price, observed_at, expires_at, ingestion_method,
pchome_product_id, momo_sku, match_status, quality_score,
data_quality_status, quality_notes_json, raw_payload_json
)
VALUES (
1, 'momo_reference', 'momo', 'MOMO-NEW', 'momo_reference:MOMO-NEW:PCH-1',
'MOMO 新資料層商品', 'https://www.momoshop.com.tw/goods/MOMO-NEW', 870, '2026-06-14 12:00:00', NULL, 'legacy_competitor_cache',
'PCH-1', 'MOMO-NEW', 'verified', 92,
'verified', '["自動同步"]',
'{"pchome_public_price": 1000, "pchome_public_name": "PChome 公開商品"}'
)
"""))
def _seed_growth_unit_price_external_offer(engine):
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE external_offers (
id INTEGER PRIMARY KEY,
source_code TEXT,
platform_code TEXT,
source_product_id TEXT,
source_offer_key TEXT,
title TEXT,
product_url TEXT,
price REAL,
observed_at TEXT,
expires_at TEXT,
ingestion_method TEXT,
pchome_product_id TEXT,
momo_sku TEXT,
match_status TEXT,
quality_score REAL,
data_quality_status TEXT,
quality_notes_json TEXT,
raw_payload_json TEXT
)
"""))
conn.execute(text("""
INSERT INTO external_offers (
id, source_code, platform_code, source_product_id, source_offer_key,
title, product_url, price, observed_at, expires_at, ingestion_method,
pchome_product_id, momo_sku, match_status, quality_score,
data_quality_status, quality_notes_json, raw_payload_json
)
VALUES (
1, 'momo_reference', 'momo', 'MOMO-UNIT', 'momo_reference:MOMO-UNIT:PCH-1:unit_price',
'MOMO 單位價商品', 'https://www.momoshop.com.tw/goods/MOMO-UNIT', 468, '2026-06-14 12:00:00', NULL, 'targeted_momo_search',
'PCH-1', 'MOMO-UNIT', 'verified', 82,
'verified', '["自動單位價比較"]',
'{"price_basis": "unit_price", "pchome_public_price": 920, "pchome_public_name": "PChome 公開商品", "tags": ["identity_v2", "price_basis_unit_price"], "unit_price_comparison": {"unit_label": "ml", "momo_unit_price": 11.7, "competitor_unit_price": 23.0, "momo_total_quantity": 40, "competitor_total_quantity": 40, "unit_gap_pct": -49.13}}'
)
"""))
def _seed_growth_review_candidate_offer(engine):
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE external_offers (
id INTEGER PRIMARY KEY,
source_code TEXT,
platform_code TEXT,
source_product_id TEXT,
source_offer_key TEXT,
title TEXT,
product_url TEXT,
image_url TEXT,
price REAL,
observed_at TEXT,
expires_at TEXT,
ingestion_method TEXT,
pchome_product_id TEXT,
momo_sku TEXT,
match_status TEXT,
quality_score REAL,
data_quality_status TEXT,
quality_notes_json TEXT,
raw_payload_json TEXT,
updated_at TEXT
)
"""))
conn.execute(text("""
INSERT INTO external_offers (
id, source_code, platform_code, source_product_id, source_offer_key,
title, product_url, image_url, price, observed_at, expires_at, ingestion_method,
pchome_product_id, momo_sku, match_status, quality_score,
data_quality_status, quality_notes_json, raw_payload_json, updated_at
)
VALUES (
1, 'momo_reference', 'momo', 'MOMO-REVIEW', 'momo_reference:MOMO-REVIEW:PCH-REVIEW',
'MOMO 候選商品', 'https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=MOMO-REVIEW',
NULL, 2300, '2026-06-25 12:00:00', NULL, 'targeted_momo_review',
'PCH-REVIEW', 'MOMO-REVIEW', 'needs_review', 97,
'needs_review', '[]',
'{"pchome_public_price": 1430, "pchome_public_name": "PChome 待確認商品", "target_gap_pct": 60.8, "match_reasons": ["variant_selection_review", "focused_exact_identity_ysl_blush_catalog"]}',
'2026-06-25 12:10:00'
)
"""))
def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
engine = create_engine("sqlite:///:memory:")
_seed_growth_tables(engine)
payload = build_pchome_growth_opportunities(engine, limit=5)
assert payload["success"] is True
assert payload["system_name"] == "PChome 業績成長自動化作戰系統"
assert payload["source_scope"]["active_external_sources"] == ["MOMO 外部價格參考"]
assert payload["source_scope"]["paused_external_sources"] == ["蝦皮", "酷澎"]
readiness = payload["source_scope"]["source_readiness"]
sources = {source["code"]: source for source in readiness["sources"]}
assert sources["momo_reference"]["status_label"] == "正在使用"
assert sources["shopee"]["status_label"] == "先暫停"
assert sources["coupang"]["status_label"] == "先暫停"
assert payload["stats"]["candidate_count"] == 2
assert payload["stats"]["mapped_count"] == 1
assert payload["stats"]["needs_mapping_count"] == 1
actions = {item["pchome_product_id"]: item["recommended_action"]["label"] for item in payload["opportunities"]}
assert actions["PCH-1"] == "檢查售價與活動"
assert actions["PCH-2"] == "先補商品對應"
pchome_1 = next(item for item in payload["opportunities"] if item["pchome_product_id"] == "PCH-1")
assert pchome_1["pchome_price"] == 3488.37
assert pchome_1["external_price"]["data_source"] == "competitor_prices"
assert payload["stats"]["external_data_source_counts"] == {"舊比價快取": 1}
assert all("identity" not in " ".join(item["reason_lines"]).lower() for item in payload["opportunities"])
def test_momo_review_candidates_return_dual_store_links_and_plain_reasons():
from services.external_market_offer_service import list_momo_review_candidates
engine = create_engine("sqlite:///:memory:")
_seed_growth_review_candidate_offer(engine)
payload = list_momo_review_candidates(engine)
assert payload["success"] is True
assert payload["count"] == 1
row = payload["rows"][0]
assert row["pchome_url"] == "https://24h.pchome.com.tw/prod/PCH-REVIEW"
assert row["momo_url"] == "https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=MOMO-REVIEW"
assert row["match_reason_labels"]
assert all("_" not in label for label in row["match_reason_labels"])
assert all("_" not in label for label in row["match_reasons"])
assert "色號" in row["reason_summary"] or "款式" in row["reason_summary"]
def test_pchome_growth_prefers_external_offers_over_legacy_competitor_cache():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
engine = create_engine("sqlite:///:memory:")
_seed_growth_tables(engine)
_seed_growth_external_offers(engine)
payload = build_pchome_growth_opportunities(engine, limit=5)
assert payload["success"] is True
pchome_1 = next(item for item in payload["opportunities"] if item["pchome_product_id"] == "PCH-1")
external_price = pchome_1["external_price"]
assert external_price["data_source"] == "external_offers"
assert external_price["data_source_label"] == "自動同步資料層"
assert external_price["momo_sku"] == "MOMO-NEW"
assert external_price["momo_price"] == 870
assert external_price["pchome_price"] == 1000
assert external_price["gap_pct"] == -13.0
assert payload["stats"]["external_data_source_counts"] == {"自動同步資料層": 1}
def test_pchome_growth_understands_unit_price_external_offers():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
engine = create_engine("sqlite:///:memory:")
_seed_growth_tables(engine)
_seed_growth_unit_price_external_offer(engine)
payload = build_pchome_growth_opportunities(engine, limit=5)
assert payload["success"] is True
pchome_1 = next(item for item in payload["opportunities"] if item["pchome_product_id"] == "PCH-1")
external_price = pchome_1["external_price"]
assert external_price["data_source"] == "external_offers"
assert external_price["price_basis"] == "unit_price"
assert external_price["price_basis_label"] == "單位價"
assert external_price["momo_price"] == 468
assert external_price["pchome_price"] == 920
assert external_price["momo_unit_price"] == 11.7
assert external_price["pchome_unit_price"] == 23.0
assert external_price["gap_pct"] == -49.1
assert pchome_1["recommended_action"]["label"] == "檢查售價與活動"
assert pchome_1["data_quality"]["label"] == "資料可用單位價判斷"
assert any("單位價" in line for line in pchome_1["reason_lines"])
def test_pchome_growth_route_cache_respects_shared_invalidation_epoch(monkeypatch):
from routes import ai_routes as routes
epoch = {"value": 1.0}
monkeypatch.setattr(routes, "get_pchome_growth_cache_epoch", lambda: epoch["value"])
routes._PCHOME_GROWTH_CACHE.update({
"expires_at": 0.0,
"epoch": 0.0,
"payload": None,
})
routes._set_pchome_growth_cache({"success": True, "stats": {"candidate_count": 1}})
cached = routes._get_cached_pchome_growth_payload()
assert cached["success"] is True
assert cached["stats"]["candidate_count"] == 1
epoch["value"] = 2.0
assert routes._get_cached_pchome_growth_payload() is None
def test_pchome_growth_momo_backfill_service_targets_unmapped_high_sales_items():
from services.pchome_growth_momo_backfill_service import run_pchome_growth_momo_backfill
captured = {}
class FakeEngine:
pass
before_payload = {
"success": True,
"stats": {"mapping_rate": 0, "candidate_count": 3, "mapped_count": 0},
"opportunities": [
{
"pchome_product_id": "PCH-NEEDS-1",
"product_name": "需要補對應商品一",
"pchome_price": 920,
"sales_7d": 120000,
"priority_score": 91,
"external_price": None,
"recommended_action": {"code": "map_external_product"},
},
{
"pchome_product_id": "PCH-MAPPED",
"product_name": "已有比價商品",
"pchome_price": 880,
"external_price": {"momo_sku": "MOMO-OLD"},
"recommended_action": {"code": "review_price_or_promo"},
},
{
"pchome_product_id": "PCH-NEEDS-2",
"product_name": "需要補對應商品二",
"pchome_price": 760,
"sales_7d": 90000,
"priority_score": 82,
"external_price": None,
"recommended_action": {"code": "map_external_product"},
},
],
}
after_payload = {
"success": True,
"stats": {"mapping_rate": 66.7, "candidate_count": 3, "mapped_count": 2},
"opportunities": [],
}
payload_calls = []
def fake_build_payload(engine, limit):
payload_calls.append(limit)
return before_payload if len(payload_calls) == 1 else after_payload
def fake_search(targets, limit):
captured["targets"] = targets
captured["search_limit"] = limit
return True, "找到候選", [
{
"product_id": "MOMO-AUTO",
"auto_compare_type": "total_price",
"can_auto_compare": True,
},
{
"product_id": "MOMO-UNIT",
"auto_compare_type": "unit_price",
"can_auto_compare": True,
},
{
"product_id": "MOMO-REVIEW",
"auto_compare_type": "manual_review",
"can_auto_compare": False,
},
]
def fake_sync(engine, candidates):
captured["sync_candidates"] = candidates
return {
"success": True,
"status": "synced",
"written_count": len(candidates),
"total_price_count": 1,
"unit_price_count": 1,
}
def fake_sync_review(engine, candidates):
captured["review_sync_candidates"] = candidates
return {
"success": True,
"status": "synced",
"written_count": len(candidates),
}
payload = run_pchome_growth_momo_backfill(
FakeEngine(),
limit=2,
build_payload_func=fake_build_payload,
search_func=fake_search,
sync_func=fake_sync,
sync_review_func=fake_sync_review,
)
assert payload["success"] is True
assert payload["data"]["scanned_products"] == 2
assert payload["data"]["candidate_count"] == 3
assert payload["data"]["auto_compare_count"] == 2
assert payload["data"]["review_count"] == 1
assert payload["data"]["external_offer_sync"]["written_count"] == 2
assert payload["data"]["review_candidate_sync"]["written_count"] == 1
assert payload["data"]["after_stats"]["mapping_rate"] == 66.7
assert [item["product_id"] for item in captured["targets"]] == ["PCH-NEEDS-1", "PCH-NEEDS-2"]
assert [item["price"] for item in captured["targets"]] == [920, 760]
assert [item["product_id"] for item in captured["sync_candidates"]] == ["MOMO-AUTO", "MOMO-UNIT"]
assert [item["product_id"] for item in captured["review_sync_candidates"]] == ["MOMO-REVIEW"]
assert captured["search_limit"] == 2
def test_pchome_growth_momo_backfill_default_search_uses_deeper_terms(monkeypatch):
from services import pchome_growth_momo_backfill_service as service
captured = {}
def fake_search(targets, **kwargs):
captured["targets"] = targets
captured["kwargs"] = kwargs
return True, "ok", []
monkeypatch.delenv("PCHOME_GROWTH_MOMO_BACKFILL_LIMIT_PER_TERM", raising=False)
monkeypatch.delenv("PCHOME_GROWTH_MOMO_BACKFILL_MAX_TERMS", raising=False)
monkeypatch.delenv("PCHOME_GROWTH_MOMO_BACKFILL_MIN_SCORE", raising=False)
monkeypatch.setattr("services.momo_crawler.search_momo_products_for_pchome_products", fake_search)
payload = service._default_search_candidates([{"product_id": "PCH-1", "name": "商品"}], limit=3)
assert payload == (True, "ok", [])
assert captured["kwargs"]["max_products"] == 3
assert captured["kwargs"]["limit_per_product"] == 8
assert captured["kwargs"]["max_terms_per_product"] == 8
assert captured["kwargs"]["min_score"] == 0.45
def test_pchome_growth_momo_backfill_route_calls_shared_service(monkeypatch):
from flask import Flask
from routes import ai_routes as routes
captured = {}
class FakeEngine:
def dispose(self):
captured["disposed"] = True
def fake_run(engine, limit):
captured["limit"] = limit
return {
"success": True,
"message": "已完成",
"data": {
"scanned_products": 2,
"candidate_count": 3,
"auto_compare_count": 2,
"review_count": 1,
"external_offer_sync": {"written_count": 2},
},
}
monkeypatch.setattr(routes, "_create_icaim_dashboard_engine", lambda database_path: FakeEngine())
monkeypatch.setattr(routes, "_run_pchome_growth_momo_backfill", fake_run)
app = Flask(__name__)
with app.test_request_context(
"/api/ai/pchome-growth/backfill-momo-candidates",
method="POST",
json={"limit": 2},
):
response = routes.api_pchome_growth_backfill_momo_candidates.__wrapped__()
payload = response.get_json()
assert payload["success"] is True
assert payload["data"]["external_offer_sync"]["written_count"] == 2
assert captured["limit"] == 2
assert captured["disposed"] is True
def test_ai_product_pick_sales_join_by_sku_disabled_by_default(monkeypatch):
from services.ai_product_pick_agent import _sales_join_by_momo_sku_enabled
monkeypatch.delenv("PCHOME_SALES_JOIN_BY_MOMO_SKU_ENABLED", raising=False)
assert _sales_join_by_momo_sku_enabled() is False
def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
from pathlib import Path
template = Path("templates/ai_intelligence.html").read_text(encoding="utf-8")
assert "PChome 業績成長自動化作戰系統" in template
assert "/api/ai/pchome-growth/opportunities" in template
assert "/api/ai/pchome-growth/external-offers/csv-dry-run" in template
assert "/api/ai/pchome-growth/review-candidates" in template
assert "growthSourceReadiness" in template
assert "MOMO 待確認候選" in template
assert "確認同款" in template
assert "不是同款" in template
assert "雙開賣場" in template
assert "openReviewCandidateStores" in template
assert "data-pchome-url" in template
assert "data-momo-url" in template
assert "PChome 賣場" in template
assert "MOMO 賣場" in template
assert "開 PChome" in template
assert "開 MOMO" in template
assert "row.match_reason_labels" in template
assert "row.match_reasons" not in template
assert "variant_selection_review" not in template
assert "focused_exact_identity" not in template
assert "momo_reference" not in template
assert "source_code" not in template
assert "PChome 價格壓力" not in template
assert "MOMO 價格優勢" not in template
assert "MOMO 更便宜" not in template
assert "PChome 有優勢" not in template
assert "review-candidate-compare" in template
assert "review_external_candidate" in template
assert "focusReviewCandidate" in template
assert "handleDrilldownKey" in template
assert "drilldown-hint" in template
assert "候選待確認" in template
assert "看明細" in template
assert "data-pchome-id" in template
assert "今日重點總覽" in template
assert "nextActionTitle" in template
assert "商品處理進度" in template
def test_growth_analysis_uses_actionable_price_command_panel():
from pathlib import Path
template = Path("templates/growth_analysis.html").read_text(encoding="utf-8")
css = Path("web/static/css/page-growth-bem.css").read_text(encoding="utf-8")
assert "PChome 價格作戰可用度" in template
assert "ga-chart-card__body--command" in template
assert "ga-competitor-command" in template
assert "可直接決策" in template
assert "待補 / 待確認" in template
assert "下一步" in template
assert "前往今日作戰" in template
assert "ga-competitor-signal" in css
assert "ga-competitor-next" in css
assert ".growth-analysis-page .ga-chart-card__body--command" in css
assert "比價資料品質" not in template
assert "高信心門檻" not in template
assert "未知新鮮度" not in template
assert "人工否決" not in template
assert "可直接決策" in template
assert "chart_data.competitor_coverage" in template
assert "先補齊高業績商品的 MOMO 對應" in template
assert "先刷新過期價格" in template
assert "先處理待補與候選確認" in template
assert "可進入價格策略檢查" in template
assert "MOMO 低價壓力趨勢" in template
assert "PChome 價格優勢" in template
assert "competitorPressureChart" in template
assert "growth-command-pro" not in template
assert "growth-ops-table" not in template
assert "外部報價預檢" not in template
def test_formal_homepage_routes_to_growth_command_center():
from pathlib import Path
route_source = Path("routes/dashboard_routes.py").read_text(encoding="utf-8")
assert "@dashboard_bp.route('/')" in route_source
assert "render_template('ai_intelligence.html', active_page='ai_intelligence')" in route_source
assert "return redirect" not in route_source
assert "url_for('ai.ai_intelligence')" not in route_source
assert "@dashboard_bp.route('/dashboard')" in route_source
assert "@dashboard_bp.route('/product-dashboard')" in route_source
def test_sidebar_uses_growth_command_center_as_primary_entry():
from pathlib import Path
shell = Path("templates/components/_ewoooc_shell.html").read_text(encoding="utf-8")
base = Path("templates/ewoooc_base.html").read_text(encoding="utf-8")
assert 'href="/"' in shell
assert 'href="/ai_intelligence"' not in shell
assert "業績成長指揮台" in shell
assert "舊商品看板" in shell
assert 'href="/dashboard?filter=pchome_review' in shell
assert 'href="/?filter=pchome_review' not in shell
assert "{% set _group_monitor = ['ai_intelligence', 'dashboard', 'edm', 'campaigns'] %}" in base
assert "momo-growth-rail" in base
assert "PChome 業績提升" in base
assert "現在:評估缺口" in base
assert "現在:分析原因" in base
assert "現在:產生建議" in base
assert "現在:執行解法" in base
assert "現在:守住品質" in base
assert "評估" in base
assert "分析" in base
assert "建議" in base
assert "解法" in base
ai_helper_line = next(line for line in shell.splitlines() if 'href="/ai_recommend"' in line)
assert "ai_intelligence" not in ai_helper_line
def test_global_ui_guard_keeps_pages_professional_and_responsive():
from pathlib import Path
shell_css = Path("web/static/css/ewoooc-shell.css").read_text(encoding="utf-8")
guard_css = Path("web/static/css/ewoooc-v3-page-guard.css").read_text(encoding="utf-8")
assert "width: min(100%, var(--momo-content-max-width))" in shell_css
assert ".momo-content > *" in shell_css
assert "--momo-page-readable-line: 74ch" in guard_css
assert "--momo-page-action-strip-width: 1040px" in guard_css
assert "overflow-x: clip" in guard_css
assert "overflow-wrap: anywhere" in guard_css
assert "white-space: normal" in guard_css
assert "-webkit-line-clamp: 2" in guard_css
assert "momo-growth-rail" in shell_css
assert "momo-growth-step.is-active" in shell_css
assert ".momo-content .btn:not(.btn-close)" in guard_css
assert ":not(.btn-close)" in guard_css
def test_primary_pages_use_growth_outcome_copy_instead_of_feature_explaining():
from pathlib import Path
expected = {
"templates/daily_sales.html": "找出下滑與價差壓力",
"templates/ai_recommend.html": "把價差、商品證據與趨勢轉成主推、調價、補比價動作",
"templates/auto_import_index.html": "保持 PChome 業績新鮮",
"templates/price_comparison.html": "確認同款、判斷價差、決定下一步",
"templates/vendor_stockout_index_v2.html": "避免主推商品斷貨",
"templates/monthly_summary_analysis.html": "判斷成長、毛利與品類結構",
"templates/dashboard_v2.html": "先看業績,再決定調價、曝光與組合",
"templates/edm_dashboard_v2.html": "用活動價格異動找主推、補貨與曝光機會",
"templates/sales_analysis.html": "用分類、品牌與毛利找出主推、守價與補資料順序",
"templates/growth_analysis.html": "用月趨勢評估成長缺口、價差壓力與毛利品質",
"templates/vendor_stockout_import_v2.html": "補齊缺貨資料,先保住主推商品供貨",
"templates/vendor_stockout_list_v2.html": "先看待發送與失敗,避免主推商品斷貨拖累業績",
"templates/vendor_stockout_send_email_v2.html": "先處理失敗通知,讓補貨協調不中斷",
"templates/vendor_stockout_vendor_management_v2.html": "維護供應商與收件人,讓缺貨補救能快速送達",
"templates/vendor_stockout_history_v2.html": "回看缺貨處理紀錄,找出需要補救的供貨風險",
}
for path, marker in expected.items():
assert marker in Path(path).read_text(encoding="utf-8")
def test_homepage_next_action_cta_keeps_visible_primary_contrast():
from pathlib import Path
template = Path("templates/ai_intelligence.html").read_text(encoding="utf-8")
assert "#commandTaskButton.growth-command-alert-action" in template
assert "background-color: #8f442b !important" in template
assert "color: #fff !important" in template
assert "width: min(100%, 980px)" in template
def test_secondary_and_governance_pages_keep_growth_decision_copy_concise():
from pathlib import Path
expected = {
"templates/dashboard.html": "先回業績成長指揮台",
"templates/edm_dashboard.html": "決定主推、補貨與曝光位置",
"templates/crawler_management.html": "讓比價與業績判斷保持新鮮",
"templates/history.html": "補救會影響主推商品的供貨風險",
"templates/import.html": "讓分析、建議與解法有可靠來源",
"templates/send_email.html": "避免補貨協調中斷",
"templates/settings.html": "守住比價資料新鮮度",
"templates/system_settings.html": "先補齊業績與備份",
"templates/admin/agent_orchestration.html": "是否支撐業績決策",
"templates/admin/ai_calls_dashboard.html": "支援業績判斷",
"templates/admin/budget.html": "留給能推動業績的任務",
"templates/admin/host_health.html": "避免 AI 建議與比價流程中斷",
"templates/admin/rag_queries.html": "避免業績建議缺少根據",
"templates/admin/quality_trend.html": "AI 建議是否可靠",
"templates/admin/promotion_review.html": "避免錯誤知識污染業績建議",
"templates/admin/business_intel.html": "追蹤閉環結果與競品訊號",
}
forbidden = [
"舊版 dashboard.html 已停用",
"舊版 edm_dashboard.html 已停用",
"舊版 crawler_management.html 已停用",
"舊版 history.html 已停用",
"舊版 import.html 已停用",
"舊版 send_email.html 已停用",
"這頁回答",
"這頁是 AI 中樞",
"這頁是 RAG",
"這裡不是流水帳",
"這裡追蹤每次 RAG",
"這裡看 AI 的回答",
"資料列表",
]
for path, marker in expected.items():
text = Path(path).read_text(encoding="utf-8")
assert marker in text
for bad in forbidden:
assert bad not in text
def test_help_empty_login_and_ppt_copy_are_action_oriented():
from pathlib import Path
expected = {
"templates/ai_recommend.html": [
"銷售動作",
"把商品證據轉成可追蹤的銷售動作",
"回到業績成長指揮台追蹤是否帶動商品處理",
],
"templates/ai_intelligence.html": [
"先整理業績、比價與資料狀態,再決定下一步",
"先檢查備援資料品質",
],
"templates/daily_sales.html": ["才能判斷下滑與價差壓力"],
"templates/growth_analysis.html": ["先匯入月度業績資料再評估成長、AOV 與毛利缺口"],
"templates/index.html": ["缺貨處理順序", "避免主推商品斷貨"],
"templates/login.html": ["登入後先看 PChome 業績、價差、缺貨與 AI 建議"],
"templates/system_settings.html": ["會更新對應年月份的業績資料"],
"templates/admin/ppt_audit_history.html": [
"先確認簡報可預覽、可審核,再把問題交給修復流程",
"先補齊視覺 QA 執行條件,再判斷簡報品質",
"先補排程或手動產出,再進行視覺 QA",
"先補視覺 QA 條件",
],
}
forbidden = [
"使用說明",
"這裡會顯示最適合的下一步",
"再用這裡檢查",
"這裡會顯示背景任務狀態",
"目前查無本月 DB 產出紀錄",
"為什麼這頁空",
"目前不是模型能力問題",
"大量資料可能需要較長時間",
]
for path, markers in expected.items():
text = Path(path).read_text(encoding="utf-8")
for marker in markers:
assert marker in text
for bad in forbidden:
assert bad not in text
def test_edge_tool_pages_keep_growth_decision_copy_concise():
from pathlib import Path
expected = {
"templates/ai_history.html": ["回收有效文案", "尚無可回收文案", "產生銷售動作"],
"templates/pchome_crawler.html": ["補齊 PChome 商品資料"],
"templates/trends.html": ["用外部趨勢挑選可主推商品"],
"templates/user_management.html": ["守住帳號權限"],
"templates/sales_analysis.html": ["直接看影響業績的圖表、分類與商品明細"],
"templates/settings.html": ["補齊比價來源"],
"templates/admin/ppt_audit_history.html": ["修復完成後回報處理結果", "可直接派工"],
"templates/external_tool_status.html": ["先回正式工作流", "避免決策資料分散", "可用正式入口"],
"templates/market_intel/disabled.html": ["市場情報尚未進入正式決策", "正式操作入口", "先處理候選同款"],
}
forbidden = [
"查看、管理和複用資料庫內已生成的文案與操作紀錄",
"尚無生成記錄",
"爬取 PChome 24h 商品資料",
"Google News、PTT、Dcard、YouTube 趨勢訊號",
"管理系統用戶帳號與權限",
"入口已由 momo-pro 接管",
"建議操作入口",
"請輸入完整的 MOMO 商城分類網址",
"選擇任一條件後,系統將自動載入並顯示詳細的圖表與數據分析",
]
for path, markers in expected.items():
text = Path(path).read_text(encoding="utf-8")
for marker in markers:
assert marker in text
for bad in forbidden:
assert bad not in text
def test_governance_and_low_frequency_pages_avoid_engineering_status_copy():
from pathlib import Path
expected = {
"templates/components/_legacy_bridge_panel.html": ["頁面狀態", "已整併", "業績流程可用"],
"templates/403.html": ["權限守門", "未授權操作影響營運資料", "權限控管"],
"templates/maintenance.html": ["服務維護", "確認業績、比價與匯入狀態", "台北時間"],
"templates/auto_import_index.html": [
"更新日報、成長分析與今日作戰清單",
"送出後更新日報、成長分析與今日作戰清單",
"作戰清單保持新鮮",
"共更新",
"buildImportActionHint",
"重新確認 Google Drive 授權",
"改用當日業績明細檔",
"重新匯入最新檔案",
],
"templates/settings.html": ["比價來源同步", "補齊 MOMO 參考來源"],
"templates/system_settings.html": [
"營運資料備份",
"可回復的業績與設定狀態",
"toImportActionMessage",
"匯入沒有完成",
"業績資料處理未完整完成",
],
"templates/ai_recommend.html": ["值得主推、調價或補比價", "回到 PChome 銷售動作"],
"templates/notification_templates.html": ["商品、風險與下一步"],
"templates/vendor_stockout_index_v2.html": ["處理缺貨", "主推商品供貨風險"],
"templates/vendor_stockout_vendor_management_v2.html": ["正確窗口"],
"templates/login_history.html": ["避免未授權操作影響業績流程", "裝置資訊"],
"templates/logs.html": ["錯誤", "注意", "資訊"],
"templates/ai_automation_smoke.html": ["AI 閉環守門", "支援業績流程"],
"templates/external_tool_status.html": ["共用預覽"],
"templates/market_intel/disabled.html": ["市場情報", "操作入口"],
"templates/brand_assets.html": ["EwoooC 品牌資產庫", "品牌資產混用"],
"templates/code_review.html": ["部署守門與程式碼審查", "業績流程上線前檢查", "上線證據"],
}
forbidden = [
"工作視窗",
"Codex",
"Claude",
"推到 Gitea",
"本輪已完成",
"剛剛修正",
"後續 session",
"MIGRATED PAGE",
"V3 READY",
"Growth workflow active",
"ACCESS CONTROL",
"403 / Forbidden",
"SYSTEM MAINTENANCE",
"Asia Taipei",
"VENDOR OPERATIONS",
"SYSTEM OPERATIONS",
"MIGRATED DASHBOARD",
"MIGRATED CAMPAIGN",
"Legacy guard",
"資料將會",
"無需重啟系統",
"完整備份",
"打包所有程式碼與資料庫",
"輸入商品名稱後點擊",
"點擊「預覽」按鈕查看效果",
"目前沒有匯入的缺貨資料",
"提示:可以從 Excel",
"系統登入記錄與異常嘗試追蹤",
"User Agent",
"四 Agent 控制面",
"不會影響 DB",
"Live Plugin",
"MARKET INTEL",
"OPERATIONS",
"WOOO TECH",
"Main Logo",
"Glass Version",
"Gradient Version",
"AI 程式碼審查",
"Google Drive →",
"匯入資料庫",
"realtime_sales_monthly",
"daily_sales_snapshot",
"資料落點",
"檢查日誌",
"發生系統錯誤",
"刪除雲端原檔",
"資料表:",
"資料表:",
"vendor_stockout / vendor_list",
"vendor_id {{",
"email_send_log",
"模板代碼",
]
legacy_paths = [
"templates/dashboard.html",
"templates/edm_dashboard.html",
"templates/send_email.html",
"templates/history.html",
"templates/import.html",
"templates/crawler_management.html",
"web/templates/vendor_stockout/send_email.html",
"web/templates/vendor_stockout/index.html",
"web/templates/vendor_stockout/list.html",
"web/templates/vendor_stockout/history.html",
"web/templates/vendor_stockout/import.html",
"web/templates/vendor_stockout/vendor_management.html",
]
for path, markers in expected.items():
text = Path(path).read_text(encoding="utf-8")
for marker in markers:
assert marker in text
for bad in forbidden:
assert bad not in text
for path in legacy_paths:
text = Path(path).read_text(encoding="utf-8")
assert "已整併" in text
for bad in forbidden:
assert bad not in text
def test_visible_operations_pages_hide_internal_runtime_terms():
from pathlib import Path
expected = {
"templates/ai_recommend.html": ["銷售動作生成", "建議目的", "處理順序"],
"templates/vendor_stockout_index_v2.html": ["先匯入缺貨批次", "供貨風險"],
"templates/dashboard_v2.html": ["尚無挑品建議", "先累積 PChome 比價與挑品資料"],
"templates/daily_sales.html": ["左右滑動看業績趨勢", "左右滑動看分類明細"],
"templates/sales_analysis.html": ["分析下一步", "選類別看貢獻", "適合檔期與活動回顧"],
"templates/vendor_stockout_vendor_management_v2.html": ["匯入供應商窗口名單", "確認窗口清單"],
"templates/vendor_stockout_import_v2.html": ["會先停止匯入", "處理缺貨清單"],
"templates/admin/ppt_audit_history.html": ["產出紀錄", "最近產出", "保存紀錄"],
"templates/admin/agent_orchestration.html": ["AI 分工指揮台", "建議路徑、工具與知識命中矩陣", "工具協作明細", "工具協作 × 使用情境"],
"templates/admin/ai_calls_dashboard.html": ["用量", "最近作戰素材", "情境 × 知識命中矩陣"],
"templates/admin/observability_overview.html": ["用量", "知識與工具矩陣"],
"templates/cicd_dashboard.html": ["最新更新流程", "更新歷史", "修復服務", "查看更新紀錄"],
"templates/admin/budget.html": ["Top 5 成本使用情境", "尚未建立預算線"],
"templates/ai_automation_smoke.html": ["下載檢查紀錄", "健康檢查服務"],
"templates/login.html": ["安全登入", "工作階段", "資料服務"],
"templates/code_review.html": ["上線證據", "已收到上線檢查資料"],
}
forbidden_by_path = {
"templates/ai_recommend.html": ["權杖:", "AI 模型", "分析模型", "AI 路徑", "Gemini 備援", "Ollama 主路徑"],
"templates/vendor_stockout_index_v2.html": ["資料庫目前沒有缺貨資料"],
"templates/system_settings.html": ["資料表:", "資料表:", "自動建表", "匯入並建立通用資料表"],
"templates/notification_templates.html": ["模板代碼", "<code>${t.code}</code>", "CI/CD Pipeline SUCCESS"],
"templates/dashboard_v2.html": ["尚無 AI 挑品", "挑品 Agent"],
"templates/daily_sales.html": ["左右滑動查看完整圖表", "左右滑動查看完整列表"],
"templates/sales_analysis.html": ["提示:</strong>選擇條件", "點擊類別查看詳情", "點擊篩選此廠商商品"],
"templates/vendor_stockout_vendor_management_v2.html": ["拖曳檔案到此處或點擊選擇檔案", "查看廠商清單"],
"templates/vendor_stockout_import_v2.html": ["API 會拒絕匯入", "查看缺貨清單"],
"templates/admin/ppt_audit_history.html": ["DB 紀錄", "DB / 預覽", "寫入 DB", "DB 產出紀錄", "資料庫快取", "本月尚無 DB"],
"templates/admin/agent_orchestration.html": [
"Agent 指揮矩陣",
"四 Agent 矩陣",
"LLM × MCP × RAG 編排矩陣",
"權杖",
"<code>{{ m.caller }}</code>",
"<code>{{ m.server }}</code>",
"呼叫端工作量",
],
"templates/admin/ai_calls_dashboard.html": ["權杖量", "權杖/次", ">權杖<", "Agent 上下文", "RAG × MCP"],
"templates/admin/observability_overview.html": ["權杖量", "RAG × MCP"],
"templates/cicd_dashboard.html": ["Pipeline Flow", "Pipeline History", "完整修復", "一鍵修復", "重啟 Registry", "舊叢集"],
"templates/admin/budget.html": ["燒錢呼叫端", "migrations/025", "<code>{{ c.caller }}</code>"],
"templates/ai_automation_smoke.html": ["JSONL", "健康檢查 API", "JSON.stringify(item.details"],
"templates/login.html": ["CSRF 防護", "Session 2h", "PostgreSQL"],
"templates/code_review.html": ["<b>提交</b>", "<b>分支</b>", "提交 ${h.commit_sha}", "🌿 ${h.branch}", "<code>${state.commit_sha"],
}
for path, markers in expected.items():
text = Path(path).read_text(encoding="utf-8")
for marker in markers:
assert marker in text
for bad in forbidden_by_path.get(path, []):
assert bad not in text