generated from coulomb/repo-seed
183 lines
5.9 KiB
Python
183 lines
5.9 KiB
Python
"""T25: Schedule pause/resume lifecycle tests.
|
|
|
|
Tests schedule_manager.py against a local embedded Temporal server
|
|
(temporalio[testing] — WorkflowEnvironment.start_local()).
|
|
|
|
Requires no Docker; the Temporal testing library bundles a self-contained server.
|
|
|
|
Run with:
|
|
uv run pytest tests/test_schedule_lifecycle.py -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import uuid
|
|
|
|
import pytest
|
|
from temporalio.client import ScheduleOverlapPolicy
|
|
from temporalio.testing import WorkflowEnvironment
|
|
|
|
from activity_core.models import ActivityDefinition, CronTriggerConfig
|
|
from activity_core.schedule_manager import (
|
|
delete_schedule,
|
|
list_schedules,
|
|
schedule_id,
|
|
upsert_schedule,
|
|
)
|
|
|
|
|
|
def _make_defn(
|
|
*,
|
|
cron: str = "0 9 * * 1-5",
|
|
misfire_policy: str = "skip",
|
|
enabled: bool = True,
|
|
jitter: int = 0,
|
|
) -> ActivityDefinition:
|
|
return ActivityDefinition(
|
|
id=uuid.uuid4(),
|
|
name="test-activity",
|
|
enabled=enabled,
|
|
trigger_config=CronTriggerConfig(
|
|
cron_expression=cron,
|
|
misfire_policy=misfire_policy,
|
|
jitter_seconds=jitter,
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
async def env():
|
|
"""Start a local embedded Temporal server for the test module."""
|
|
async with await WorkflowEnvironment.start_local() as e:
|
|
yield e
|
|
|
|
|
|
# ── T25a: upsert creates a schedule and list_schedules finds it ──────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_upsert_schedule_creates_schedule(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn()
|
|
sid = schedule_id(defn.id)
|
|
|
|
await upsert_schedule(env.client, defn)
|
|
|
|
# The embedded test server's visibility index is eventually consistent —
|
|
# wait briefly for the new schedule to appear in the listing.
|
|
ids: list[str] = []
|
|
for _ in range(10):
|
|
schedules = await list_schedules(env.client)
|
|
ids = [s["schedule_id"] for s in schedules]
|
|
if sid in ids:
|
|
break
|
|
await asyncio.sleep(0.3)
|
|
assert sid in ids, f"Expected schedule {sid!r} in {ids}"
|
|
|
|
# Cleanup
|
|
await delete_schedule(env.client, defn.id)
|
|
|
|
|
|
# ── T25b: upsert with enabled=False creates a paused schedule ────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_upsert_disabled_creates_paused_schedule(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn(enabled=False)
|
|
|
|
await upsert_schedule(env.client, defn)
|
|
|
|
handle = env.client.get_schedule_handle(schedule_id(defn.id))
|
|
desc = await handle.describe()
|
|
assert desc.schedule.state.paused, "Schedule should be paused when enabled=False"
|
|
|
|
await delete_schedule(env.client, defn.id)
|
|
|
|
|
|
# ── T25c: second upsert (enabled=True) unpauses the schedule ────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_upsert_reenables_paused_schedule(env: WorkflowEnvironment) -> None:
|
|
defn_disabled = _make_defn(enabled=False)
|
|
|
|
await upsert_schedule(env.client, defn_disabled)
|
|
|
|
# Re-enable the same activity
|
|
defn_enabled = ActivityDefinition(
|
|
id=defn_disabled.id,
|
|
name=defn_disabled.name,
|
|
enabled=True,
|
|
trigger_config=defn_disabled.trigger_config,
|
|
)
|
|
await upsert_schedule(env.client, defn_enabled)
|
|
|
|
handle = env.client.get_schedule_handle(schedule_id(defn_enabled.id))
|
|
desc = await handle.describe()
|
|
assert not desc.schedule.state.paused, "Schedule should be unpaused after re-enable"
|
|
|
|
await delete_schedule(env.client, defn_enabled.id)
|
|
|
|
|
|
# ── T25d: delete_schedule removes the schedule ───────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_schedule_removes_schedule(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn()
|
|
|
|
await upsert_schedule(env.client, defn)
|
|
await delete_schedule(env.client, defn.id)
|
|
|
|
sid = schedule_id(defn.id)
|
|
ids: list[str] = []
|
|
for _ in range(10):
|
|
schedules = await list_schedules(env.client)
|
|
ids = [s["schedule_id"] for s in schedules]
|
|
if sid not in ids:
|
|
break
|
|
await asyncio.sleep(0.3)
|
|
assert sid not in ids, "Schedule should be gone after delete"
|
|
|
|
|
|
# ── T25e: delete_schedule is idempotent (no-op for non-existent schedule) ────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_schedule_nonexistent_is_noop(env: WorkflowEnvironment) -> None:
|
|
# Should not raise
|
|
await delete_schedule(env.client, uuid.uuid4())
|
|
|
|
|
|
# ── T24: misfire_policy round-trip ───────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_misfire_policy_skip_sets_overlap_skip(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn(misfire_policy="skip")
|
|
await upsert_schedule(env.client, defn)
|
|
|
|
handle = env.client.get_schedule_handle(schedule_id(defn.id))
|
|
desc = await handle.describe()
|
|
assert desc.schedule.policy.overlap == ScheduleOverlapPolicy.SKIP
|
|
|
|
await delete_schedule(env.client, defn.id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_misfire_policy_catchup_sets_overlap_buffer_all(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn(misfire_policy="catchup")
|
|
await upsert_schedule(env.client, defn)
|
|
|
|
handle = env.client.get_schedule_handle(schedule_id(defn.id))
|
|
desc = await handle.describe()
|
|
assert desc.schedule.policy.overlap == ScheduleOverlapPolicy.BUFFER_ALL
|
|
|
|
await delete_schedule(env.client, defn.id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_misfire_policy_compress_sets_overlap_buffer_one(env: WorkflowEnvironment) -> None:
|
|
defn = _make_defn(misfire_policy="compress")
|
|
await upsert_schedule(env.client, defn)
|
|
|
|
handle = env.client.get_schedule_handle(schedule_id(defn.id))
|
|
desc = await handle.describe()
|
|
assert desc.schedule.policy.overlap == ScheduleOverlapPolicy.BUFFER_ONE
|
|
|
|
await delete_schedule(env.client, defn.id)
|