mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
A job may pin its own IANA timezone; its cron expression is then that zone's wall clock, independent of the profile's configured timezone. Absent/None inherits the profile, so an existing jobs.json keeps byte-identical behaviour and needs no migration. The dispatch path needs the pin as much as the compute does. `compute_next_run()` resolves the zone as job pin -> profile zone -> base offset, and every site that writes `next_run_at` now passes it (advance, claim, resume, re-arm, recovery, and the quota/unreachable planners); one missed site re-fires or skips a slot because the stored instant and the expression would be read in different zones. `_evaluate_due_job()` normalizes the stored instant into the JOB's zone, which hands every downstream cron predicate the job's clock, and a pinned job skips `_repair_timezone_shifted_cron` outright. That repair exists for jobs that FOLLOW the profile zone across a zone change: on a pinned job its wall-clock comparison reads a due occurrence as still-in-the-future and re-anchors past it. Failing shape this fixes (profile America/Los_Angeles, job 0 9 * * 1-5 pinned to America/New_York, scanned at 09:00 New York): `get_due_jobs()` returned [] and the job was re-anchored to the next day, i.e. job zones east of the profile zone silently never fired. The regression test drives the public store API and asserts both the fire and the absence of the repair, and is proven red on main ([] and a rewritten instant). Knobs live in the new sibling `cron/jobs_time.py`; `cron/jobs.py` stays over the file-lines ratchet and only gained the call-site plumbing.
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
"""Unit tests for the extracted ``hermes cron`` parser builder.
|
|
|
|
Confirms ``build_cron_parser`` wires up the same subactions, aliases, options,
|
|
and ``func=cmd_cron`` dispatch that lived inline in ``main()`` before the
|
|
god-file Phase 2 extraction.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
|
|
from hermes_cli.subcommands.cron import build_cron_parser
|
|
|
|
|
|
def _sentinel_handler(args): # pragma: no cover - only identity is asserted
|
|
return "cron-handler"
|
|
|
|
|
|
def _build():
|
|
parser = argparse.ArgumentParser(prog="hermes")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
build_cron_parser(subparsers, cmd_cron=_sentinel_handler)
|
|
return parser
|
|
|
|
|
|
|
|
|
|
def test_cron_edit_no_agent_tristate():
|
|
parser = _build()
|
|
# --no-agent -> True, --agent -> False, neither -> None
|
|
assert parser.parse_args(["cron", "edit", "j", "--no-agent"]).no_agent is True
|
|
assert parser.parse_args(["cron", "edit", "j", "--agent"]).no_agent is False
|
|
assert parser.parse_args(["cron", "edit", "j"]).no_agent is None
|
|
|
|
|
|
def test_cron_accept_hooks_flag_on_run_and_tick():
|
|
parser = _build()
|
|
# --accept-hooks is suppressed-default; present only when passed.
|
|
ns = parser.parse_args(["cron", "run", "jid", "--accept-hooks"])
|
|
assert ns.accept_hooks is True
|
|
ns2 = parser.parse_args(["cron", "tick", "--accept-hooks"])
|
|
assert ns2.accept_hooks is True
|
|
|
|
|
|
def test_cron_timezone_flags():
|
|
parser = _build()
|
|
created = parser.parse_args(
|
|
["cron", "create", "0 9 * * *", "hi", "--timezone", "Asia/Hong_Kong"])
|
|
assert created.timezone == "Asia/Hong_Kong"
|
|
|
|
edited = parser.parse_args(["cron", "edit", "j", "--timezone", ""])
|
|
assert edited.timezone == "", "an empty value is the documented clear"
|
|
|
|
assert parser.parse_args(["cron", "edit", "j"]).timezone is None
|