diff --git a/tests/conftest.py b/tests/conftest.py index 8cc29485..ec546903 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,45 @@ def _head(url: str, *args: object, **kwargs: object) -> SimpleNamespace: monkeypatch.setattr(version_service.httpx, "head", _head) +@pytest.fixture(autouse=True) +def _redirect_global_state_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Keep auto-update bookkeeping out of the developer's real config directory. + + Both writers below resolve through ``platformdirs.user_config_dir``, so + neither honours ``--config-dir``: they always address the machine's own + files. That is correct for the shipped CLI -- the version cache and a + pending update are per-machine, not per-project -- and exactly why the + test suite has to redirect them rather than the production code. + + ``auto_update._get_cache_path`` backs ``version_cache.json``. Any test + reaching the real ``_write_cache`` -- ``TestMaybeAutoUpdate`` has two that + mock ``_read_cache`` but not the write -- stamps the canonical fake latest + version ``1.0.0`` into it. The CLI then believes for a full + ``AUTO_UPDATE_CHECK_INTERVAL`` (1 hour) that a release tag which does not + exist is available, prints an update banner on every command, and fails + the reinstall: running ``make test`` broke auto-update for whoever ran it. + + ``update_runner.state_dir`` backs the deferred-update marker / exit / log + files. No test reaches it today (the deferral tests mock + ``request_deferred_update``, and ``should_defer()`` is False off Windows), + but it is the same latent bug: on Windows ``should_defer()`` defaults to + True, and ``report_finished_deferred_update`` -- which runs ahead of every + skip gate -- consumes and unlinks a genuine pending report while reading + it, so a developer's own update outcome would vanish into a test run. + + Redirecting the module attributes (not the top-level import in + ``test_auto_update.py``, which ``TestGetCachePath`` still exercises + against the real resolver) covers tests written later that forget to mock + the writer. ``test_suite_never_resolves_to_the_real_user_cache`` fails if + this fixture is removed. + """ + from keboola_agent_cli import auto_update, update_runner + + cache_file = tmp_path / "version_cache.json" + monkeypatch.setattr(auto_update, "_get_cache_path", lambda: cache_file) + monkeypatch.setattr(update_runner, "state_dir", lambda: tmp_path) + + @pytest.fixture def tmp_config_dir(tmp_path: Path) -> Path: """Provide a temporary directory for configuration files.""" diff --git a/tests/test_auto_update.py b/tests/test_auto_update.py index b8abb56d..8e265504 100644 --- a/tests/test_auto_update.py +++ b/tests/test_auto_update.py @@ -682,6 +682,38 @@ def test_returns_path_with_filename(self): assert path.name == "version_cache.json" assert "keboola-agent-cli" in str(path) + def test_suite_never_resolves_to_the_real_user_cache(self): + """Guard for the `_redirect_global_state_paths` autouse fixture. + + `_get_cache_path` resolves through `platformdirs`, so it is NOT + `--config-dir`-aware and points at the developer's own + `version_cache.json`. Two tests in `TestMaybeAutoUpdate` reach the + real `_write_cache` (they mock `_read_cache`, not the write), which + stamped the canonical fake latest version `1.0.0` into that file -- + after which every `kbagent` command on the machine spent an hour + (`AUTO_UPDATE_CHECK_INTERVAL`) trying to install a release tag that + does not exist. The conftest fixture redirects the module attribute + per test; this asserts it is actually in force, so deleting the + fixture fails here instead of silently on someone's laptop. + + Uses the module attribute, not the name imported at the top of this + file -- the latter is bound to the original function object and is + deliberately left unpatched for the test above. + """ + import platformdirs + + from keboola_agent_cli import update_runner + + real_dir = Path(platformdirs.user_config_dir("keboola-agent-cli")).resolve() + assert real_dir != auto_update_module._get_cache_path().resolve().parent + # `update_runner.state_dir` resolves through the very same call and backs + # the deferred-update marker / exit / log files. `should_defer()` is True + # by default on Windows, so a test reaching the scheduler there would + # write real state -- and `report_finished_deferred_update`, which runs + # before every skip gate, unlinks a genuine pending report while reading + # it. Same class of bug, so the fixture covers both. + assert real_dir != update_runner.state_dir().resolve() + # --------------------------------------------------------------------------- # `kbagent changelog` does not duplicate "What's new" output