from sqlalchemy import create_engine, text def test_connector_contract_lists_mainstream_sources_without_auto_alerts(): from services.external_market_offer_service import build_connector_contracts payload = build_connector_contracts() assert payload["success"] is True assert payload["plain_summary"] == "所有外部市場資料都先轉成同一份商品報價格式,再進作戰清單。" sources = {source["code"]: source for source in payload["sources"]} assert sources["momo_reference"]["status_label"] == "正在使用" assert sources["shopee"]["status_label"] == "先暫停" assert sources["coupang"]["status_label"] == "先暫停" assert { "shopee", "lazada", "amazon", "google_merchant", "tiktok_shop", "line_shopping", "rakuten", "yahoo_shopping", "ruten", "shopify_brand_store", "meta_commerce", "coupang", }.issubset(sources) assert all( sources[code]["status_code"] == "paused" for code in sources if code != "momo_reference" ) assert "手動 CSV" in sources["shopee"]["input_methods"] assert "官方 API" in sources["coupang"]["input_methods"] assert "官方報表" in sources["google_merchant"]["input_methods"] assert "商品 Feed" in sources["shopify_brand_store"]["input_methods"] assert sources["tiktok_shop"]["plain_note"].startswith("保留短影音電商") assert sources["ruten"]["display_name"] == "露天" assert "price" in payload["manual_csv"]["required_headers"] assert "product_url" in payload["manual_csv"]["optional_headers"] assert "image_url" in payload["manual_csv"]["optional_headers"] assert "stock_status" in payload["manual_csv"]["optional_headers"] evidence_fields = { field["name"] for field in payload["offer_evidence_contract"]["professional_evidence_fields"] } assert { "source_product_id", "title", "product_url", "image_url", "price", "stock_status", "observed_at", "quality_score", }.issubset(evidence_fields) def test_normalized_offer_payload_validates_plain_required_fields(): from services.external_market_offer_service import normalize_external_offer_payload record, errors = normalize_external_offer_payload({ "source_code": "momo_reference", "source_product_id": "MOMO-1", "title": "外部參考商品", "price": "899", "observed_at": "2026-06-15T10:00:00", "ingestion_method": "manual_csv", "product_url": "https://momo.test/MOMO-1", "image_url": "https://img.test/MOMO-1.jpg", "stock_status": "in_stock", "quality_score": 82, "quality_note": "人工確認同款", }) assert errors == [] assert record is not None assert record.to_record()["source_offer_key"] == "momo_reference:MOMO-1" assert record.to_record()["product_url"] == "https://momo.test/MOMO-1" assert record.to_record()["image_url"] == "https://img.test/MOMO-1.jpg" assert record.to_record()["stock_status"] == "in_stock" assert record.to_record()["data_quality_status"] == "needs_review" missing_record, missing_errors = normalize_external_offer_payload({ "source_code": "shopee", "price": 0, }) assert missing_record is None assert "缺少外部商品 ID" in missing_errors assert "售價必須大於 0" in missing_errors def test_external_offer_csv_dry_run_accepts_chinese_headers_and_classifies_rows(): from services.external_market_offer_service import dry_run_external_offer_csv csv_text = "\n".join([ "資料來源,外部商品ID,商品名稱,售價,資料時間,取得方式,PChome商品ID,同款狀態,資料可信度", "momo_reference,MOMO-1,可用商品,899,2026-06-15T10:00:00,manual_csv,PCH-1,verified,88", "shopee,SHP-1,暫停來源商品,799,2026-06-15T10:01:00,manual_csv,PCH-2,verified,90", "momo_reference,MOMO-2,缺價格商品,,2026-06-15T10:02:00,manual_csv,PCH-3,verified,90", ]) payload = dry_run_external_offer_csv(csv_text) assert payload["success"] is True assert payload["message"] == "CSV 預檢完成,尚未寫入資料。" assert payload["summary"] == { "total_rows": 3, "ready_count": 1, "review_count": 1, "blocked_count": 1, } rows = payload["rows"] assert rows[0]["status_label"] == "可使用" assert rows[1]["status_label"] == "需 AI 自動驗證確認" assert "這個來源目前先暫停,不進告警" in rows[1]["reasons"] assert rows[2]["status_label"] == "不能使用" assert "缺少售價" in rows[2]["reasons"] def test_external_offer_csv_dry_run_accepts_plain_business_headers_and_values(): from services.external_market_offer_service import dry_run_external_offer_csv csv_text = "\n".join([ "來源,平台商品編號,商品名稱,售價,資料時間,取得方式,PChome商品編號,是否同款,可信度", "MOMO,MOMO-1,可用商品,899,2026-06-15T10:00:00,備用資料,PCH-1,是,88", "MOMO,MOMO-2,待確認商品,799,2026-06-15T10:01:00,備援資料,,待確認,60", ]) payload = dry_run_external_offer_csv(csv_text) assert payload["success"] is True assert payload["summary"]["total_rows"] == 2 assert payload["summary"]["ready_count"] == 1 assert payload["summary"]["review_count"] == 1 assert payload["summary"]["blocked_count"] == 0 rows = payload["rows"] assert rows[0]["source_code"] == "momo_reference" assert rows[0]["status_label"] == "可使用" assert rows[1]["source_code"] == "momo_reference" assert rows[1]["status_label"] == "需 AI 自動驗證確認" assert "尚未確認同款" in rows[1]["reasons"] assert "缺少 PChome 商品 ID,無法連到業績" in rows[1]["reasons"] def test_external_offer_csv_dry_run_reports_empty_file_plainly(): from services.external_market_offer_service import dry_run_external_offer_csv payload = dry_run_external_offer_csv("") assert payload["success"] is False assert payload["errors"] == ["CSV 內容是空的"] assert payload["summary"]["blocked_count"] == 0 def _seed_external_offer_sync_tables(engine): with engine.begin() as conn: conn.execute(text(""" CREATE TABLE external_market_sources ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, platform_code TEXT NOT NULL, source_kind TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'paused', enabled BOOLEAN NOT NULL DEFAULT 0, write_enabled BOOLEAN NOT NULL DEFAULT 0, allowed_input_methods_json TEXT, quality_policy_json TEXT, plain_note TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) """)) conn.execute(text(""" CREATE TABLE external_offers ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_code TEXT NOT NULL, platform_code TEXT NOT NULL, source_product_id TEXT NOT NULL, source_offer_key TEXT NOT NULL, title TEXT NOT NULL, brand TEXT, category_text TEXT, product_url TEXT, image_url TEXT, price REAL, original_price REAL, currency TEXT NOT NULL DEFAULT 'TWD', stock_status TEXT, sold_count INTEGER, rating REAL, review_count INTEGER, observed_at TEXT NOT NULL, expires_at TEXT, ingestion_method TEXT NOT NULL, connector_key TEXT, pchome_product_id TEXT, momo_sku TEXT, match_status TEXT NOT NULL DEFAULT 'unmatched', quality_score REAL NOT NULL DEFAULT 0, data_quality_status TEXT NOT NULL DEFAULT 'needs_review', quality_notes_json TEXT, raw_payload_json TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, UNIQUE (source_code, source_product_id, observed_at, ingestion_method) ) """)) 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, url TEXT, image_url 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 competitor_prices (sku, source, competitor_product_id, competitor_product_name, price, match_score, tags, crawled_at, expires_at) VALUES ('MOMO-1', 'pchome', 'PCH-1', 'PChome 商品', 1000, 0.91, '["identity_v2"]', '2026-06-15 09:30:00', NULL), ('MOMO-2', 'pchome', 'PCH-2', '低信心商品', 600, 0.60, '["identity_v2"]', '2026-06-15 09:30:00', NULL) """)) conn.execute(text( "INSERT INTO products (id, i_code, name, url, image_url, status) " "VALUES (1, 'MOMO-1', 'MOMO 同款商品', 'https://momo.test/1', 'https://img.test/1.jpg', 'ACTIVE')" )) conn.execute(text( "INSERT INTO price_records (id, product_id, price, timestamp) " "VALUES (1, 1, 900, '2026-06-15 10:00:00')" )) def test_sync_legacy_momo_reference_offers_writes_verified_cache_to_external_offers(monkeypatch): from services import external_market_offer_service as service stale_marks = [] monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True)) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = service.sync_legacy_momo_reference_offers(engine, limit=20) assert payload["success"] is True assert payload["status"] == "synced" assert payload["candidate_count"] == 1 assert payload["written_count"] == 1 with engine.connect() as conn: rows = conn.execute(text(""" SELECT source_code, platform_code, source_product_id, title, price, pchome_product_id, momo_sku, match_status, quality_score, data_quality_status, ingestion_method FROM external_offers """)).mappings().all() assert len(rows) == 1 row = dict(rows[0]) assert row["source_code"] == "momo_reference" assert row["platform_code"] == "momo" assert row["source_product_id"] == "MOMO-1" assert row["pchome_product_id"] == "PCH-1" assert row["title"] == "MOMO 同款商品" assert row["price"] == 900 assert row["match_status"] == "verified" assert row["quality_score"] == 91 assert row["data_quality_status"] == "verified" assert row["ingestion_method"] == "legacy_competitor_cache" assert stale_marks == [True] def test_sync_legacy_momo_reference_offers_dry_run_does_not_write(): from services.external_market_offer_service import sync_legacy_momo_reference_offers engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = sync_legacy_momo_reference_offers(engine, limit=20, dry_run=True) assert payload["success"] is True assert payload["status"] == "dry_run" assert payload["candidate_count"] == 1 assert payload["written_count"] == 0 with engine.connect() as conn: count = conn.execute(text("SELECT COUNT(*) FROM external_offers")).scalar() assert count == 0 def test_sync_targeted_momo_candidates_writes_unit_price_offer(monkeypatch): from services import external_market_offer_service as service stale_marks = [] monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True)) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = service.sync_targeted_momo_candidates_to_external_offers(engine, [ { "product_id": "10833188", "name": "MOMO B5 修復霜 40ml", "price": 468, "original_price": 835, "product_url": "https://momo.test/10833188", "image_url": "https://img.test/10833188.jpg", "brand": "理膚寶水", "target_pchome_product_id": "PCH-1", "target_pchome_name": "PChome B5 修復霜 40ml", "target_pchome_price": 920, "target_match_score": 0.74, "auto_compare_type": "unit_price", "target_price_basis": "unit_price", "target_match_reasons": ["unit_comparable"], "target_comparison_mode": "unit_comparable", "target_unit_price_comparison": { "comparable": True, "unit_label": "ml", "momo_total_quantity": 40, "competitor_total_quantity": 40, "momo_unit_price": 11.7, "competitor_unit_price": 23.0, "unit_gap_pct": -49.13, }, }, { "product_id": "REVIEW-1", "name": "仍需人工商品", "price": 500, "target_pchome_product_id": "PCH-2", "auto_compare_type": "manual_review", }, ]) assert payload["success"] is True assert payload["status"] == "synced" assert payload["candidate_count"] == 2 assert payload["written_count"] == 1 assert payload["unit_price_count"] == 1 assert payload["skipped_reasons"] == {"不是可自動使用的候選": 1} with engine.connect() as conn: row = conn.execute(text(""" SELECT source_product_id, price, pchome_product_id, match_status, quality_score, data_quality_status, ingestion_method, raw_payload_json FROM external_offers """)).mappings().one() raw_payload = __import__("json").loads(row["raw_payload_json"]) assert row["source_product_id"] == "10833188" assert row["price"] == 468 assert row["pchome_product_id"] == "PCH-1" assert row["match_status"] == "verified" assert row["quality_score"] == 82 assert row["data_quality_status"] == "verified" assert row["ingestion_method"] == "targeted_momo_search" assert raw_payload["price_basis"] == "unit_price" assert raw_payload["pchome_public_price"] == 920 assert raw_payload["unit_price_comparison"]["unit_gap_pct"] == -49.13 assert stale_marks == [True] def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeypatch): from services import external_market_offer_service as service stale_marks = [] monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True)) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = service.sync_targeted_momo_candidates_to_external_offers(engine, [ { "product_id": "12876190", "name": "【LAURA MERCIER 蘿拉蜜思】煥顏透明蜜粉 29g(#Rose-國際航空版)", "price": 809, "target_pchome_product_id": "PCH-LAURA", "target_pchome_name": "【Laura Mercier 蘿拉蜜思】煥顏透明蜜粉 29g", "target_pchome_price": 899, "target_match_score": 0.98, "auto_compare_type": "total_price", "target_price_basis": "manual_review", "target_alert_tier": "identity_review", "target_match_reasons": ["variant_selection_review", "strong_exact_spec_match"], "target_comparison_mode": "exact_identity", }, ]) assert payload["success"] is True assert payload["candidate_count"] == 1 assert payload["written_count"] == 0 assert payload["skipped_reasons"] == {"候選仍需 AI 自動驗證確認": 1} assert stale_marks == [] with engine.connect() as conn: count = conn.execute(text("SELECT COUNT(*) FROM external_offers")).scalar() assert count == 0 def test_sync_targeted_momo_review_candidates_writes_needs_review_offer(monkeypatch): from services import external_market_offer_service as service stale_marks = [] monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True)) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = service.sync_targeted_momo_review_candidates_to_external_offers(engine, [ { "product_id": "14917079", "name": "【cle de peau 肌膚之鑰】光采柔焦蜜粉 24g (國際航空版)", "price": 2618, "target_pchome_product_id": "PCH-CDP", "target_pchome_name": "cle de peau 光采柔焦蜜粉 24g #1", "target_pchome_price": 2790, "target_match_score": 1.0, "auto_compare_type": "manual_review", "target_price_basis": "none", "target_alert_tier": "identity_review", "target_match_type": "exact", "target_match_reasons": ["variant_selection_review", "strong_exact_spec_match"], "target_comparison_mode": "exact_identity", "target_gap_pct": -6.16, }, { "product_id": "LOW-SCORE", "name": "低分候選", "price": 100, "target_pchome_product_id": "PCH-LOW", "target_match_score": 0.4, "target_alert_tier": "identity_review", }, ]) assert payload["success"] is True assert payload["status"] == "synced" assert payload["candidate_count"] == 2 assert payload["written_count"] == 1 assert payload["skipped_reasons"] == {"AI 自動驗證確認候選分數過低": 1} with engine.connect() as conn: row = conn.execute(text(""" SELECT source_product_id, price, pchome_product_id, match_status, quality_score, data_quality_status, ingestion_method, raw_payload_json FROM external_offers """)).mappings().one() readiness = service.build_external_source_readiness(engine) raw_payload = __import__("json").loads(row["raw_payload_json"]) assert row["source_product_id"] == "14917079" assert row["price"] == 2618 assert row["pchome_product_id"] == "PCH-CDP" assert row["match_status"] == "needs_review" assert row["quality_score"] == 100 assert row["data_quality_status"] == "needs_review" assert row["ingestion_method"] == "targeted_momo_review" assert raw_payload["review_state"] == "needs_review" assert raw_payload["price_basis"] == "none" assert raw_payload["alert_tier"] == "identity_review" assert "needs_review" in raw_payload["tags"] assert readiness["review_offer_count"] == 1 assert stale_marks == [True] def test_momo_review_candidate_queue_can_confirm_candidate(monkeypatch): from services import external_market_offer_service as service stale_marks = [] monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True)) monkeypatch.setattr( service, "_fetch_pchome_public_image_map", lambda product_ids: {"PCH-CDP": "https://cs-a.ecimg.tw/items/PCH-CDP/000001.jpg"}, ) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) service.sync_targeted_momo_review_candidates_to_external_offers(engine, [ { "product_id": "14917079", "name": "【cle de peau 肌膚之鑰】光采柔焦蜜粉 24g (國際航空版)", "price": 2618, "target_pchome_product_id": "PCH-CDP", "target_pchome_name": "cle de peau 光采柔焦蜜粉 24g #1", "target_pchome_price": 2790, "target_match_score": 1.0, "target_price_basis": "none", "target_alert_tier": "identity_review", "target_match_type": "exact", "target_match_reasons": ["variant_selection_review"], "target_gap_pct": -6.16, }, ]) queue = service.list_momo_review_candidates(engine) assert queue["success"] is True assert queue["count"] == 1 candidate = queue["rows"][0] assert candidate["pchome_product_name"] == "cle de peau 光采柔焦蜜粉 24g #1" assert candidate["momo_sku"] == "14917079" assert candidate["plain_status"] == "待確認同款或色號" assert candidate["pchome_image_url"] == "https://cs-a.ecimg.tw/items/PCH-CDP/000001.jpg" assert "momo_image_url" in candidate updated = service.update_momo_review_candidate(engine, candidate["id"], "confirm", note="同款 #1") assert updated["success"] is True assert updated["match_status"] == "verified" assert service.list_momo_review_candidates(engine)["count"] == 0 with engine.connect() as conn: row = conn.execute(text(""" SELECT match_status, data_quality_status, raw_payload_json FROM external_offers WHERE id = :id """), {"id": candidate["id"]}).mappings().one() raw_payload = __import__("json").loads(row["raw_payload_json"]) assert row["match_status"] == "verified" assert row["data_quality_status"] == "verified" assert raw_payload["review_action"] == "confirm" assert stale_marks == [True, True] def test_sync_targeted_momo_candidates_keeps_best_unit_quantity_match(monkeypatch): from services import external_market_offer_service as service monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: None) engine = create_engine("sqlite:///:memory:") _seed_external_offer_sync_tables(engine) payload = service.sync_targeted_momo_candidates_to_external_offers(engine, [ { "product_id": "MOMO-SINGLE", "name": "雪之上 全效合一水凝霜 80g/瓶", "price": 1380, "target_pchome_product_id": "PCH-YUKINOUE", "target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組", "target_pchome_price": 2960, "target_match_score": 0.74, "auto_compare_type": "unit_price", "target_price_basis": "unit_price", "target_match_reasons": ["count_conflict", "unit_comparable"], "target_comparison_mode": "unit_comparable", "target_unit_price_comparison": { "comparable": True, "unit_label": "g", "momo_total_quantity": 80, "competitor_total_quantity": 240, "momo_unit_price": 17.25, "competitor_unit_price": 12.33, "unit_gap_pct": 39.86, }, }, { "product_id": "MOMO-THREE", "name": "雪之上 全效合一水凝霜 80g X 3入瓶裝", "price": 2680, "target_pchome_product_id": "PCH-YUKINOUE", "target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組", "target_pchome_price": 2960, "target_match_score": 0.74, "auto_compare_type": "unit_price", "target_price_basis": "unit_price", "target_match_reasons": ["unit_comparable"], "target_comparison_mode": "unit_comparable", "target_unit_price_comparison": { "comparable": True, "unit_label": "g", "momo_total_quantity": 240, "competitor_total_quantity": 240, "momo_unit_price": 11.17, "competitor_unit_price": 12.33, "unit_gap_pct": -9.46, }, }, ]) assert payload["success"] is True assert payload["candidate_count"] == 2 assert payload["written_count"] == 1 with engine.connect() as conn: row = conn.execute(text(""" SELECT source_product_id, raw_payload_json FROM external_offers """)).mappings().one() raw_payload = __import__("json").loads(row["raw_payload_json"]) assert row["source_product_id"] == "MOMO-THREE" assert raw_payload["unit_price_comparison"]["momo_total_quantity"] == 240 def test_external_source_readiness_uses_legacy_momo_reference_cache(): from services.external_market_offer_service import build_external_source_readiness engine = create_engine("sqlite:///:memory:") with engine.begin() as conn: conn.execute(text( "CREATE TABLE competitor_prices (" "source TEXT, competitor_product_id TEXT, price REAL, match_score REAL, " "tags TEXT, crawled_at TEXT, expires_at TEXT)" )) conn.execute(text(""" INSERT INTO competitor_prices (source, competitor_product_id, price, match_score, tags, crawled_at, expires_at) VALUES ('pchome', 'PCH-1', 1000, 0.91, '["identity_v2"]', '2026-06-15 10:00:00', NULL), ('pchome', 'PCH-2', 500, 0.50, '["identity_v2"]', '2026-06-15 10:00:00', NULL) """)) payload = build_external_source_readiness(engine) assert payload["success"] is True assert payload["schema_ready"] is False sources = {source["code"]: source for source in payload["sources"]} assert sources["momo_reference"]["usable_offer_count"] == 1 assert sources["momo_reference"]["plain_state"] == "已接入,可進作戰清單" assert sources["shopee"]["plain_state"] == "先保留接口,不進告警" assert sources["tiktok_shop"]["plain_state"] == "先保留接口,不進告警" assert sources["line_shopping"]["plain_state"] == "先保留接口,不進告警" assert sources["shopify_brand_store"]["plain_state"] == "先保留接口,不進告警" evidence_fields = { field["name"] for field in payload["offer_evidence_contract"]["professional_evidence_fields"] } assert "image_url" in evidence_fields assert "stock_status" in evidence_fields assert payload["offer_evidence_contract"]["quality_gate"]["unverified_data_route"] == "待確認候選,不進告警" assert payload["plain_summary"] == "MOMO 先用;其他主流平台已列管,未接合法穩定來源前不進告警。" def test_external_market_migration_creates_source_and_offer_tables(): from pathlib import Path migration = Path("migrations/044_external_market_offer_normalization.sql").read_text(encoding="utf-8") assert "CREATE TABLE IF NOT EXISTS external_market_sources" in migration assert "CREATE TABLE IF NOT EXISTS external_offers" in migration assert "momo_reference" in migration assert "shopee" in migration assert "coupang" in migration assert "DROP " not in migration.upper() assert "TRUNCATE " not in migration.upper() def test_external_offer_csv_dry_run_route_is_registered_as_post_only(): from pathlib import Path route_source = Path("routes/ai_routes.py").read_text(encoding="utf-8") assert "@ai_bp.route('/api/ai/pchome-growth/external-offers/csv-dry-run', methods=['POST'])" in route_source assert "dry_run_external_offer_csv" in route_source assert "@ai_bp.route('/api/ai/pchome-growth/review-candidates')" in route_source assert "@ai_bp.route('/api/ai/pchome-growth/review-candidates/', methods=['POST'])" in route_source assert "list_momo_review_candidates" in route_source assert "update_momo_review_candidate" in route_source def test_external_offer_sync_is_registered_in_scheduler(): from pathlib import Path scheduler_source = Path("scheduler.py").read_text(encoding="utf-8") run_scheduler_source = Path("run_scheduler.py").read_text(encoding="utf-8") assert "def run_external_offer_sync_task" in scheduler_source assert "sync_legacy_momo_reference_offers" in scheduler_source assert "def run_pchome_growth_momo_backfill_task" in scheduler_source assert "run_pchome_growth_momo_backfill" in scheduler_source assert "run_external_offer_sync_task" in run_scheduler_source assert "run_pchome_growth_momo_backfill_task" in run_scheduler_source assert "external_offer_sync" in run_scheduler_source assert "pchome_growth_momo_backfill" in run_scheduler_source