fix(cron): resolve the no-override store fallback lazily so late env repoints can't write the real jobs file

Complements ec0227b43 (context-scoped cron store): the ContextVar override
is the right tool for deliberate cross-profile scoping, but with no
override active, _current_cron_store() returned the import-time constants —
so a HERMES_HOME set AFTER cron.jobs import (the filed incident: test
fixtures patching the env too late) still read/wrote the user's real
jobs.json. The fallback now resolves the active profile home fresh via
get_hermes_home() (context-local override, then env) and scopes the store
to it; when the home is unchanged since import, the exact module-level
constants are returned as before (zero change in the common path, and they
remain the documented compatibility surface). use_cron_store() still wins.

Three tests: late env repoint scopes the store; unchanged home returns the
import-time constants identically; an active use_cron_store() override
beats the env.
This commit is contained in:
slow4cyl 2026-07-16 12:02:43 -05:00 committed by kshitij
parent 0f102fa4dc
commit 5c121f157f
2 changed files with 51 additions and 2 deletions

View file

@ -116,11 +116,27 @@ _cron_store_override: ContextVar[Optional[_CronStorePaths]] = ContextVar(
def _current_cron_store() -> _CronStorePaths:
"""Return paths pinned to this execution context's profile."""
"""Return paths pinned to this execution context's profile.
With no use_cron_store() override active, the active profile home is
resolved FRESH via get_hermes_home() (context-local override, then the
HERMES_HOME env var) instead of trusting the import-time capture. A test
or embedder that re-points HERMES_HOME after this module was imported
therefore reads/writes ITS OWN store not whatever jobs.json the import
happened to freeze (the filed incident: fixtures that patched the env too
late silently rewrote the user's real jobs file). The module-level
constants keep their import-time values as the documented compatibility
surface, and the common path (home unchanged since import) still returns
them unchanged.
"""
override = _cron_store_override.get()
if override is not None:
return override
return _CronStorePaths(CRON_DIR, JOBS_FILE, OUTPUT_DIR)
home = get_hermes_home().resolve()
if home == HERMES_DIR:
return _CronStorePaths(CRON_DIR, JOBS_FILE, OUTPUT_DIR)
cron_dir = home / "cron"
return _CronStorePaths(cron_dir, cron_dir / "jobs.json", cron_dir / "output")
@contextlib.contextmanager

View file

@ -1945,3 +1945,36 @@ class TestClaimDispatch:
# At minimum, the good job's record is still intact (no corruption from the bad neighbor)
loaded = {j["id"]: j for j in load_jobs()}
assert "good" in loaded
class TestLateEnvRepointScopesStore:
"""A HERMES_HOME set AFTER cron.jobs import must scope the store even
without use_cron_store(): fixtures that patch the environment too late
previously read/wrote the import-time jobs.json the user's real file."""
def test_late_env_repoint_scopes_store(self, tmp_path, monkeypatch):
import cron.jobs as jobs
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
store = jobs._current_cron_store()
expected = tmp_path.resolve() / "cron"
assert store.cron_dir == expected
assert store.jobs_file == expected / "jobs.json"
assert store.output_dir == expected / "output"
# the import-time compatibility constants are untouched
assert jobs.JOBS_FILE != store.jobs_file
def test_unchanged_home_returns_import_time_constants(self, monkeypatch):
import cron.jobs as jobs
monkeypatch.setenv("HERMES_HOME", str(jobs.HERMES_DIR))
store = jobs._current_cron_store()
assert store.jobs_file is jobs.JOBS_FILE
def test_use_cron_store_override_still_wins(self, tmp_path, monkeypatch):
import cron.jobs as jobs
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "env-home"))
with jobs.use_cron_store(tmp_path / "override-home"):
store = jobs._current_cron_store()
assert store.jobs_file == (tmp_path / "override-home").resolve() / "cron" / "jobs.json"