Files
gyxx-flow/tests/test_product_runtime_data_boundaries.py
T
wangyunlong 6f1c193f4b feat(product-commerce): 款式主键统一 style_id 并下线评价采集
采集端与 data-hub 统一用 style_id (= dim_style.id) 关联款式,style_name 降级为显示冗余。

款式主键:
- 14 张采集表加 style_id BIGINT + style_match_method + *_style_id_idx 索引
- 一律逻辑外键:不建 FOREIGN KEY,早期 10 个 *_style_id_fk 在 schema.sql 里幂等 DROP
- loader 下发 dim_style.id;db 层 attach_style_ids / resolve_style_ids_by_product 兜底
- 新增 backfill_style_id.py 三段回填(款名 / 日报按商品 ID / 广告表按 spu_id)
- 新增视图 v_style_id_coverage、v_style_unmatched_product、v_style_unmatched_source
- 实库补录 199 个商品 ID 到 67 款 spus;日报 style_id 覆盖 49.1% -> 79.0%,
  tm 广告 99.4% / jd 92.4% / dy 83.2%,其余 11 张 100%

评价采集下线:
- product_reviews 归档为 product_reviews_archived_20261101
- 差评改由 shop.{tmall,jd,dy}_negative_reviews.daily 采集,消费方读 v_negative_reviews
- 删除 import_product_reviews.py / orchestrate_review_collection.py 及相关命令与绑定
- dy_product_scraping 默认流程摘除评论采集,保留 --reviews-only

天猫广告:
- 迁移到独立表 tm_product_ad_daily_metrics,不再写 product_daily_metrics.raw_data

同批附带:
- 光合上传支持天猫新店 new_bag(仅黑曜石系列)
- 京东商智双月日历排除 -diff-month 补位格
- 万相主体搜索词覆盖 STYLE_SEARCH_KEYWORDS
- ERP 部分编码失败时用已成功编码销量兜底,避免整款归零
2026-11-01 16:43:27 +08:00

147 lines
4.8 KiB
Python

from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_ROOT = (
PROJECT_ROOT
/ "src"
/ "gyxx_flow"
/ "modules"
/ "product_commerce"
)
def _load_runtime_paths(monkeypatch: pytest.MonkeyPatch, data_root: Path):
monkeypatch.setenv("GYXX_DATA_ROOT", str(data_root))
monkeypatch.setenv("GYXX_MODULE_ROOT", str(MODULE_ROOT))
module_path = MODULE_ROOT / "runtime_paths.py"
name = "_gyxx_product_runtime_paths_boundaries"
spec = importlib.util.spec_from_file_location(name, module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
try:
spec.loader.exec_module(module)
finally:
sys.modules.pop(name, None)
return module
def test_managed_data_path_rejects_writes_outside_data_home(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
paths = _load_runtime_paths(monkeypatch, tmp_path)
assert paths.managed_data_path("data/raw/product_commerce/run.json") == (
tmp_path / "data" / "raw" / "product_commerce" / "run.json"
)
assert paths.managed_data_path(paths.TMP_ROOT / "batch.json") == (
tmp_path / "tmp" / "product_commerce" / "batch.json"
)
with pytest.raises(ValueError, match="outside GYXX_DATA_ROOT"):
paths.managed_data_path(tmp_path.parent / "escape.json")
with pytest.raises(ValueError, match="outside GYXX_DATA_ROOT"):
paths.managed_data_path("../escape.json")
def test_artifact_relative_path_is_relative_to_data_home(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
paths = _load_runtime_paths(monkeypatch, tmp_path)
artifact = paths.RAW_DATA_ROOT / "jd" / "record.json"
assert paths.artifact_relative_path(artifact) == (
"data/raw/product_commerce/jd/record.json"
)
with pytest.raises(ValueError, match="outside GYXX_DATA_ROOT"):
paths.artifact_relative_path(MODULE_ROOT / "record.json")
@pytest.mark.parametrize(
("relative", "forbidden"),
[
("collect_retry_utils.py", 'Path("data/logs")'),
("rebuild_market_rank_documents.py", 'PROJECT_ROOT / "data'),
("scripts/insert_main_image_records.py", 'PROJECT_ROOT / "_tmp_'),
("jd_product_data_collector.py", 'os.path.join(PROJECT_ROOT, "debug"'),
("vendors/jd-data-flow/jd_data_collector.py", "os.getcwd()"),
(
"vendors/jd-data-flow/jd_peer_product_data_collector.py",
"os.path.dirname(os.path.abspath(__file__)), \"data\"",
),
],
)
def test_runtime_writers_do_not_fall_back_to_cwd_or_source_tree(
relative: str, forbidden: str
) -> None:
text = (MODULE_ROOT / relative).read_text(encoding="utf-8-sig")
assert forbidden not in text
@pytest.mark.parametrize(
"relative",
[
"collect_jd_market_rank.py",
"collect_sycm_market_rank.py",
"scripts/insert_jd_main_image_records.py",
"scripts/insert_main_image_records.py",
],
)
def test_runtime_artifact_references_do_not_assume_source_tree_parent(
relative: str,
) -> None:
text = (MODULE_ROOT / relative).read_text(encoding="utf-8-sig")
assert ".relative_to(PROJECT_ROOT)" not in text
def test_checkpoints_and_operational_json_use_state_and_log_roots() -> None:
retry_text = (MODULE_ROOT / "collect_retry_utils.py").read_text(
encoding="utf-8-sig"
)
assert "LOG_ROOT / \"failures\"" in retry_text
for relative in (
"collect_dy_market_rank.py",
"collect_jd_market_rank.py",
"collect_sycm_market_rank.py",
):
text = (MODULE_ROOT / relative).read_text(encoding="utf-8-sig")
assert "STATE_ROOT" in text
assert "CHECKPOINT_PATH = STATE_ROOT" in text
decline = (MODULE_ROOT / "check_nine_day_decline.py").read_text(
encoding="utf-8-sig"
)
insert = (MODULE_ROOT / "insert_bitable_records.py").read_text(
encoding="utf-8-sig"
)
assert "LOG_PATH = LOG_ROOT" in decline
assert "failures_path = LOG_ROOT" in insert
@pytest.mark.parametrize(
"relative",
[
"aggregate_daily_final.py",
"collect_erp_yesterday_metrics.py",
"check_nine_day_decline.py",
"jd_main_image_collector.py",
"jd_self_inventory_sales_collector.py",
"jd_product_data_collector.py",
"taobao_dmp_item_crowd_insight_screenshots.py",
"taobao_wanxiang_ai_creative_report.py",
"scripts/insert_jd_main_image_records.py",
"scripts/insert_main_image_records.py",
"vendors/jd-data-flow/jd_data_collector.py",
"vendors/jd-data-flow/jd_product_data_collector.py",
],
)
def test_cli_output_overrides_use_the_data_home_guard(relative: str) -> None:
text = (MODULE_ROOT / relative).read_text(encoding="utf-8-sig")
assert "managed_data_path" in text