From 5c121f157fb694fe536ee96eef36d3ce7011eec3 Mon Sep 17 00:00:00 2001 From: slow4cyl <22582211+slow4cyl@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:02:43 -0500 Subject: [PATCH] fix(cron): resolve the no-override store fallback lazily so late env repoints can't write the real jobs file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cron/jobs.py | 20 ++++++++++++++++++-- tests/cron/test_jobs.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cron/jobs.py b/cron/jobs.py index f81e40f2598e..3af18d3b411c 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -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 diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index 6e0b5633cc5d..689413b3d8d6 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -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"