diff --git a/cron/jobs.py b/cron/jobs.py index 3af18d3b411c..d3bb624128c3 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -115,26 +115,40 @@ _cron_store_override: ContextVar[Optional[_CronStorePaths]] = ContextVar( ) +# Import-time snapshot of the compatibility constants, so deliberate +# re-pointing of the module surface (monkeypatched CRON_DIR/JOBS_FILE/ +# OUTPUT_DIR — the documented escape hatch existing tests/embedders use) +# is distinguishable from the constants merely being stale. +_IMPORT_STORE = _CronStorePaths(CRON_DIR, JOBS_FILE, OUTPUT_DIR) + + def _current_cron_store() -> _CronStorePaths: """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. + Precedence, most explicit first: + + 1. an active use_cron_store() override (ContextVar); + 2. deliberately re-pointed module constants — if CRON_DIR/JOBS_FILE/ + OUTPUT_DIR no longer match their import-time values, someone chose + the documented process-wide compatibility surface; honor it; + 3. the ACTIVE profile home, resolved fresh via get_hermes_home() + (context-local override, then the HERMES_HOME env var) — so a test + or embedder that re-points HERMES_HOME after this module was + imported 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); + 4. the import-time constants (home unchanged since import — the common + path, returned unchanged). """ override = _cron_store_override.get() if override is not None: return override + live_constants = _CronStorePaths(CRON_DIR, JOBS_FILE, OUTPUT_DIR) + if live_constants != _IMPORT_STORE: + return live_constants home = get_hermes_home().resolve() if home == HERMES_DIR: - return _CronStorePaths(CRON_DIR, JOBS_FILE, OUTPUT_DIR) + return live_constants cron_dir = home / "cron" return _CronStorePaths(cron_dir, cron_dir / "jobs.json", cron_dir / "output") diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index 689413b3d8d6..ea265ae8f88c 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -1978,3 +1978,16 @@ class TestLateEnvRepointScopesStore: 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" + + def test_patched_compatibility_constants_beat_env(self, tmp_path, monkeypatch): + """Deliberately re-pointed module constants are the documented + process-wide escape hatch — they win over a repointed HERMES_HOME.""" + import cron.jobs as jobs + + patched_dir = tmp_path / "patched-cron" + monkeypatch.setattr(jobs, "CRON_DIR", patched_dir) + monkeypatch.setattr(jobs, "JOBS_FILE", patched_dir / "jobs.json") + monkeypatch.setattr(jobs, "OUTPUT_DIR", patched_dir / "output") + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "env-home")) + store = jobs._current_cron_store() + assert store.jobs_file == patched_dir / "jobs.json"