81 lines
3.1 KiB
Python
81 lines
3.1 KiB
Python
"""Regression test for #97905 / carrier PR #92489.
|
|
|
|
A multiplex ticker (desktop dashboard backend, multiplex gateway) resolves
|
|
``hermes_time`` under the process's own startup profile, then ticks OTHER
|
|
profiles' cron stores via ``set_hermes_home_override()`` + ``use_cron_store()``.
|
|
Before the profile-keyed timezone cache, the first profile's resolved zone
|
|
was process-global, so ``compute_next_run`` / ``create_job`` / ``mark_job_run``
|
|
persisted ``next_run_at`` into the ticked profile's jobs.json with the FOREIGN
|
|
process's UTC offset — e.g. ``0 14 * * *`` for an America/New_York profile
|
|
stored as ``14:00+00:00``, which becomes due at 10:00 ET and fires four hours
|
|
early (#97905; single-profile sibling report #88220).
|
|
|
|
The invariant pinned here: ``next_run_at`` written during a foreign-process
|
|
tick carries the JOB-OWNING profile's configured UTC offset.
|
|
"""
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
import hermes_time
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fresh_tz_cache(monkeypatch):
|
|
monkeypatch.delenv("HERMES_TIMEZONE", raising=False)
|
|
hermes_time.reset_cache()
|
|
yield
|
|
hermes_time.reset_cache()
|
|
|
|
|
|
def test_foreign_process_tick_persists_owning_profile_offset(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""next_run_at persisted during a multiplex tick uses the ticked
|
|
profile's timezone, not the backend process's."""
|
|
backend_home = tmp_path / "backend" # desktop backend's own profile: UTC
|
|
profile_a = tmp_path / "north-caribbean" # job-owning profile: Eastern
|
|
backend_home.mkdir()
|
|
profile_a.mkdir()
|
|
(backend_home / "config.yaml").write_text(
|
|
"timezone: UTC\n", encoding="utf-8"
|
|
)
|
|
(profile_a / "config.yaml").write_text(
|
|
"timezone: America/New_York\n", encoding="utf-8"
|
|
)
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(backend_home))
|
|
|
|
# Backend process resolves its own timezone first (process startup).
|
|
assert hermes_time.now().utcoffset() == datetime.now(
|
|
ZoneInfo("UTC")
|
|
).utcoffset()
|
|
|
|
from hermes_constants import (
|
|
reset_hermes_home_override,
|
|
set_hermes_home_override,
|
|
)
|
|
from cron.jobs import create_job, load_jobs, use_cron_store
|
|
|
|
# Exactly the scoping the multiplex ticker applies per profile
|
|
# (cron/scheduler_provider.py::_tick_profiles).
|
|
token = set_hermes_home_override(str(profile_a))
|
|
try:
|
|
with use_cron_store(profile_a):
|
|
create_job(name="daily-2pm", prompt="x", schedule="0 14 * * *")
|
|
job = load_jobs()[0]
|
|
finally:
|
|
reset_hermes_home_override(token)
|
|
|
|
next_run = datetime.fromisoformat(job["next_run_at"])
|
|
expected_offset = datetime.now(ZoneInfo("America/New_York")).utcoffset()
|
|
assert next_run.utcoffset() == expected_offset, (
|
|
f"next_run_at {job['next_run_at']} persisted with foreign offset "
|
|
f"{next_run.utcoffset()} instead of the owning profile's "
|
|
f"{expected_offset} — fires (offset delta) early (#97905)"
|
|
)
|
|
# And the wall clock honors the cron expression in the profile's zone.
|
|
assert (next_run.hour, next_run.minute) == (14, 0)
|