from __future__ import annotations from pathlib import Path from gyxx_flow.adapters.native import DeferredModuleCommandStep from gyxx_flow.catalog import WorkflowCatalog from gyxx_flow.modules.shop_intelligence import ( COLLECTION_TIMEOUT_SECONDS, SHOP_RESOURCE, SHOP_WORKFLOW_IDS, ShopIntelligenceModule, ) PROJECT_ROOT = Path(__file__).resolve().parents[1] def test_shop_module_registers_all_catalog_workflows() -> None: module = ShopIntelligenceModule.from_catalog( WorkflowCatalog.load(PROJECT_ROOT / "config") ) assert tuple(item.workflow_id for item in module.workflow_definitions()) == SHOP_WORKFLOW_IDS def test_shop_workflows_use_project_owned_module_commands() -> None: module = ShopIntelligenceModule.from_catalog( WorkflowCatalog.load(PROJECT_ROOT / "config") ) for definition in module.workflow_definitions(): assert definition.steps for step in definition.steps: assert isinstance(step.action, DeferredModuleCommandStep) if definition.workflow_id == "shop.jd_self_operated.daily": expected_timeout = {"brand": 600, "product": 1_800}[step.step_id] elif definition.workflow_id == "shop.douyin_price_appeal": expected_timeout = 1_800 else: expected_timeout = COLLECTION_TIMEOUT_SECONDS assert step.timeout_seconds == expected_timeout assert step.max_attempts == 1 assert step.resources == (f"{SHOP_RESOURCE}:{step.step_id}",) assert step.production_sink is True def test_shop_metrics_platforms_are_independent_graph_steps() -> None: module = ShopIntelligenceModule.from_catalog( WorkflowCatalog.load(PROJECT_ROOT / "config") ) metrics = next( item for item in module.workflow_definitions() if item.workflow_id == "shop.metrics.weekly" ) assert tuple(step.step_id for step in metrics.steps) == ("jd", "dy", "tm") assert all(not step.depends_on for step in metrics.steps) def test_douyin_price_appeal_reuses_weekly_browser_resource() -> None: module = ShopIntelligenceModule.from_catalog( WorkflowCatalog.load(PROJECT_ROOT / "config") ) definitions = {item.workflow_id: item for item in module.workflow_definitions()} metrics_douyin = next( step for step in definitions["shop.metrics.weekly"].steps if step.step_id == "dy" ) appeal = definitions["shop.douyin_price_appeal"].steps[0] assert appeal.step_id == "dy" assert appeal.action.command_entry == ( "collectors/dy_store_competitor_store_scraping.py" ) assert appeal.action.command_args == ("--price-appeal", "--execute") assert appeal.resources == metrics_douyin.resources == ( f"{SHOP_RESOURCE}:dy", ) assert appeal.replay_policy == "idempotent" def test_jd_self_operated_preserves_source_runner_graph_contract() -> None: module = ShopIntelligenceModule.from_catalog( WorkflowCatalog.load(PROJECT_ROOT / "config") ) workflow = next( item for item in module.workflow_definitions() if item.workflow_id == "shop.jd_self_operated.daily" ) brand, product = workflow.steps assert (brand.step_id, product.step_id) == ("brand", "product") assert brand.action.command_entry == ( "collectors/jd_self_operated_brand_daily.py" ) assert product.action.command_entry == ( "collectors/jd_self_operated_product_daily.py" ) assert brand.timeout_seconds == 600 assert product.timeout_seconds == 1_800 assert brand.depends_on == () assert product.depends_on == ("brand",) assert product.run_after_failure is True