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:
parent
3f497e2b4f
commit
aaa6a97378
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue