feat: consolidate legacy workflows into gyxx-flow
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gyxx_flow.catalog import WorkflowCatalog
|
||||
from gyxx_flow.modules import (
|
||||
EXPECTED_MODULE_IDS,
|
||||
BusinessModule,
|
||||
ModuleRegistry,
|
||||
create_default_registry,
|
||||
)
|
||||
from gyxx_flow.modules.content_marketing import ContentMarketingModule
|
||||
from gyxx_flow.modules.product_commerce import ProductCommerceModule
|
||||
from gyxx_flow.modules.shop_intelligence import ShopIntelligenceModule
|
||||
from gyxx_flow.modules.supply_chain import SupplyChainModule
|
||||
from gyxx_flow.workflow import WorkflowDefinition
|
||||
|
||||
|
||||
def test_default_registry_contains_exactly_the_four_business_modules() -> None:
|
||||
registry = create_default_registry()
|
||||
|
||||
assert registry.module_ids == EXPECTED_MODULE_IDS
|
||||
assert isinstance(registry.get("content_marketing"), ContentMarketingModule)
|
||||
assert isinstance(registry.get("product_commerce"), ProductCommerceModule)
|
||||
assert isinstance(registry.get("shop_intelligence"), ShopIntelligenceModule)
|
||||
assert isinstance(registry.get("supply_chain"), SupplyChainModule)
|
||||
|
||||
|
||||
def test_catalog_composition_registers_all_migrated_scheduled_and_manual_workflows() -> None:
|
||||
catalog = WorkflowCatalog.load(Path(__file__).parents[1] / "config")
|
||||
|
||||
registry = create_default_registry(catalog=catalog)
|
||||
|
||||
assert len(registry.workflow_ids) == 28
|
||||
assert set(registry.workflow_ids) == {
|
||||
"shop.metrics.weekly",
|
||||
"shop.competitor.weekly",
|
||||
"supply.purchase_confirmation.daily",
|
||||
"supply.replenishment.weekly",
|
||||
"supply.replenishment_alert.daily",
|
||||
"supply.purchase_order_update",
|
||||
"content.metrics.daily",
|
||||
"content.marketing_report.daily",
|
||||
"content.relogin.weekly",
|
||||
"content.self_operated.weekly",
|
||||
"content.creator_report.monthly",
|
||||
"content.summary.monthly",
|
||||
"content.cooperations.daily",
|
||||
"content.comments.weekly",
|
||||
"content.summary.weekly",
|
||||
"content.mapping.refresh",
|
||||
"content.retry_failed",
|
||||
"content.metrics.backfill",
|
||||
"product.persona.daily",
|
||||
"product.daily",
|
||||
"product.alert.daily",
|
||||
"product.import.daily",
|
||||
"product.style_analysis.interval",
|
||||
"product.main_image.jd.weekly",
|
||||
"product.main_image.weekly",
|
||||
"product.backfill",
|
||||
"product.market_rank",
|
||||
"product.review_collection",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("module", "module_id", "expected_workflow_ids"),
|
||||
[
|
||||
(ContentMarketingModule(), "content_marketing", ()),
|
||||
(ProductCommerceModule(), "product_commerce", ()),
|
||||
(ShopIntelligenceModule(), "shop_intelligence", ()),
|
||||
(SupplyChainModule(), "supply_chain", ()),
|
||||
],
|
||||
)
|
||||
def test_every_module_implements_the_uniform_contract(
|
||||
module: BusinessModule, module_id: str, expected_workflow_ids: tuple[str, ...]
|
||||
) -> None:
|
||||
assert isinstance(module, BusinessModule)
|
||||
assert module.module_id == module_id
|
||||
assert tuple(
|
||||
definition.workflow_id for definition in module.workflow_definitions()
|
||||
) == expected_workflow_ids
|
||||
|
||||
|
||||
def test_registry_rejects_duplicate_module_ids() -> None:
|
||||
with pytest.raises(ValueError, match="duplicate module id"):
|
||||
ModuleRegistry([ContentMarketingModule(), ContentMarketingModule()])
|
||||
|
||||
|
||||
def test_registry_indexes_workflows_without_exposing_mutable_state() -> None:
|
||||
class ExampleModule:
|
||||
module_id = "content_marketing"
|
||||
|
||||
def workflow_definitions(self) -> tuple[WorkflowDefinition, ...]:
|
||||
return (WorkflowDefinition("content.example", ()),)
|
||||
|
||||
registry = ModuleRegistry([ExampleModule()])
|
||||
|
||||
assert registry.workflow_ids == ("content.example",)
|
||||
assert registry.workflow("content.example").workflow_id == "content.example"
|
||||
with pytest.raises(KeyError):
|
||||
registry.workflow("content.missing")
|
||||
|
||||
|
||||
def test_business_packages_do_not_import_each_other_or_forbidden_gyxx_layers() -> None:
|
||||
source_root = Path(__file__).parents[1] / "src" / "gyxx_flow" / "modules"
|
||||
business_packages = set(EXPECTED_MODULE_IDS)
|
||||
allowed_shared_roots = {
|
||||
"gyxx_flow.core",
|
||||
"gyxx_flow.workflow",
|
||||
"gyxx_flow.adapters",
|
||||
"gyxx_flow.contracts",
|
||||
"gyxx_flow.modules.contracts",
|
||||
}
|
||||
violations: list[str] = []
|
||||
|
||||
for package in sorted(business_packages):
|
||||
allowed_roots = allowed_shared_roots | {f"gyxx_flow.modules.{package}"}
|
||||
for path in (source_root / package).rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
for imported in _resolved_imports(tree, package):
|
||||
other_business = {
|
||||
f"gyxx_flow.modules.{name}" for name in business_packages - {package}
|
||||
}
|
||||
if any(imported == root or imported.startswith(f"{root}.") for root in other_business):
|
||||
violations.append(f"{path.name}: cross-module import {imported}")
|
||||
continue
|
||||
if imported.startswith("gyxx_flow.") and not any(
|
||||
imported == root or imported.startswith(f"{root}.")
|
||||
for root in allowed_roots
|
||||
):
|
||||
violations.append(f"{path.name}: forbidden layer import {imported}")
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def _resolved_imports(tree: ast.AST, package: str) -> tuple[str, ...]:
|
||||
names: list[str] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
names.extend(alias.name for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level == 0 and node.module:
|
||||
names.append(node.module)
|
||||
continue
|
||||
base = ["gyxx_flow", "modules", package]
|
||||
parent = base[: len(base) - node.level + 1]
|
||||
if node.module:
|
||||
names.append(".".join([*parent, node.module]))
|
||||
else:
|
||||
names.extend(".".join([*parent, alias.name]) for alias in node.names)
|
||||
return tuple(names)
|
||||
Reference in New Issue
Block a user