fix(managed_uv): keep project uv config on the candidate locked sync

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.
This commit is contained in:
Brooklyn Nicholson 2026-08-01 20:02:43 -05:00
parent 3f497e2b4f
commit aaa6a97378
2 changed files with 47 additions and 2 deletions

View File

@ -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:

View File

@ -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,