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"