chore: [workflow] 提交工作区改动
This commit is contained in:
@@ -32,6 +32,45 @@ def test_buyin_source_uses_doudian_market_and_cookie_scope():
|
||||
assert xingtu.BUSINESS_COOKIE_DOMAINS == ("jinritemai.com",)
|
||||
|
||||
|
||||
def test_buyin_search_stays_on_market_route_instead_of_clicking_global_nav():
|
||||
xingtu.configure_source("buyin")
|
||||
events = []
|
||||
|
||||
class Locator:
|
||||
def __init__(self, count):
|
||||
self._count = count
|
||||
self.first = self
|
||||
|
||||
def count(self):
|
||||
return self._count
|
||||
|
||||
def wait_for(self, **kwargs):
|
||||
events.append(("wait", kwargs))
|
||||
|
||||
class Page:
|
||||
url = "https://www.douyinec.com/"
|
||||
|
||||
def locator(self, selector):
|
||||
events.append(("locator", selector))
|
||||
return Locator(0)
|
||||
|
||||
def goto(self, url, **kwargs):
|
||||
events.append(("goto", url, kwargs))
|
||||
|
||||
def get_by_text(self, *_args, **_kwargs):
|
||||
raise AssertionError("Buyin must not click the global 找达人 navigation")
|
||||
|
||||
xingtu.select_nickname_search(Page())
|
||||
|
||||
assert events[0] == (
|
||||
"locator",
|
||||
f"{xingtu.BUYIN_SEARCH_INPUT_SELECTOR}:visible",
|
||||
)
|
||||
assert events[1][0] == "goto"
|
||||
assert events[1][1] == xingtu.MARKET_URL
|
||||
assert events[2][0] == "wait"
|
||||
|
||||
|
||||
def test_save_state_creates_cookie_parent_directory(tmp_path, monkeypatch):
|
||||
cookie_file = tmp_path / "nested" / "cookies.json"
|
||||
monkeypatch.setattr(xingtu, "DATA_DIR", tmp_path / "raw")
|
||||
|
||||
@@ -36,13 +36,29 @@ def test_style_plan_rejects_one_erp_code_owned_by_two_styles() -> None:
|
||||
backfill.build_style_plans(
|
||||
{
|
||||
"styles": {
|
||||
"款式甲": {"erp_codes": ["10416"]},
|
||||
"款式乙": {"erp_codes": ["10416"]},
|
||||
"款式甲": {"erp_codes": ["10416"], "tm": {"item_ids": ["tm-1"]}},
|
||||
"款式乙": {"erp_codes": ["10416"], "tm": {"item_ids": ["tm-2"]}},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_style_plan_skips_erp_only_style_until_product_id_is_added() -> None:
|
||||
plans = backfill.build_style_plans(
|
||||
{
|
||||
"styles": {
|
||||
"研发款": {"erp_codes": ["ERP-ONLY"]},
|
||||
"已上架款": {
|
||||
"erp_codes": ["ERP-READY"],
|
||||
"jd": {"spus": ["JD-1"]},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert plans == (backfill.StylePlan("已上架款", ("ERP-READY",)),)
|
||||
|
||||
|
||||
def test_all_shop_filter_selects_every_shop_and_batches_all_codes() -> None:
|
||||
class Frame:
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -210,8 +210,11 @@ def test_erp_style_plan_uses_style_base_codes_and_aliases() -> None:
|
||||
ProductTarget("云栖相机双肩包", 200),
|
||||
]
|
||||
styles = {
|
||||
"盖世m1": {"erp_codes": ["10416", "10455", "10416"]},
|
||||
"星迹2": {"erp_codes": ["10504", "10394"]},
|
||||
"盖世m1": {
|
||||
"erp_codes": ["10416", "10455", "10416"],
|
||||
"tm": {"item_ids": ["tm-1"]},
|
||||
},
|
||||
"星迹2": {"erp_codes": ["10504", "10394"], "tm": {"item_ids": ["tm-2"]}},
|
||||
"云栖": {"erp_codes": []},
|
||||
}
|
||||
|
||||
|
||||
@@ -321,6 +321,83 @@ def test_upload_template_supports_tmall_dialog_import_button(
|
||||
assert chooser.files == str(template)
|
||||
|
||||
|
||||
class _AnnouncementDialog:
|
||||
def __init__(self) -> None:
|
||||
self.dismiss = _LoginLocator()
|
||||
|
||||
def is_visible(self) -> bool:
|
||||
return True
|
||||
|
||||
def inner_text(self, **_kwargs) -> str:
|
||||
return "公告 26年天猫活动通知"
|
||||
|
||||
def count(self) -> int:
|
||||
return 1
|
||||
|
||||
def nth(self, _index: int):
|
||||
return self
|
||||
|
||||
def get_by_role(self, role: str, *, name: str, exact: bool):
|
||||
if role == "button" and name == "我已阅读完成" and exact:
|
||||
return self.dismiss
|
||||
return _LoginLocator(visible=False)
|
||||
|
||||
def get_by_text(self, _label: str, *, exact: bool):
|
||||
return _LoginLocator(visible=False)
|
||||
|
||||
def locator(self, _selector: str):
|
||||
return _LoginLocator(visible=False)
|
||||
|
||||
|
||||
class _AnnouncementPage:
|
||||
frames: list[object] = []
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dialog = _AnnouncementDialog()
|
||||
|
||||
def get_by_role(self, role: str, **_kwargs):
|
||||
if role == "dialog":
|
||||
return self.dialog
|
||||
return _LoginLocator(visible=False)
|
||||
|
||||
|
||||
def test_dismiss_blocking_announcements_closes_only_announcement_dialog() -> None:
|
||||
page = _AnnouncementPage()
|
||||
|
||||
assert baibu._dismiss_blocking_announcements(page) is True
|
||||
assert page.dialog.dismiss.click_count == 1
|
||||
|
||||
|
||||
def test_open_batch_import_dismisses_announcement_before_click(monkeypatch) -> None:
|
||||
events: list[str] = []
|
||||
page = SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_page_text",
|
||||
lambda _page: (
|
||||
"您于2026-09-03 15:00:51执行了商品批量导入操作,"
|
||||
"总数量2件,已成功2件,失败0件,待查看0件。"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(baibu.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_dismiss_blocking_announcements",
|
||||
lambda _page: events.append("dismiss") or True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_click_text",
|
||||
lambda _page, _labels: events.append("click") or True,
|
||||
)
|
||||
|
||||
baibu._open_batch_import(page, _entry())
|
||||
|
||||
assert events[:2] == ["dismiss", "dismiss"]
|
||||
assert events[2:] == ["click"]
|
||||
|
||||
|
||||
def test_open_batch_import_refuses_an_active_previous_operation(monkeypatch) -> None:
|
||||
page = SimpleNamespace()
|
||||
monkeypatch.setattr(
|
||||
@@ -538,6 +615,33 @@ def test_return_to_draft_list_falls_back_to_browser_history(monkeypatch) -> None
|
||||
assert events == ["draft", "back"]
|
||||
|
||||
|
||||
def test_return_to_draft_list_closes_editor_before_clicking_draft_tab(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
events: list[str] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_close_editor",
|
||||
lambda _page: events.append("close") or True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_draft_list_is_ready",
|
||||
lambda _page: events.append("ready") or True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
baibu,
|
||||
"_click_draft_tab",
|
||||
lambda _page: pytest.fail("关闭编辑器后列表已就绪时不得重复点草稿"),
|
||||
)
|
||||
monkeypatch.setattr(baibu.time, "sleep", lambda _seconds: None)
|
||||
|
||||
baibu._return_to_draft_list(SimpleNamespace(), _entry())
|
||||
|
||||
assert events == ["close", "ready"]
|
||||
|
||||
|
||||
def test_tmall_config_enables_draft_completion_for_only_old_all_3c() -> None:
|
||||
old_all_3c = baibu._load_entry("old_all_3c")
|
||||
old_all_bag = baibu._load_entry("old_all_bag")
|
||||
|
||||
+23
-18
@@ -11,15 +11,15 @@ from gyxx_flow.catalog import CatalogError, WorkflowCatalog
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_catalog_maps_27_project_scheduled_tasks() -> None:
|
||||
def test_catalog_maps_29_project_scheduled_tasks() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
scheduled = catalog.scheduled_workflows()
|
||||
|
||||
assert len(scheduled) == 27
|
||||
assert len({item.source_task_name for item in scheduled}) == 27
|
||||
assert len(scheduled) == 29
|
||||
assert len({item.source_task_name for item in scheduled}) == 29
|
||||
assert Counter(item.module for item in scheduled) == {
|
||||
"content_marketing": 9,
|
||||
"product_commerce": 10,
|
||||
"product_commerce": 12,
|
||||
"shop_intelligence": 4,
|
||||
"supply_chain": 4,
|
||||
}
|
||||
@@ -30,10 +30,7 @@ def test_catalog_schedules_douyin_price_appeal() -> None:
|
||||
schedule = catalog.schedule_for("shop.douyin_price_appeal")
|
||||
|
||||
assert len(catalog.workflows) == 29
|
||||
assert tuple(item.workflow_id for item in catalog.manual_workflows()) == (
|
||||
"product.video_upload",
|
||||
"product.jd_video_upload",
|
||||
)
|
||||
assert catalog.manual_workflows() == ()
|
||||
assert "shop.douyin_price_appeal" in {
|
||||
schedule.workflow_id for schedule in catalog.schedules
|
||||
}
|
||||
@@ -42,13 +39,13 @@ def test_catalog_schedules_douyin_price_appeal() -> None:
|
||||
assert schedule.effective_times == ("08:00", "16:00", "22:00")
|
||||
|
||||
|
||||
def test_tmall_video_upload_is_an_idempotent_store_selected_manual_workflow() -> None:
|
||||
def test_tmall_video_upload_is_an_idempotent_store_selected_scheduled_workflow() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
workflow = next(
|
||||
item for item in catalog.workflows if item.workflow_id == "product.video_upload"
|
||||
)
|
||||
|
||||
assert workflow.trigger == "manual"
|
||||
assert workflow.trigger == "scheduled"
|
||||
assert [step.step_id for step in workflow.steps] == ["upload"]
|
||||
step = workflow.steps[0]
|
||||
assert step.entry == "upload_video_to_guanghe.py"
|
||||
@@ -60,11 +57,15 @@ def test_tmall_video_upload_is_an_idempotent_store_selected_manual_workflow() ->
|
||||
assert step.timeout_seconds == 21600
|
||||
assert workflow.topic_config is not None
|
||||
assert workflow.topic_config.default_keyword == "我的夏日焕新清单"
|
||||
with pytest.raises(KeyError):
|
||||
catalog.schedule_for(workflow.workflow_id)
|
||||
schedule = catalog.schedule_for(workflow.workflow_id)
|
||||
assert schedule.kind == "interval_days"
|
||||
assert schedule.at == "14:00"
|
||||
assert schedule.every_days == 3
|
||||
assert schedule.anchor_date == "2026-09-04"
|
||||
assert schedule.enabled is True
|
||||
|
||||
|
||||
def test_jd_video_upload_is_an_idempotent_manual_workflow() -> None:
|
||||
def test_jd_video_upload_is_an_idempotent_scheduled_workflow() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
workflow = next(
|
||||
item
|
||||
@@ -72,7 +73,7 @@ def test_jd_video_upload_is_an_idempotent_manual_workflow() -> None:
|
||||
if item.workflow_id == "product.jd_video_upload"
|
||||
)
|
||||
|
||||
assert workflow.trigger == "manual"
|
||||
assert workflow.trigger == "scheduled"
|
||||
assert [step.step_id for step in workflow.steps] == ["upload"]
|
||||
step = workflow.steps[0]
|
||||
assert step.entry == "upload_video_to_jd.py"
|
||||
@@ -84,8 +85,12 @@ def test_jd_video_upload_is_an_idempotent_manual_workflow() -> None:
|
||||
)
|
||||
assert step.replay_policy == "idempotent"
|
||||
assert step.timeout_seconds == 21600
|
||||
with pytest.raises(KeyError):
|
||||
catalog.schedule_for(workflow.workflow_id)
|
||||
schedule = catalog.schedule_for(workflow.workflow_id)
|
||||
assert schedule.kind == "interval_days"
|
||||
assert schedule.at == "14:00"
|
||||
assert schedule.every_days == 3
|
||||
assert schedule.anchor_date == "2026-09-04"
|
||||
assert schedule.enabled is True
|
||||
|
||||
|
||||
def test_tmall_baibu_apply_is_a_six_step_hourly_workflow() -> None:
|
||||
@@ -181,13 +186,13 @@ def test_every_scheduled_workflow_has_one_valid_asia_shanghai_schedule() -> None
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
|
||||
assert catalog.timezone == "Asia/Shanghai"
|
||||
assert len(catalog.schedules) == 27
|
||||
assert len(catalog.schedules) == 29
|
||||
for workflow in catalog.scheduled_workflows():
|
||||
schedule = catalog.schedule_for(workflow.workflow_id)
|
||||
assert schedule.workflow_id == workflow.workflow_id
|
||||
assert schedule.kind in {"daily", "weekly", "monthly", "interval_days"}
|
||||
assert schedule.at.count(":") == 1
|
||||
assert sum(schedule.enabled for schedule in catalog.schedules) == 23
|
||||
assert sum(schedule.enabled for schedule in catalog.schedules) == 25
|
||||
|
||||
|
||||
def test_scheduled_workflows_use_python_entries_and_content_tasks_are_graphs() -> None:
|
||||
|
||||
@@ -51,7 +51,7 @@ def test_default_cli_registry_exposes_all_project_scheduled_workflows(
|
||||
scheduled_ids = {
|
||||
entry.workflow_id for entry in registry.catalog.scheduled_workflows()
|
||||
}
|
||||
assert len(scheduled_ids) == 27
|
||||
assert len(scheduled_ids) == 29
|
||||
assert scheduled_ids == {
|
||||
schedule.workflow_id for schedule in registry.catalog.schedules
|
||||
}
|
||||
|
||||
@@ -24,11 +24,8 @@ def test_product_module_registers_scheduled_and_manual_entries() -> None:
|
||||
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) == 10
|
||||
assert PRODUCT_MANUAL_WORKFLOW_IDS == (
|
||||
"product.video_upload",
|
||||
"product.jd_video_upload",
|
||||
)
|
||||
assert len(PRODUCT_SCHEDULED_WORKFLOW_IDS) == 12
|
||||
assert PRODUCT_MANUAL_WORKFLOW_IDS == ()
|
||||
|
||||
|
||||
def test_tmall_baibu_import_mode_filters_hyperlink_steps_and_rewires_dependencies() -> None:
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_catalog_and_scripts_work_after_project_and_data_roots_move(
|
||||
|
||||
assert workflows.returncode == 0, workflows.stderr
|
||||
assert scripts.returncode == 0, scripts.stderr
|
||||
assert len(json.loads(workflows.stdout)) == 27
|
||||
assert len(json.loads(workflows.stdout)) == 29
|
||||
commands = json.loads(scripts.stdout)
|
||||
assert len(commands) == 41
|
||||
assert len({row["command_id"] for row in commands}) == 41
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_python_scheduler_status_is_project_owned_and_cross_platform(tmp_path: P
|
||||
assert exit_code == 0
|
||||
payload = json.loads(output.getvalue())
|
||||
assert payload["timezone"] == "Asia/Shanghai"
|
||||
assert payload["scheduled_count"] == 27
|
||||
assert payload["scheduled_count"] == 29
|
||||
assert payload["state"]["schema_version"] == 1
|
||||
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ def test_misfire_older_than_grace_is_not_launched(tmp_path: Path) -> None:
|
||||
def test_configured_schedules_disable_cookie_refresh_job() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
|
||||
assert len(catalog.schedules) == 27
|
||||
assert len(catalog.schedules) == 29
|
||||
assert [
|
||||
schedule.workflow_id for schedule in catalog.schedules if not schedule.enabled
|
||||
] == [
|
||||
|
||||
@@ -80,8 +80,8 @@ def test_default_registry_covers_every_executable_workflow_entry() -> None:
|
||||
for entry in entries:
|
||||
workflow_entries.add(f"{workflow['module']}:{entry}")
|
||||
|
||||
assert scheduled_workflows == 27
|
||||
assert manual_workflows == 2
|
||||
assert scheduled_workflows == 29
|
||||
assert manual_workflows == 0
|
||||
assert len(workflow_entries) == 44
|
||||
assert len(catalog.scripts) == 49
|
||||
assert workflow_entries < set(catalog.script_ids)
|
||||
|
||||
@@ -47,8 +47,8 @@ def test_deployment_documents_one_scheduler_and_external_data_root() -> None:
|
||||
assert "GYXX_DATA_ROOT" in guide
|
||||
assert "/var/lib/gyxx-flow" in guide
|
||||
assert "Windows Task Scheduler" in guide
|
||||
assert len(payload["schedules"]) == 27
|
||||
assert sum(item.get("enabled", True) for item in payload["schedules"]) == 23
|
||||
assert len(payload["schedules"]) == 29
|
||||
assert sum(item.get("enabled", True) for item in payload["schedules"]) == 25
|
||||
|
||||
|
||||
def test_three_canonical_documents_cover_architecture_deployment_and_operations() -> None:
|
||||
|
||||
Reference in New Issue
Block a user