feat: expand module workflows, dynamic config, notifications and console API
This commit is contained in:
@@ -6,25 +6,72 @@ from pathlib import Path
|
||||
from gyxx_flow.adapters.native import DeferredModuleCommandStep
|
||||
from gyxx_flow.catalog import WorkflowCatalog
|
||||
from gyxx_flow.modules.product_commerce import (
|
||||
PRODUCT_MANUAL_WORKFLOW_IDS,
|
||||
PRODUCT_RESOURCE,
|
||||
PRODUCT_SCHEDULED_WORKFLOW_IDS,
|
||||
PRODUCT_TIMEOUT_SECONDS,
|
||||
PRODUCT_WORKFLOW_IDS,
|
||||
TMALL_BAIBU_IMPORT_MODE_ALL,
|
||||
TMALL_BAIBU_IMPORT_MODE_WITHOUT_HYPERLINKS,
|
||||
ProductCommerceModule,
|
||||
)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_product_module_registers_only_scheduled_entries() -> None:
|
||||
def test_product_module_registers_scheduled_and_manual_entries() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
)
|
||||
|
||||
assert tuple(item.workflow_id for item in module.workflow_definitions()) == PRODUCT_WORKFLOW_IDS
|
||||
assert len(PRODUCT_SCHEDULED_WORKFLOW_IDS) == 8
|
||||
assert len(PRODUCT_SCHEDULED_WORKFLOW_IDS) == 10
|
||||
assert PRODUCT_MANUAL_WORKFLOW_IDS == (
|
||||
"product.video_upload",
|
||||
"product.jd_video_upload",
|
||||
)
|
||||
|
||||
|
||||
def test_tmall_baibu_import_mode_filters_hyperlink_steps_and_rewires_dependencies() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
|
||||
all_steps = next(
|
||||
definition
|
||||
for definition in ProductCommerceModule.from_catalog(
|
||||
catalog,
|
||||
tmall_baibu_import_mode=TMALL_BAIBU_IMPORT_MODE_ALL,
|
||||
).workflow_definitions()
|
||||
if definition.workflow_id == "product.tmall_baibu_apply"
|
||||
).steps
|
||||
filtered_steps = next(
|
||||
definition
|
||||
for definition in ProductCommerceModule.from_catalog(
|
||||
catalog,
|
||||
tmall_baibu_import_mode=TMALL_BAIBU_IMPORT_MODE_WITHOUT_HYPERLINKS,
|
||||
).workflow_definitions()
|
||||
if definition.workflow_id == "product.tmall_baibu_apply"
|
||||
).steps
|
||||
|
||||
assert [step.step_id for step in all_steps] == [
|
||||
"old_chain_bag",
|
||||
"old_all_bag",
|
||||
"old_all_3c",
|
||||
"old_chain_3c",
|
||||
"new_chain_bag",
|
||||
"new_all_bag",
|
||||
]
|
||||
assert [step.step_id for step in filtered_steps] == [
|
||||
"old_all_bag",
|
||||
"old_all_3c",
|
||||
"new_all_bag",
|
||||
]
|
||||
assert [step.depends_on for step in filtered_steps] == [
|
||||
(),
|
||||
("old_all_bag",),
|
||||
("old_all_3c",),
|
||||
]
|
||||
assert all(step.replay_policy == "repeatable" for step in all_steps)
|
||||
assert all(step.replay_policy == "repeatable" for step in filtered_steps)
|
||||
|
||||
def test_product_workflows_use_project_owned_module_commands() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
@@ -36,8 +83,23 @@ def test_product_workflows_use_project_owned_module_commands() -> None:
|
||||
assert isinstance(step.action, DeferredModuleCommandStep)
|
||||
assert step.timeout_seconds == PRODUCT_TIMEOUT_SECONDS == 6 * 60 * 60
|
||||
assert step.max_attempts == 1
|
||||
if definition.workflow_id == "product.main_image.weekly":
|
||||
if definition.workflow_id == "product.ecommerce_costs.daily":
|
||||
assert step.resources == (
|
||||
f"{PRODUCT_RESOURCE}:{definition.workflow_id}:{step.step_id}",
|
||||
)
|
||||
elif definition.workflow_id == "product.main_image.weekly":
|
||||
assert step.resources == (f"{PRODUCT_RESOURCE}:{step.step_id}",)
|
||||
elif definition.workflow_id in {
|
||||
"product.daily",
|
||||
"product.persona.daily",
|
||||
"product.style_analysis.interval",
|
||||
"product.video_upload",
|
||||
"product.jd_video_upload",
|
||||
"product.tmall_baibu_apply",
|
||||
}:
|
||||
assert step.resources == (
|
||||
f"{PRODUCT_RESOURCE}:{definition.workflow_id}",
|
||||
)
|
||||
else:
|
||||
assert step.resources == (PRODUCT_RESOURCE,) == (
|
||||
"module:product_commerce",
|
||||
@@ -45,6 +107,62 @@ def test_product_workflows_use_project_owned_module_commands() -> None:
|
||||
assert step.production_sink is True
|
||||
|
||||
|
||||
def test_workflow_scoped_product_resources_are_distinct() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
)
|
||||
resources = {
|
||||
definition.workflow_id: definition.steps[0].resources
|
||||
for definition in module.workflow_definitions()
|
||||
if definition.workflow_id
|
||||
in {
|
||||
"product.daily",
|
||||
"product.persona.daily",
|
||||
"product.style_analysis.interval",
|
||||
"product.ecommerce_costs.daily",
|
||||
"product.video_upload",
|
||||
"product.jd_video_upload",
|
||||
"product.tmall_baibu_apply",
|
||||
}
|
||||
}
|
||||
|
||||
assert resources == {
|
||||
"product.daily": ("module:product_commerce:product.daily",),
|
||||
"product.persona.daily": (
|
||||
"module:product_commerce:product.persona.daily",
|
||||
),
|
||||
"product.style_analysis.interval": (
|
||||
"module:product_commerce:product.style_analysis.interval",
|
||||
),
|
||||
"product.ecommerce_costs.daily": (
|
||||
"module:product_commerce:product.ecommerce_costs.daily:tmall_download",
|
||||
),
|
||||
"product.video_upload": (
|
||||
"module:product_commerce:product.video_upload",
|
||||
),
|
||||
"product.jd_video_upload": (
|
||||
"module:product_commerce:product.jd_video_upload",
|
||||
),
|
||||
"product.tmall_baibu_apply": (
|
||||
"module:product_commerce:product.tmall_baibu_apply",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def test_persona_workflow_allows_idempotent_recovery_after_partial_platform_runs() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
)
|
||||
|
||||
workflow = next(
|
||||
item
|
||||
for item in module.workflow_definitions()
|
||||
if item.workflow_id == "product.persona.daily"
|
||||
)
|
||||
|
||||
assert workflow.steps[0].replay_policy == "idempotent"
|
||||
|
||||
|
||||
def test_combined_main_image_workflow_runs_platforms_independently() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
@@ -65,6 +183,50 @@ def test_combined_main_image_workflow_runs_platforms_independently() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_tmall_video_upload_workflow_allows_idempotent_selected_store_rescans() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
)
|
||||
|
||||
workflow = next(
|
||||
item
|
||||
for item in module.workflow_definitions()
|
||||
if item.workflow_id == "product.video_upload"
|
||||
)
|
||||
assert len(workflow.steps) == 1
|
||||
step = workflow.steps[0]
|
||||
assert step.step_id == "upload"
|
||||
assert step.replay_policy == "idempotent"
|
||||
assert step.action.command_entry == "upload_video_to_guanghe.py"
|
||||
assert step.action.command_args == (
|
||||
"--execute",
|
||||
"--keep-browser-open-on-failure",
|
||||
)
|
||||
|
||||
|
||||
def test_jd_video_upload_workflow_allows_idempotent_rescans() -> None:
|
||||
module = ProductCommerceModule.from_catalog(
|
||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
)
|
||||
|
||||
workflow = next(
|
||||
item
|
||||
for item in module.workflow_definitions()
|
||||
if item.workflow_id == "product.jd_video_upload"
|
||||
)
|
||||
assert len(workflow.steps) == 1
|
||||
step = workflow.steps[0]
|
||||
assert step.step_id == "upload"
|
||||
assert step.replay_policy == "idempotent"
|
||||
assert step.action.command_entry == "upload_video_to_jd.py"
|
||||
assert step.action.command_args == (
|
||||
"--anchor-record-id",
|
||||
"recvrXTRWWFXeJ",
|
||||
"--manual-login",
|
||||
"--execute",
|
||||
)
|
||||
|
||||
|
||||
def test_product_runtime_compatibility_package_points_to_flattened_module() -> None:
|
||||
module_root = (
|
||||
PROJECT_ROOT
|
||||
|
||||
Reference in New Issue
Block a user