From aaa6a973781fd5e034c5782ba161c1be123be969 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 1 Aug 2026 20:02:43 -0500 Subject: [PATCH] fix(managed_uv): keep project uv config on the candidate locked sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SQLite runtime repair staged its replacement environment with `uv sync --extra all --locked --no-config`, and managed_python_env also exports UV_NO_CONFIG=1. Both drop `[tool.uv]` from pyproject.toml — including `exclude-newer = "14 days"`, which uv.lock was generated with. uv 0.12 treats the missing setting as a resolver change, re-resolves, and then refuses to write under `--locked`: Resolving despite existing lockfile due to removal of global exclude newer error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. So every repair attempt failed at the dependency-sync gate and reported "replacement environment did not pass dependency and import smoke tests", leaving vulnerable-SQLite installs stuck on journal_mode=DELETE with a guaranteed-failure warning on each `hermes update`. Drop `--no-config` from the sync argv and pop UV_NO_CONFIG from its env. Interpreter provisioning keeps both: only the sync has to agree with the lockfile the project shipped. --- hermes_cli/managed_uv.py | 7 +++-- tests/hermes_cli/test_managed_uv.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/hermes_cli/managed_uv.py b/hermes_cli/managed_uv.py index 60f2cc9d95252..85256e880a229 100644 --- a/hermes_cli/managed_uv.py +++ b/hermes_cli/managed_uv.py @@ -750,6 +750,10 @@ def _stage_candidate_venv( logger.warning("candidate dependency sync refused: uv.lock is missing") _remove_tree(candidate, boundary=runtime_root) return None + # Locked sync must see project [tool.uv] exclude-newer; --no-config / + # UV_NO_CONFIG drops it and uv 0.12+ refuses --locked. + sync_env = dict(env) + sync_env.pop("UV_NO_CONFIG", None) synced = subprocess.run( [ uv_bin, @@ -759,10 +763,9 @@ def _stage_candidate_venv( "--locked", "--python", str(_venv_python(candidate)), - "--no-config", ], cwd=project_root, - env=env, + env=sync_env, check=False, ) if synced.returncode != 0: diff --git a/tests/hermes_cli/test_managed_uv.py b/tests/hermes_cli/test_managed_uv.py index 86f9456d7cefa..8cc34c40c88a6 100644 --- a/tests/hermes_cli/test_managed_uv.py +++ b/tests/hermes_cli/test_managed_uv.py @@ -389,6 +389,48 @@ class TestRuntimeRepair: assert not (root / ".hermes-runtime").exists() mock_install.assert_not_called() + def test_stage_candidate_sync_keeps_uv_project_config(self, tmp_path): + from hermes_cli.managed_uv import _stage_candidate_venv + + root = tmp_path / "checkout" + root.mkdir() + (root / "uv.lock").write_text("# lock\n", encoding="utf-8") + generation = root / ".hermes-runtime" / "python" / "gen" + python = generation / "bin" / "python" + python.parent.mkdir(parents=True) + python.write_text("py", encoding="utf-8") + + calls = [] + + def fake_run(argv, **kwargs): + calls.append((list(argv), kwargs.get("env"))) + return MagicMock(returncode=0) + + with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \ + patch( + "hermes_cli.managed_uv._smoke_candidate_venv", + return_value=(True, "", None), + ), \ + patch("hermes_cli.managed_uv.platform.system", return_value="Linux"): + candidate = _stage_candidate_venv( + "uv", + project_root=root, + generation=generation, + python=python, + ) + + assert candidate is not None + assert len(calls) == 2 + venv_argv, venv_env = calls[0] + sync_argv, sync_env = calls[1] + assert venv_argv[:2] == ["uv", "venv"] + assert "--no-config" in venv_argv + assert venv_env.get("UV_NO_CONFIG") == "1" + assert sync_argv[:2] == ["uv", "sync"] + assert "--locked" in sync_argv + assert "--no-config" not in sync_argv + assert "UV_NO_CONFIG" not in sync_env + def test_failed_candidate_preserves_live_venv(self, tmp_path): from hermes_cli.managed_uv import ( _acquire_repair_lock,