feat(pchome): close freshness and multi-market decision gaps
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -60,7 +60,7 @@ def test_daily_sales_import_fails_when_monthly_sync_fails(monkeypatch, tmp_path)
|
||||
|
||||
source_df = pd.DataFrame([
|
||||
{
|
||||
"日期": "2026-06-24",
|
||||
"日期": pd.Timestamp.today().strftime("%Y-%m-%d"),
|
||||
"商品ID": "A001",
|
||||
"商品名稱": "測試商品",
|
||||
"銷售金額": 1200,
|
||||
@@ -349,4 +349,41 @@ def test_auto_import_empty_drive_folder_remains_success(monkeypatch, tmp_path):
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["file_count"] == 0
|
||||
assert result["status"] == "upstream_missing"
|
||||
assert result["decision_ready"] is False
|
||||
assert result["message"] == "沒有找到待匯入的檔案"
|
||||
|
||||
|
||||
def test_auto_import_empty_drive_exposes_stale_upstream_as_degraded(monkeypatch, tmp_path):
|
||||
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
import_service.Base.metadata.create_all(import_service.engine)
|
||||
with import_service.engine.begin() as conn:
|
||||
conn.execute(text("CREATE TABLE IF NOT EXISTS daily_sales_snapshot (snapshot_date TEXT)"))
|
||||
conn.execute(text("INSERT INTO daily_sales_snapshot (snapshot_date) VALUES ('2020-01-01')"))
|
||||
|
||||
class FakeDriveService:
|
||||
last_error_kind = None
|
||||
last_error = None
|
||||
|
||||
def list_files_in_folder(self, folder_path, file_pattern):
|
||||
return []
|
||||
|
||||
import services.openclaw_strategist_service as strategist
|
||||
|
||||
alerts = []
|
||||
monkeypatch.setattr(import_service, "drive_service", FakeDriveService())
|
||||
monkeypatch.setattr(
|
||||
strategist,
|
||||
"_send_data_stale_alert",
|
||||
lambda **kwargs: alerts.append(kwargs),
|
||||
)
|
||||
|
||||
result = import_service.ImportService().auto_import_from_drive()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["status"] == "upstream_stale"
|
||||
assert result["decision_ready"] is False
|
||||
assert result["requires_upstream_acquisition"] is True
|
||||
assert result["latest_sales_date"] == "2020-01-01"
|
||||
assert result["data_lag_days"] >= 3
|
||||
assert alerts and alerts[0]["report_type"] == "upstream_drive"
|
||||
|
||||
@@ -133,15 +133,15 @@ def test_dashboard_match_status_shows_manual_review_closure_states():
|
||||
"best_match_score": 0.68,
|
||||
})
|
||||
|
||||
assert rejected["label"] == "人工已否決"
|
||||
assert rejected["label"] == "AI 已排除"
|
||||
assert "跳過正式價差寫入" in rejected["summary"]
|
||||
assert unit_price["label"] == "人工標記單位價"
|
||||
assert unit_price["label"] == "AI 標記單位價"
|
||||
assert "總價不可直接比較" in unit_price["summary"]
|
||||
assert needs_research["label"] == "人工要求補搜尋"
|
||||
assert needs_research["label"] == "AI 要求補搜尋"
|
||||
assert "重新抓取" in needs_research["summary"]
|
||||
|
||||
decision = _build_competitor_decision(980, 899, match_status=rejected)
|
||||
assert decision["label"] == "人工已否決"
|
||||
assert decision["label"] == "AI 已排除"
|
||||
assert decision["gap_amount"] is None
|
||||
|
||||
|
||||
@@ -154,9 +154,9 @@ def test_dashboard_match_status_shows_rescore_accepted_review_state():
|
||||
"error_message": "matcher_rescore=accepted_current; reasons=strong_exact_spec_match",
|
||||
})
|
||||
|
||||
assert status["label"] == "重算待人工覆核"
|
||||
assert status["label"] == "重算待 AI 決策"
|
||||
assert status["tone"] == "watch"
|
||||
assert "人工確認身份後才可寫入正式 PChome 價差" in status["summary"]
|
||||
assert "AI 決策信封確認身份" in status["summary"]
|
||||
|
||||
|
||||
def test_dashboard_match_status_uses_specific_matcher_reason_labels():
|
||||
@@ -175,8 +175,8 @@ def test_dashboard_match_status_uses_specific_matcher_reason_labels():
|
||||
|
||||
assert finish_gap["label"] == "妝效質地不同"
|
||||
assert "妝效質地不同" in finish_gap["summary"]
|
||||
assert variant_review["label"] == "多款任選待確認"
|
||||
assert "需人工確認" in variant_review["summary"]
|
||||
assert variant_review["label"] == "多款任選待決策"
|
||||
assert "需 AI 例外決策" in variant_review["summary"]
|
||||
|
||||
|
||||
def test_dashboard_match_status_uses_focused_marketplace_reason_labels():
|
||||
|
||||
@@ -184,12 +184,11 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "\"action_label\"" in source
|
||||
assert "build_unit_price_comparison" in source
|
||||
assert "需單位價覆核" in daily_template
|
||||
assert "人工採用" in daily_template
|
||||
assert "人工否決" in daily_template
|
||||
assert "人工單位價" in daily_template
|
||||
assert "AI 採用" in daily_template
|
||||
assert "AI 排除" in daily_template
|
||||
assert "AI 單位價" in daily_template
|
||||
assert "competitor_intel.review_queue" in daily_template
|
||||
assert "coverage.fresh_matches" in growth_template
|
||||
assert "coverage.fresh_match_rate" in growth_template
|
||||
assert "coverage.match_rate" in growth_template
|
||||
assert "coverage.decision_ready_rate" in growth_template
|
||||
assert "coverage.decision_support_rate" in growth_template
|
||||
assert "coverage.catalog_comparable_count" in growth_template
|
||||
@@ -199,12 +198,12 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "未形成有效身份配對" in growth_template
|
||||
assert "coverage.unit_comparable_count" in growth_template
|
||||
assert "coverage.rescore_accepted_count" in growth_template
|
||||
assert "重算待人工覆核" in growth_template
|
||||
assert "重算待 AI 決策" in growth_template
|
||||
assert "coverage.manual_accept_count" in growth_template
|
||||
assert "coverage.manual_reject_count" in growth_template
|
||||
assert "coverage.manual_unit_price_count" in growth_template
|
||||
assert "comp_coverage.rescore_accepted_count" in daily_template
|
||||
assert "重算待人工覆核" in daily_template
|
||||
assert "重算待 AI 決策" in daily_template
|
||||
assert "comp_coverage.decision_support_rate" in daily_template
|
||||
assert "comp_coverage.catalog_comparable_count" in daily_template
|
||||
assert "精準可告警覆蓋" in daily_template
|
||||
@@ -364,7 +363,8 @@ def test_competitor_review_reasons_prefer_json_payload_labels():
|
||||
assert envelope["identity_evidence"]["brand"]["shared"] == ["mac"]
|
||||
assert envelope["offer_evidence"]["gap_pct"] == 12.5
|
||||
assert envelope["difference_highlights"][0]["dimension"] == "妝效/質地不同"
|
||||
assert envelope["recommended_action"]["requires_hitl"] is True
|
||||
assert envelope["recommended_action"]["requires_hitl"] is False
|
||||
assert envelope["recommended_action"]["requires_ai_exception"] is True
|
||||
assert envelope["recommended_action"]["action"] == "verify_or_reject_identity"
|
||||
assert any(evidence["metric"] == "reasons" for evidence in envelope["evidence"])
|
||||
assert any(evidence["metric"] == "identity_evidence" for evidence in envelope["evidence"])
|
||||
@@ -502,12 +502,13 @@ def test_review_decision_brief_is_shared_by_openclaw_and_ppt():
|
||||
|
||||
brief = summarize_review_decision_envelopes([item], limit=5)
|
||||
|
||||
assert brief["hitl_count"] == 1
|
||||
assert brief["hitl_count"] == 0
|
||||
assert brief["ai_exception_count"] == 1
|
||||
assert brief["auto_execute_blocked_count"] == 1
|
||||
assert brief["severity_counts"]
|
||||
assert brief["data_quality_counts"] == {"complete": 1}
|
||||
assert "人工確認身份後採用同款" in brief["text"]
|
||||
assert "HITL" in brief["text"]
|
||||
assert "AI 確認身份後採用同款" in brief["text"]
|
||||
assert "AI例外" in brief["text"]
|
||||
assert "DDAO4C-A79050612" in brief["text"]
|
||||
|
||||
repo_source = (ROOT / "services" / "competitor_intel_repository.py").read_text(encoding="utf-8")
|
||||
@@ -518,11 +519,11 @@ def test_review_decision_brief_is_shared_by_openclaw_and_ppt():
|
||||
assert "\"review_decision_brief\": summarize_review_decision_envelopes" in repo_source
|
||||
assert "summarize_review_decision_envelopes" in openclaw_source
|
||||
assert "review_decision_text" in openclaw_source
|
||||
assert "PChome 覆核決策信封(HITL,不可自動寫正式價差)" in openclaw_source
|
||||
assert "PChome AI 決策信封(no-write receipt 前不寫正式價差)" in openclaw_source
|
||||
assert "SUM(CASE WHEN attempt_status IN ('unit_comparable'" not in openclaw_source
|
||||
assert "review_decision_brief" in bot_source
|
||||
assert "覆核決策信封(HITL,不可自動寫正式價差)" in bot_source
|
||||
assert "覆核決策信封(HITL)" in ppt_source
|
||||
assert "AI 決策信封(no-write receipt 前不寫正式價差)" in bot_source
|
||||
assert "AI 決策信封" in ppt_source
|
||||
|
||||
|
||||
def test_competitor_ppt_prompt_uses_neutral_ewooc_viewpoint():
|
||||
|
||||
@@ -146,7 +146,7 @@ def test_competitor_db_evidence_actions_are_concrete():
|
||||
)
|
||||
|
||||
assert actions == [
|
||||
"[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.84|建議人工確認 PChome identity_v2 後評估跟價或促銷|PChome D123456"
|
||||
"[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.84|建議 AI 自動驗證確認 PChome identity_v2 後評估跟價或促銷|PChome D123456"
|
||||
]
|
||||
|
||||
|
||||
@@ -279,9 +279,10 @@ def test_price_review_decision_envelope_blocks_auto_execution():
|
||||
|
||||
assert envelope["decision_type"] == "price_decision_review"
|
||||
assert envelope["source_agent"] == "elephant_alpha"
|
||||
assert envelope["recommended_action"]["requires_hitl"] is True
|
||||
assert envelope["recommended_action"]["requires_hitl"] is False
|
||||
assert envelope["recommended_action"]["requires_ai_exception"] is True
|
||||
assert envelope["guardrails"]["can_auto_execute"] is False
|
||||
assert envelope["guardrails"]["blocked_reason"] == "price decisions require HITL; execution_plan skipped"
|
||||
assert envelope["guardrails"]["blocked_reason"] == "price decisions require AI 例外決策; execution_plan skipped"
|
||||
assert envelope["guardrails"]["data_quality"] == "complete"
|
||||
|
||||
|
||||
@@ -701,7 +702,8 @@ def test_human_escalation_decision_envelope_blocks_auto_execution():
|
||||
assert envelope["decision_type"] == "ea_escalation"
|
||||
assert envelope["guardrails"]["can_auto_execute"] is False
|
||||
assert envelope["guardrails"]["data_quality"] == "partial"
|
||||
assert envelope["recommended_action"]["requires_hitl"] is True
|
||||
assert envelope["recommended_action"]["requires_hitl"] is False
|
||||
assert envelope["recommended_action"]["requires_ai_exception"] is True
|
||||
assert envelope["trace"]["provider"] == "elephant_alpha"
|
||||
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ def test_dispatch_decision_envelope_skips_ai_handler(monkeypatch):
|
||||
assert "🎯 <b>標的</b>" in message
|
||||
assert "📊 <b>價格證據</b>" in message
|
||||
assert "🧩 <b>比對證據</b>" in message
|
||||
assert "✅ <b>人工下一步</b>" in message
|
||||
assert "✅ <b>AI 例外下一步</b>" in message
|
||||
assert "🚦 <b>通知分級</b>" in message
|
||||
assert "直接價格威脅" in message
|
||||
assert "高信心同款 / 總價可比 / 可直接價格告警" in message
|
||||
|
||||
@@ -21,6 +21,8 @@ def test_connector_contract_lists_mainstream_sources_without_auto_alerts():
|
||||
"line_shopping",
|
||||
"rakuten",
|
||||
"yahoo_shopping",
|
||||
"etmall",
|
||||
"friday",
|
||||
"ruten",
|
||||
"shopify_brand_store",
|
||||
"meta_commerce",
|
||||
@@ -91,7 +93,7 @@ def test_external_offer_csv_dry_run_accepts_chinese_headers_and_classifies_rows(
|
||||
}
|
||||
rows = payload["rows"]
|
||||
assert rows[0]["status_label"] == "可使用"
|
||||
assert rows[1]["status_label"] == "需人工確認"
|
||||
assert rows[1]["status_label"] == "需 AI 自動驗證確認"
|
||||
assert "這個來源目前先暫停,不進告警" in rows[1]["reasons"]
|
||||
assert rows[2]["status_label"] == "不能使用"
|
||||
assert "缺少售價" in rows[2]["reasons"]
|
||||
@@ -117,7 +119,7 @@ def test_external_offer_csv_dry_run_accepts_plain_business_headers_and_values():
|
||||
assert rows[0]["source_code"] == "momo_reference"
|
||||
assert rows[0]["status_label"] == "可使用"
|
||||
assert rows[1]["source_code"] == "momo_reference"
|
||||
assert rows[1]["status_label"] == "需人工確認"
|
||||
assert rows[1]["status_label"] == "需 AI 自動驗證確認"
|
||||
assert "尚未確認同款" in rows[1]["reasons"]
|
||||
assert "缺少 PChome 商品 ID,無法連到業績" in rows[1]["reasons"]
|
||||
|
||||
@@ -377,7 +379,7 @@ def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeyp
|
||||
assert payload["success"] is True
|
||||
assert payload["candidate_count"] == 1
|
||||
assert payload["written_count"] == 0
|
||||
assert payload["skipped_reasons"] == {"候選仍需人工確認": 1}
|
||||
assert payload["skipped_reasons"] == {"候選仍需 AI 自動驗證確認": 1}
|
||||
assert stale_marks == []
|
||||
with engine.connect() as conn:
|
||||
count = conn.execute(text("SELECT COUNT(*) FROM external_offers")).scalar()
|
||||
@@ -424,7 +426,7 @@ def test_sync_targeted_momo_review_candidates_writes_needs_review_offer(monkeypa
|
||||
assert payload["status"] == "synced"
|
||||
assert payload["candidate_count"] == 2
|
||||
assert payload["written_count"] == 1
|
||||
assert payload["skipped_reasons"] == {"人工確認候選分數過低": 1}
|
||||
assert payload["skipped_reasons"] == {"AI 自動驗證確認候選分數過低": 1}
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(text("""
|
||||
@@ -608,7 +610,55 @@ def test_external_source_readiness_uses_legacy_momo_reference_cache():
|
||||
assert sources["tiktok_shop"]["plain_state"] == "先保留接口,不進告警"
|
||||
assert sources["line_shopping"]["plain_state"] == "先保留接口,不進告警"
|
||||
assert sources["shopify_brand_store"]["plain_state"] == "先保留接口,不進告警"
|
||||
assert payload["plain_summary"] == "MOMO 先用;其他主流平台已列管,未接合法穩定來源前不進告警。"
|
||||
assert payload["sources_with_data_count"] == 1
|
||||
assert payload["source_coverage_rate"] > 0
|
||||
assert "只有 runtime 已啟用" in payload["plain_summary"]
|
||||
|
||||
|
||||
def test_source_seeds_preserve_runtime_activation_and_readiness_uses_it():
|
||||
from services import external_market_offer_service as service
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
_seed_external_offer_sync_tables(engine)
|
||||
|
||||
with engine.begin() as conn:
|
||||
service._ensure_external_market_source_seeds(conn)
|
||||
conn.execute(text("""
|
||||
UPDATE external_market_sources
|
||||
SET status = 'active', enabled = 1
|
||||
WHERE code = 'shopee'
|
||||
"""))
|
||||
service._ensure_external_market_source_seeds(conn)
|
||||
source = conn.execute(text("""
|
||||
SELECT status, enabled
|
||||
FROM external_market_sources
|
||||
WHERE code = 'shopee'
|
||||
""")).mappings().one()
|
||||
|
||||
assert source["status"] == "active"
|
||||
assert bool(source["enabled"]) is True
|
||||
|
||||
|
||||
def test_source_aliases_normalize_pixelrag_platform_codes():
|
||||
from services.external_market_offer_service import normalize_external_offer_payload
|
||||
|
||||
base = {
|
||||
"source_product_id": "SKU-1",
|
||||
"title": "測試商品",
|
||||
"price": 100,
|
||||
"observed_at": "2026-07-10T10:00:00",
|
||||
"ingestion_method": "provider_api",
|
||||
}
|
||||
expected = {
|
||||
"yahoo_shopping_tw": "yahoo_shopping",
|
||||
"etmall_tw": "etmall",
|
||||
"friday_tw": "friday",
|
||||
"rakuten_tw": "rakuten",
|
||||
}
|
||||
for raw, normalized in expected.items():
|
||||
record, errors = normalize_external_offer_payload({**base, "source_code": raw})
|
||||
assert errors == []
|
||||
assert record.source_code == normalized
|
||||
|
||||
|
||||
def test_external_market_migration_creates_source_and_offer_tables():
|
||||
|
||||
@@ -245,6 +245,7 @@ def test_growth_workflow_pages_hide_raw_export_and_fallback_content():
|
||||
|
||||
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")
|
||||
aliases = (ROOT / "web/static/css/ewoooc-tokens-v2-alias.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
|
||||
@@ -256,6 +257,8 @@ def test_frontend_v2_syncs_latest_momo_pro_prototype_tokens_and_shell():
|
||||
assert "--momo-border-strong" in tokens
|
||||
assert "--momo-sidebar-width" in tokens
|
||||
assert ".momo-app button:not(.btn):not(.btn-close)" in tokens
|
||||
assert "--momo-warm-caramel: var(--momo-page-accent)" not in aliases
|
||||
assert "--momo-warm-caramel: #c96442" in aliases
|
||||
|
||||
assert "background: #1f1a14;" in shell
|
||||
assert "rgba(250, 247, 240, 0.08)" in shell
|
||||
|
||||
@@ -882,7 +882,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
|
||||
assert "市場情報尚未進入正式決策" in template
|
||||
assert "資料狀態" in template
|
||||
assert "來源規格" in template
|
||||
assert "手動整理" in template
|
||||
assert "AI 受控整理" in template
|
||||
assert "data-market-intel-preview" not in template
|
||||
assert "data-market-intel-writer" not in template
|
||||
assert "/api/market_intel/" not in template
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_targeted_momo_search_keeps_identity_review_candidates_manual(monkeypatc
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert "需人工確認 1 筆" in message
|
||||
assert "需 AI 自動驗證確認 1 筆" in message
|
||||
assert products[0]["auto_compare_type"] == "manual_review"
|
||||
assert products[0]["can_auto_compare"] is False
|
||||
assert products[0]["target_price_basis"] == "none"
|
||||
|
||||
@@ -37,7 +37,8 @@ def test_price_decision_envelope_has_hilt_guardrails():
|
||||
assert envelope["guardrails"]["match_type"] == "exact"
|
||||
assert envelope["guardrails"]["price_basis"] == "total_price"
|
||||
assert envelope["guardrails"]["alert_tier"] == "price_alert_exact"
|
||||
assert envelope["recommended_action"]["requires_hitl"] is True
|
||||
assert envelope["recommended_action"]["requires_hitl"] is False
|
||||
assert envelope["recommended_action"]["requires_ai_exception"] is True
|
||||
assert envelope["expected_impact"]["revenue_loss_7d"] == 65000
|
||||
assert envelope["expected_impact"]["gap_amount"] == 201
|
||||
assert envelope["expected_impact"]["momo_price"] == 1200
|
||||
|
||||
@@ -158,7 +158,7 @@ class TestKPIComputation:
|
||||
assert "fetch_competitor_coverage" in source
|
||||
assert "summarize_review_decision_envelopes" in source
|
||||
assert "review_decision_text" in source
|
||||
assert "重算待人工覆核" in source
|
||||
assert "重算待 AI 決策" in source
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -129,6 +129,59 @@ def _seed_growth_unit_price_external_offer(engine):
|
||||
"""))
|
||||
|
||||
|
||||
def _seed_growth_multiplatform_external_offers(engine):
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE external_market_sources (
|
||||
code TEXT PRIMARY KEY,
|
||||
status TEXT,
|
||||
enabled BOOLEAN
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE external_offers (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_code TEXT,
|
||||
platform_code TEXT,
|
||||
source_product_id 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_market_sources (code, status, enabled)
|
||||
VALUES ('momo_reference', 'active', 1), ('yahoo_shopping', 'active', 1)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT INTO external_offers (
|
||||
id, source_code, platform_code, source_product_id, 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-1', 'MOMO 商品', 'https://momo.test/1',
|
||||
870, '2026-07-10 10:00:00', NULL, 'official_api', 'PCH-1', 'MOMO-1',
|
||||
'verified', 92, 'verified', '[]',
|
||||
'{"pchome_public_price": 1000, "pchome_public_name": "PChome 商品"}'),
|
||||
(2, 'yahoo_shopping', 'yahoo_shopping', 'YAHOO-1', 'Yahoo 商品', 'https://yahoo.test/1',
|
||||
800, '2026-07-10 11:00:00', NULL, 'official_api', 'PCH-1', 'YAHOO-1',
|
||||
'verified', 90, 'verified', '[]',
|
||||
'{"pchome_public_price": 1000, "pchome_public_name": "PChome 商品"}')
|
||||
"""))
|
||||
|
||||
|
||||
def _seed_growth_review_candidate_offer(engine):
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
@@ -185,7 +238,9 @@ def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang
|
||||
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"] == ["蝦皮", "酷澎"]
|
||||
assert {"蝦皮", "酷澎", "東森購物", "friDay 購物"}.issubset(
|
||||
set(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"] == "正在使用"
|
||||
@@ -194,6 +249,9 @@ def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang
|
||||
assert payload["stats"]["candidate_count"] == 2
|
||||
assert payload["stats"]["mapped_count"] == 1
|
||||
assert payload["stats"]["needs_mapping_count"] == 1
|
||||
assert payload["stats"]["catalog_product_count"] == 2
|
||||
assert payload["stats"]["mapped_product_count"] == 1
|
||||
assert payload["stats"]["sales_freshness"]["status"] == "critical"
|
||||
|
||||
actions = {item["pchome_product_id"]: item["recommended_action"]["label"] for item in payload["opportunities"]}
|
||||
assert actions["PCH-1"] == "檢查售價與活動"
|
||||
@@ -272,6 +330,41 @@ def test_pchome_growth_understands_unit_price_external_offers():
|
||||
assert any("單位價" in line for line in pchome_1["reason_lines"])
|
||||
|
||||
|
||||
def test_pchome_growth_compares_all_enabled_platforms_and_keeps_evidence():
|
||||
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
_seed_growth_tables(engine)
|
||||
_seed_growth_multiplatform_external_offers(engine)
|
||||
|
||||
payload = build_pchome_growth_opportunities(engine, limit=5)
|
||||
|
||||
pchome_1 = next(item for item in payload["opportunities"] if item["pchome_product_id"] == "PCH-1")
|
||||
assert pchome_1["external_price"]["source_code"] == "yahoo_shopping"
|
||||
assert pchome_1["external_price"]["source"] == "Yahoo 購物"
|
||||
assert pchome_1["external_price"]["momo_price"] == 800
|
||||
assert {offer["source_code"] for offer in pchome_1["market_offers"]} == {
|
||||
"momo_reference",
|
||||
"yahoo_shopping",
|
||||
}
|
||||
assert pchome_1["cross_platform"]["decision_ready_platform_count"] == 2
|
||||
assert payload["stats"]["external_platform_counts"] == {
|
||||
"Yahoo 購物": 1,
|
||||
"MOMO 外部價格參考": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_sales_freshness_blocks_decisions_after_two_days():
|
||||
from datetime import date
|
||||
from services.pchome_revenue_growth_service import _sales_freshness
|
||||
|
||||
assert _sales_freshness("2026-07-09", today=date(2026, 7, 10))["decision_ready"] is True
|
||||
stale = _sales_freshness("2026-07-07", today=date(2026, 7, 10))
|
||||
assert stale["status"] == "critical"
|
||||
assert stale["age_days"] == 3
|
||||
assert stale["decision_ready"] is False
|
||||
|
||||
|
||||
def test_pchome_growth_route_cache_respects_shared_invalidation_epoch(monkeypatch):
|
||||
from routes import ai_routes as routes
|
||||
|
||||
@@ -491,7 +584,9 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
|
||||
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 "sources.map((source)" in template
|
||||
assert "visibleSources.map((source)" in template
|
||||
assert "source.has_runtime_data" in template
|
||||
assert "其他平台" in template
|
||||
assert "MOMO 待確認候選" in template
|
||||
assert "確認同款" in template
|
||||
assert "不是同款" in template
|
||||
@@ -542,7 +637,7 @@ def test_growth_analysis_uses_actionable_price_command_panel():
|
||||
assert "ga-chart-card__body--command" in template
|
||||
assert "ga-competitor-command" in template
|
||||
assert "可直接決策" in template
|
||||
assert "待補 / 待確認" in template
|
||||
assert "待補 / AI 例外" in template
|
||||
assert "下一步" in template
|
||||
assert "前往今日作戰" in template
|
||||
assert "ga-competitor-signal" in css
|
||||
@@ -728,7 +823,7 @@ def test_help_empty_login_and_ppt_copy_are_action_oriented():
|
||||
"templates/system_settings.html": ["會更新對應年月份的業績資料"],
|
||||
"templates/admin/ppt_audit_history.html": [
|
||||
"先確認簡報可預覽、可審核,再把問題交給修復流程",
|
||||
"先補齊視覺 QA 執行條件,再判斷簡報品質",
|
||||
"補齊轉檔與視覺模型後即可自動審核",
|
||||
"先補排程或手動產出,再進行視覺 QA",
|
||||
"先補視覺 QA 條件",
|
||||
],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import ast
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -228,10 +230,51 @@ def test_roi_ai_smoke_and_daily_report_schedules_stay_staggered():
|
||||
assert 'schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task)' in source
|
||||
assert 'schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)' in source
|
||||
assert 'schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_task)' in source
|
||||
assert "schedule.every(30).minutes.do(run_pchome_growth_momo_backfill_catchup_task)" in source
|
||||
assert "schedule.every(6).hours.do(run_action_plan_hygiene_task)" in source
|
||||
assert "schedule.every(15).minutes.do(run_ollama_111_usage_guard_check)" in source
|
||||
|
||||
|
||||
def test_pchome_growth_backfill_catchup_uses_runtime_receipt_cooldown(monkeypatch, tmp_path):
|
||||
run_scheduler = _load_run_scheduler(monkeypatch)
|
||||
stats_path = tmp_path / "scheduler_stats.json"
|
||||
stats_path.write_text(json.dumps({
|
||||
"pchome_growth_momo_backfill": [{
|
||||
"status": "Success",
|
||||
"last_run": "2026-07-10 09:00:00",
|
||||
}],
|
||||
}), encoding="utf-8")
|
||||
monkeypatch.setenv("SCHEDULER_STATS_PATH", str(stats_path))
|
||||
|
||||
due, reason = run_scheduler._pchome_growth_backfill_due(
|
||||
now=datetime(2026, 7, 10, 11, 0, 0)
|
||||
)
|
||||
assert due is False
|
||||
assert reason == "cooldown_4.0h"
|
||||
|
||||
due, reason = run_scheduler._pchome_growth_backfill_due(
|
||||
now=datetime(2026, 7, 10, 13, 30, 0)
|
||||
)
|
||||
assert due is True
|
||||
assert reason == "stale_receipt_4.5h"
|
||||
|
||||
|
||||
def test_pchome_growth_backfill_catchup_executes_missing_receipt(monkeypatch, tmp_path):
|
||||
run_scheduler = _load_run_scheduler(monkeypatch)
|
||||
monkeypatch.setenv("SCHEDULER_STATS_PATH", str(tmp_path / "missing.json"))
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
run_scheduler,
|
||||
"run_pchome_growth_momo_backfill_task",
|
||||
lambda: calls.append("executed"),
|
||||
)
|
||||
|
||||
result = run_scheduler.run_pchome_growth_momo_backfill_catchup_task()
|
||||
|
||||
assert result == {"status": "executed", "reason": "missing_runtime_receipt"}
|
||||
assert calls == ["executed"]
|
||||
|
||||
|
||||
def test_ollama_111_usage_guard_stays_observational(monkeypatch):
|
||||
run_scheduler = _load_run_scheduler(monkeypatch)
|
||||
source = inspect.getsource(run_scheduler.run_ollama_111_usage_guard_check)
|
||||
|
||||
@@ -266,7 +266,7 @@ def test_triaged_alert_renders_decision_envelope_contract():
|
||||
assert "🧩 <b>比對證據</b>" in msg
|
||||
assert "Match:<code>0.91</code>" in msg
|
||||
assert "identity_v2 + price_alert_exact" in msg
|
||||
assert "✅ <b>人工下一步</b>" in msg
|
||||
assert "✅ <b>AI 例外下一步</b>" in msg
|
||||
assert "動作:<code>human_review</code>" in msg
|
||||
assert "ai_call_id=123" in msg
|
||||
assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:decision_env_001"
|
||||
|
||||
@@ -136,7 +136,8 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
||||
assert payload["metadata"]["stale_match_count"] == 18
|
||||
assert payload["metadata"]["pending_match_count"] == 612
|
||||
assert payload["metadata"]["review_queue_count"] == 1
|
||||
assert payload["metadata"]["hitl_count"] == 1
|
||||
assert payload["metadata"]["hitl_count"] == 0
|
||||
assert payload["metadata"]["ai_exception_count"] == 1
|
||||
assert payload["metadata"]["auto_execute_blocked_count"] == 1
|
||||
assert payload["metadata"]["decision_envelope_source"] == "competitor_intel_repository"
|
||||
assert payload["reviewDecisionBrief"]["items"][0]["sku"] == "SKU-REVIEW"
|
||||
@@ -188,7 +189,7 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
|
||||
assert payload["metadata"]["stale_match_count"] == 2
|
||||
assert payload["metadata"]["pending_match_count"] == 9
|
||||
assert payload["metadata"]["review_queue_count"] == 0
|
||||
assert payload["reviewDecisionBrief"]["text"] == "(目前沒有待覆核決策信封)"
|
||||
assert payload["reviewDecisionBrief"]["text"] == "(目前沒有待 AI 決策信封)"
|
||||
assert "非同款、單位價或變體候選" in payload["aiCandidate"]["thesis"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user