feat: expand module workflows, dynamic config, notifications and console API
This commit is contained in:
@@ -84,9 +84,10 @@ def test_tick_launches_due_workflow_once_and_persists_slot(tmp_path: Path) -> No
|
||||
scheduler.tick(now)
|
||||
|
||||
assert {item[0] for item in launcher.launched} == {
|
||||
"content.marketing_report.daily",
|
||||
"content.summary.weekly",
|
||||
"product.persona.daily",
|
||||
"product.ecommerce_costs.daily",
|
||||
"product.tmall_baibu_apply",
|
||||
}
|
||||
assert set(started) == {item[0] for item in launcher.launched}
|
||||
state = json.loads((tmp_path / "state" / "scheduler" / "state.json").read_text(encoding="utf-8"))
|
||||
@@ -157,6 +158,44 @@ def test_multiple_daily_slots_launch_once_without_overlap(tmp_path: Path) -> Non
|
||||
)
|
||||
|
||||
|
||||
def test_content_metrics_schedule_has_initial_and_retry_slots(tmp_path: Path) -> None:
|
||||
workflow = WorkflowEntry(
|
||||
workflow_id="content.metrics.daily",
|
||||
module="content_marketing",
|
||||
trigger="scheduled",
|
||||
entry="run_all.py",
|
||||
source_task_name="content metrics daily",
|
||||
)
|
||||
schedule = ScheduleEntry(
|
||||
workflow_id=workflow.workflow_id,
|
||||
kind="daily",
|
||||
at="01:00",
|
||||
at_times=("01:00", "05:00"),
|
||||
)
|
||||
catalog = WorkflowCatalog(
|
||||
timezone="Asia/Shanghai",
|
||||
workflows=(workflow,),
|
||||
schedules=(schedule,),
|
||||
)
|
||||
launcher = FakeLauncher()
|
||||
scheduler = PythonScheduler(
|
||||
catalog,
|
||||
tmp_path,
|
||||
launcher=launcher,
|
||||
misfire_grace_seconds=600,
|
||||
)
|
||||
|
||||
scheduler.tick(datetime(2026, 9, 2, 1, 5, tzinfo=SHANGHAI))
|
||||
launcher.launched[0][3].return_code = 0
|
||||
scheduler.tick(datetime(2026, 9, 2, 1, 6, tzinfo=SHANGHAI))
|
||||
scheduler.tick(datetime(2026, 9, 2, 5, 5, tzinfo=SHANGHAI))
|
||||
|
||||
assert [item[1].strftime("%H:%M") for item in launcher.launched] == [
|
||||
"01:00",
|
||||
"05:00",
|
||||
]
|
||||
|
||||
|
||||
def test_misfire_older_than_grace_is_not_launched(tmp_path: Path) -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
launcher = FakeLauncher()
|
||||
@@ -165,11 +204,18 @@ def test_misfire_older_than_grace_is_not_launched(tmp_path: Path) -> None:
|
||||
assert launcher.launched == []
|
||||
|
||||
|
||||
def test_all_configured_schedules_are_enabled() -> None:
|
||||
def test_configured_schedules_disable_cookie_refresh_job() -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
|
||||
assert len(catalog.schedules) == 23
|
||||
assert all(schedule.enabled for schedule in catalog.schedules)
|
||||
assert len(catalog.schedules) == 27
|
||||
assert [
|
||||
schedule.workflow_id for schedule in catalog.schedules if not schedule.enabled
|
||||
] == [
|
||||
"content.metrics.backfill",
|
||||
"content.marketing_report.daily",
|
||||
"content.relogin.weekly",
|
||||
"supply.replenishment.weekly",
|
||||
]
|
||||
|
||||
|
||||
def test_dry_run_reports_due_without_writing_state(tmp_path: Path) -> None:
|
||||
@@ -213,7 +259,7 @@ def test_scheduler_instance_lock_rejects_second_owner(tmp_path: Path) -> None:
|
||||
assert not lock_path.exists()
|
||||
|
||||
|
||||
def test_schedule_business_date_offset_uses_previous_day(tmp_path: Path) -> None:
|
||||
def test_schedule_business_date_offset_uses_two_days_before(tmp_path: Path) -> None:
|
||||
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||
launcher = FakeLauncher()
|
||||
scheduler = PythonScheduler(
|
||||
@@ -223,14 +269,14 @@ def test_schedule_business_date_offset_uses_previous_day(tmp_path: Path) -> None
|
||||
misfire_grace_seconds=600,
|
||||
)
|
||||
|
||||
scheduler.tick(datetime(2026, 8, 1, 16, 5, tzinfo=SHANGHAI))
|
||||
scheduler.tick(datetime(2026, 8, 1, 9, 5, tzinfo=SHANGHAI))
|
||||
|
||||
launched = next(
|
||||
item for item in launcher.launched
|
||||
if item[0] == "shop.jd_self_operated.daily"
|
||||
)
|
||||
assert launched[1].date() == date(2026, 8, 1)
|
||||
assert launched[2] == date(2026, 7, 31)
|
||||
assert launched[2] == date(2026, 7, 30)
|
||||
|
||||
|
||||
def test_product_daily_schedule_passes_previous_business_day(tmp_path: Path) -> None:
|
||||
@@ -284,4 +330,41 @@ def test_subprocess_launcher_passes_explicit_business_date_and_execute(
|
||||
"2026-07-31",
|
||||
"--execute",
|
||||
]
|
||||
assert captured["kwargs"]["env"]["GYXX_SCHEDULE_SLOT"] == "2026-08-01T10:00:00+08:00"
|
||||
assert captured["kwargs"]["env"]["GYXX_SCHEDULE_TIME"] == "10:00"
|
||||
assert "--scheduled" not in captured["argv"]
|
||||
|
||||
|
||||
def test_subprocess_launcher_selects_tmall_no_hyperlink_scope(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class Process:
|
||||
pid = 9091
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
def fake_popen(argv, **kwargs):
|
||||
captured["argv"] = argv
|
||||
return Process()
|
||||
|
||||
monkeypatch.setattr("gyxx_flow.scheduler_service.subprocess.Popen", fake_popen)
|
||||
launcher = SubprocessWorkflowLauncher(tmp_path, tmp_path / "var")
|
||||
process = launcher.launch(
|
||||
"product.tmall_baibu_apply",
|
||||
datetime(2026, 8, 31, 18, 0, tzinfo=SHANGHAI),
|
||||
date(2026, 8, 31),
|
||||
)
|
||||
launcher.close(process)
|
||||
|
||||
assert captured["argv"][-6:] == [
|
||||
"product.tmall_baibu_apply",
|
||||
"--date",
|
||||
"2026-08-31",
|
||||
"--tmall-baibu-import-mode",
|
||||
"without_hyperlinks",
|
||||
"--execute",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user