feat(pchome): close freshness and multi-market decision gaps
Some checks failed
CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-07-10 21:22:42 +08:00
parent 744cd946f3
commit b8cc27e4c4
29 changed files with 1373 additions and 123 deletions

View File

@@ -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"

View File

@@ -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():

View File

@@ -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():

View File

@@ -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"

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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
# ═══════════════════════════════════════════════════════════════════════════

View File

@@ -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 條件",
],

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"]