配置来源收敛 - 系列平台编码 tm/jd/dy/jd_self 与 9 类飞书地址改为整系列并集去重后挂锚点, 子款编码保留在 series.platform_ids 备查;多表冲突保留最高优先级并告警 - 写表脚本(当日/天猫主图/京东主图)去掉本地 JSON 回退,地址缺失或读取失败 直接 fail closed,避免库与表地址不一致被静默掩盖 - 测试注入 records_provider 时禁用配置缓存读写,防止假数据写入生产快照 采集开关(dim_style.collect_enabled) - 锚点为系列总开关:false 则整系列不采 - 锚点为 true 时,子款自身 false 仅把自己的 ERP 编码排除出聚合,其余子款照常 - 排除结果记录在 series.erp_excluded 并在运行时提示 聚水潭全店铺日报防错 - 列布局 fail closed、行 td 数校验、满页判定 + 总页数兜底、值域交叉校验 - 新增单日总量闸:超近 14 天中位数 8 倍拒绝落库 其他 - 新增电商费用归集配置 ad_cost_rollup.json 与积分兑换周报采集 - 店铺情报、供应链到货同步、通知路由与相关配置/文档同步更新 - 忽略本地 dsh workbench 脚手架(含 node_modules)
63 lines
2.1 KiB
Python
63 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
from gyxx_flow.catalog import WorkflowCatalog
|
|
from gyxx_flow.script_catalog import ScriptCatalog
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
MODULES = (
|
|
"content_marketing",
|
|
"product_commerce",
|
|
"shop_intelligence",
|
|
"supply_chain",
|
|
)
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def test_all_source_manifests_use_one_schema_and_verify_target_hashes() -> None:
|
|
for module in MODULES:
|
|
path = PROJECT_ROOT / "config" / "source-manifests" / f"{module}.json"
|
|
manifest = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
assert manifest["schema_version"] == 1
|
|
assert manifest["module"] == module
|
|
assert isinstance(manifest["snapshot"], str) and manifest["snapshot"]
|
|
assert manifest["files"]
|
|
assert isinstance(manifest["intentionally_excluded"], list)
|
|
for item in manifest["files"]:
|
|
target = PROJECT_ROOT.joinpath(
|
|
*PurePosixPath(item["target_relative_path"]).parts
|
|
)
|
|
assert target.is_file(), target
|
|
assert _sha256(target) == item["target_sha256"], target
|
|
|
|
|
|
def test_every_workflow_and_manual_script_resolves_inside_new_project() -> None:
|
|
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
|
for workflow in catalog.workflows:
|
|
if workflow.trigger == "unavailable":
|
|
continue
|
|
target = (
|
|
PROJECT_ROOT
|
|
/ "src"
|
|
/ "gyxx_flow"
|
|
/ "modules"
|
|
/ workflow.module
|
|
/ workflow.entry
|
|
).resolve(strict=True)
|
|
assert target.is_relative_to(PROJECT_ROOT)
|
|
|
|
commands = ScriptCatalog.load_default()
|
|
assert len(commands.scripts) == 62
|
|
assert len(commands.command_ids) == len(set(commands.command_ids))
|
|
assert all(command.path.is_relative_to(PROJECT_ROOT) for command in commands.scripts)
|