deer-flow/backend/tests/test_scheduled_task_schedules.py
Baldwinzc 486b51eb51
fix(scheduler): normalize once-schedule next_run_at to UTC (#4607)
The once branch of next_run_at returned the run time with the task's
local timezone offset attached, while the cron branch normalizes to UTC.
The value is persisted into scheduled_tasks.next_run_at, and SQLAlchemy's
SQLite dialect discards tzinfo on bind, so a once task declared in a
non-UTC timezone fires shifted by the whole offset (e.g. 8 hours late
for Asia/Shanghai, hours early for negative offsets - also bypassing the
min_once_delay_seconds guard). Postgres timestamptz normalizes on write,
which is why only SQLite deployments are affected.

Align the once branch with the cron branch by converting to UTC before
returning.
2026-07-31 17:27:51 +08:00

74 lines
1.9 KiB
Python

from datetime import UTC, datetime, timedelta
import pytest
from deerflow.scheduler.schedules import (
next_run_at,
normalize_cron_expression,
validate_timezone,
)
def test_validate_timezone_accepts_iana_name():
assert validate_timezone("Asia/Shanghai") == "Asia/Shanghai"
def test_validate_timezone_rejects_unknown_name():
with pytest.raises(ValueError):
validate_timezone("Mars/Base")
def test_normalize_cron_accepts_five_fields():
assert normalize_cron_expression("0 9 * * 1") == "0 9 * * 1"
def test_normalize_cron_rejects_seconds_field():
with pytest.raises(ValueError):
normalize_cron_expression("0 0 9 * * 1")
def test_next_run_at_for_once_returns_none_after_fire_time():
now = datetime(2026, 7, 2, 2, 0, tzinfo=UTC)
result = next_run_at(
"once",
{"run_at": "2026-07-02T01:00:00+00:00"},
"UTC",
now=now,
)
assert result is None
def test_next_run_at_for_once_normalizes_naive_run_at_to_utc():
now = datetime(2026, 7, 31, 0, 0, tzinfo=UTC)
result = next_run_at(
"once",
{"run_at": "2026-08-01T09:00:00"},
"Asia/Shanghai",
now=now,
)
assert result == datetime(2026, 8, 1, 1, 0, tzinfo=UTC)
assert result.utcoffset() == timedelta(0)
def test_next_run_at_for_once_normalizes_aware_run_at_to_utc():
now = datetime(2026, 7, 31, 0, 0, tzinfo=UTC)
result = next_run_at(
"once",
{"run_at": "2026-08-01T09:00:00+08:00"},
"UTC",
now=now,
)
assert result == datetime(2026, 8, 1, 1, 0, tzinfo=UTC)
assert result.utcoffset() == timedelta(0)
def test_next_run_at_for_cron_uses_timezone():
now = datetime(2026, 7, 1, 0, 30, tzinfo=UTC)
result = next_run_at(
"cron",
{"cron": "0 9 * * *"},
"Asia/Shanghai",
now=now,
)
assert result == datetime(2026, 7, 1, 1, 0, tzinfo=UTC)