feat(cron): cron.provider config + plugins/cron discovery + resolver
Phase 2 of the pluggable cron-scheduler refactor. Still no call-site changes;
this wires up provider SELECTION with a hard safety net.
Task 2.1: cron.provider config key (hermes_cli/config.py), empty = built-in.
Additive key — deep-merge picks it up into existing configs with no version
bump (verified: load_config() yields the key on a pre-existing config.yaml).
Task 2.2: plugins/cron/__init__.py — discovery machinery cloned near-verbatim
from plugins/memory/__init__.py, retargeted at CronScheduler /
register_cron_scheduler. Bundled (plugins/cron/<name>/) + user
(/plugins/<name>/) dirs, bundled wins collisions. The built-in is
NOT discovered here — it's core, so the fallback can't be removed.
Task 2.3: resolve_cron_scheduler() in cron/scheduler_provider.py — reads
cron.provider and ALWAYS degrades to built-in (missing / unavailable / load
error / typo all fall back with a warning). cron can never be left without a
trigger.
Deviation from plan: the plan's resolver snippet used cfg_get("cron.provider")
(dotted-string form). The real cfg_get signature is cfg_get(cfg, *keys,
default=) — corrected to cfg_get(load_config(), "cron", "provider", default=""),
matching plugins/memory/__init__.py:349. Tests monkeypatch load_config (not
cfg_get) so the real traversal runs.
Tests: default key empty, discovery returns list, unknown load returns None,
and the four resolver paths (empty→builtin, no-section→builtin,
unknown→builtin, unavailable→builtin, available→used). Full tests/cron/: 453
passed; config suite green (additive key, no migration break).
This commit is contained in:
parent
e6ff41ca95
commit
ae8fa11097
4 changed files with 495 additions and 0 deletions
|
|
@ -159,3 +159,106 @@ def test_inprocess_provider_stop_is_noop():
|
|||
from cron.scheduler_provider import InProcessCronScheduler
|
||||
|
||||
assert InProcessCronScheduler().stop() is None
|
||||
|
||||
|
||||
# ── Phase 2: config key, discovery, resolver ─────────────────────────────────
|
||||
|
||||
|
||||
def test_default_config_cron_provider_is_empty():
|
||||
"""The new cron.provider key defaults to empty (= built-in)."""
|
||||
from hermes_cli.config import DEFAULT_CONFIG
|
||||
|
||||
assert DEFAULT_CONFIG["cron"]["provider"] == ""
|
||||
|
||||
|
||||
def test_discover_cron_schedulers_returns_list():
|
||||
"""Discovery returns a list. May be empty — the built-in is core, not
|
||||
discovered, and no bundled non-default provider ships yet."""
|
||||
from plugins.cron import discover_cron_schedulers
|
||||
|
||||
result = discover_cron_schedulers()
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
def test_load_unknown_cron_scheduler_returns_none():
|
||||
from plugins.cron import load_cron_scheduler
|
||||
|
||||
assert load_cron_scheduler("does-not-exist-xyz") is None
|
||||
|
||||
|
||||
def test_resolve_defaults_to_builtin(monkeypatch):
|
||||
"""Empty cron.provider → built-in."""
|
||||
import hermes_cli.config as cfg
|
||||
from cron import scheduler_provider as sp
|
||||
|
||||
monkeypatch.setattr(cfg, "load_config", lambda: {"cron": {"provider": ""}})
|
||||
prov = sp.resolve_cron_scheduler()
|
||||
assert prov.name == "builtin"
|
||||
|
||||
|
||||
def test_resolve_no_cron_section_falls_back_to_builtin(monkeypatch):
|
||||
"""Config with no cron section at all → built-in (cfg_get returns default)."""
|
||||
import hermes_cli.config as cfg
|
||||
from cron import scheduler_provider as sp
|
||||
|
||||
monkeypatch.setattr(cfg, "load_config", lambda: {})
|
||||
prov = sp.resolve_cron_scheduler()
|
||||
assert prov.name == "builtin"
|
||||
|
||||
|
||||
def test_resolve_unknown_provider_falls_back_to_builtin(monkeypatch):
|
||||
"""A named provider that doesn't exist → built-in (cron never dies)."""
|
||||
import hermes_cli.config as cfg
|
||||
from cron import scheduler_provider as sp
|
||||
|
||||
monkeypatch.setattr(cfg, "load_config", lambda: {"cron": {"provider": "nope-not-real"}})
|
||||
prov = sp.resolve_cron_scheduler()
|
||||
assert prov.name == "builtin"
|
||||
|
||||
|
||||
def test_resolve_unavailable_provider_falls_back(monkeypatch):
|
||||
"""A provider that loads but reports is_available()==False → built-in."""
|
||||
import hermes_cli.config as cfg
|
||||
import plugins.cron as pc
|
||||
from cron import scheduler_provider as sp
|
||||
from cron.scheduler_provider import CronScheduler
|
||||
|
||||
class Unavailable(CronScheduler):
|
||||
@property
|
||||
def name(self):
|
||||
return "unavailable"
|
||||
|
||||
def is_available(self):
|
||||
return False
|
||||
|
||||
def start(self, stop_event, **kw):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(cfg, "load_config", lambda: {"cron": {"provider": "unavailable"}})
|
||||
monkeypatch.setattr(pc, "load_cron_scheduler", lambda n: Unavailable())
|
||||
prov = sp.resolve_cron_scheduler()
|
||||
assert prov.name == "builtin"
|
||||
|
||||
|
||||
def test_resolve_available_provider_is_used(monkeypatch):
|
||||
"""A provider that loads and is available is returned (not the fallback)."""
|
||||
import hermes_cli.config as cfg
|
||||
import plugins.cron as pc
|
||||
from cron import scheduler_provider as sp
|
||||
from cron.scheduler_provider import CronScheduler
|
||||
|
||||
class Fake(CronScheduler):
|
||||
@property
|
||||
def name(self):
|
||||
return "fake"
|
||||
|
||||
def is_available(self):
|
||||
return True
|
||||
|
||||
def start(self, stop_event, **kw):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(cfg, "load_config", lambda: {"cron": {"provider": "fake"}})
|
||||
monkeypatch.setattr(pc, "load_cron_scheduler", lambda n: Fake())
|
||||
prov = sp.resolve_cron_scheduler()
|
||||
assert prov.name == "fake"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue