fix(managed_uv): repair vulnerable SQLite runtime in .venv installs too

repair_vulnerable_runtime() hardcoded <checkout>/venv as the live venv,
so uv-default/dev checkouts installed into .venv got 'not-applicable' on
every hermes update — no repair path ever fired, leaving state.db-class
DBs on journal_mode=DELETE forever (measured 26 ms + ~5.5 fsyncs per
append vs ~0.01 ms under WAL, ~2,600x) while the WAL fallback warning
falsely promised hermes update would repair the runtime.

- _default_live_venv(): target venv/ when it has an interpreter (managed
  layout precedence), fall back to .venv/, keep not-applicable when
  neither exists. Explicit venv_dir arg unchanged; all staging/smoke/
  cutover/rollback machinery untouched.
- Rebuilt against the pruned test suite (main's test-prune waves 1+2
  rewrote test_managed_uv.py, so this reapplies cleanly): 3 new
  TestDefaultLiveVenv tests + repair neutralized in the 6 unit tests
  whose subject is uv install/self-update mechanics — with .venv now
  probed for real, CI's own vulnerable .venv made the unmocked repair
  hook fire inside those tests and re-invoke _install_uv.

33/33 tests green on the pruned suite.
This commit is contained in:
teknium1
2026-07-29 16:55:31 -07:00
committed by Teknium
parent 08e428ac8b
commit 1ec5928012
2 changed files with 88 additions and 1 deletions
+27 -1
View File
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
_RUNTIME_DIR_NAME = ".hermes-runtime"
_VENV_NAME = "venv"
_ALT_VENV_NAME = ".venv"
_REPAIR_LOCK_NAME = "runtime-repair.lock"
# ---------------------------------------------------------------------------
@@ -986,6 +987,31 @@ def _refresh_managed_uv_catalog(uv_bin: str) -> bool:
return after != before
def _default_live_venv(root: Path) -> Path:
"""Return the venv that runtime repair should target for *root*.
Managed installs create ``<checkout>/venv``, but uv-default and dev
checkouts use ``<checkout>/.venv``. Historically only ``venv`` was
probed, so a ``.venv`` install linking a vulnerable SQLite returned
``not-applicable`` on every ``hermes update`` and stayed on
journal_mode=DELETE forever — even though the WAL fallback warning
promises that ``hermes update`` repairs the runtime (issue class:
2,600x slower ``state.db`` appends under DELETE).
``venv`` wins when it holds an interpreter (managed layout takes
precedence); otherwise fall back to ``.venv`` when that one does.
When neither has an interpreter, return the ``venv`` path so the
caller's existing ``not-applicable`` handling fires unchanged.
"""
primary = root / _VENV_NAME
if _venv_python(primary).is_file():
return primary
fallback = root / _ALT_VENV_NAME
if _venv_python(fallback).is_file():
return fallback
return primary
def repair_vulnerable_runtime(
uv_bin: str,
*,
@@ -998,7 +1024,7 @@ def repair_vulnerable_runtime(
post-cutover smoke failures restore the parked venv synchronously.
"""
root = Path(project_root) if project_root is not None else _PROJECT_ROOT
live = Path(venv_dir) if venv_dir is not None else root / _VENV_NAME
live = Path(venv_dir) if venv_dir is not None else _default_live_venv(root)
live_python = _venv_python(live)
if not (root / "pyproject.toml").is_file() or not live_python.is_file():
return RuntimeRepairResult("not-applicable")
+61
View File
@@ -39,6 +39,18 @@ def _runtime_info(
)
def _RRR(status):
"""not-applicable RuntimeRepairResult for tests that neutralize the
repair hook. ensure_uv()/update_managed_uv() invoke runtime repair as a
side effect; unmocked, it probes the REAL checkout's venv/.venv — on CI
the repo .venv links vulnerable SQLite, so repair fires for real and
re-invokes _install_uv (uv-refresh retry), breaking call-count asserts.
"""
from hermes_cli.managed_uv import RuntimeRepairResult
return RuntimeRepairResult(status)
def _make_runtime_install(
tmp_path: Path,
*,
@@ -101,6 +113,7 @@ class TestEnsureUv:
def test_installs_if_missing(self, tmp_path):
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv._install_uv") as mock_install:
# Simulate the installer creating the binary
def fake_install(target):
@@ -166,6 +179,7 @@ class TestEnsureUvUpdateBoundary:
def test_success_usable_as_single_value(self, tmp_path):
_make_executable(tmp_path / "bin" / "uv")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"):
from hermes_cli.managed_uv import ensure_uv
uv_bin = ensure_uv()
@@ -175,6 +189,7 @@ class TestEnsureUvUpdateBoundary:
def test_success_unpacks_as_legacy_two_tuple(self, tmp_path):
_make_executable(tmp_path / "bin" / "uv")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"):
from hermes_cli.managed_uv import ensure_uv
uv_bin, fresh = ensure_uv() # old: uv_bin, fresh_bootstrap = ensure_uv()
@@ -183,6 +198,7 @@ class TestEnsureUvUpdateBoundary:
def test_failure_unpacks_without_raising(self, tmp_path):
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch("hermes_cli.managed_uv._install_uv", side_effect=RuntimeError("network down")):
from hermes_cli.managed_uv import ensure_uv
@@ -221,6 +237,7 @@ class TestEnsureUvWindowsSafe:
# On (mocked) Windows the managed binary is uv.exe.
_make_executable(tmp_path / "bin" / "uv.exe")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv.platform.system", return_value="Windows"):
from hermes_cli.managed_uv import _UvResult, ensure_uv
uv_bin = ensure_uv()
@@ -280,6 +297,7 @@ class TestUpdateManagedUv:
_os.utime(stamp, (old, old))
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="uv 0.2.0")
update_managed_uv()
@@ -847,3 +865,46 @@ class TestRepairRetriesAfterUvRefresh:
assert "replacement environment" in result.detail
assert len(attempts) == 2
assert sentinel.read_text(encoding="utf-8") == "live"
class TestDefaultLiveVenv:
"""_default_live_venv() must cover BOTH install layouts (venv/ and .venv/).
Historically repair hardcoded venv/, so uv-default/.venv checkouts got
'not-applicable' on every hermes update and stayed on journal_mode=DELETE
(2,600x slower state.db appends) while the WAL warning promised repair.
"""
def _checkout(self, tmp_path, *dirs):
root = tmp_path / "checkout"
root.mkdir()
(root / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
for d in dirs:
bin_dir = root / d / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "python").write_text("py", encoding="utf-8")
return root
def test_dot_venv_only_is_targeted(self, tmp_path):
from hermes_cli.managed_uv import _default_live_venv
root = self._checkout(tmp_path, ".venv")
assert _default_live_venv(root) == root / ".venv"
def test_managed_venv_takes_precedence(self, tmp_path):
from hermes_cli.managed_uv import _default_live_venv
root = self._checkout(tmp_path, "venv", ".venv")
assert _default_live_venv(root) == root / "venv"
def test_neither_layout_keeps_not_applicable(self, tmp_path):
from hermes_cli.managed_uv import (
_default_live_venv,
repair_vulnerable_runtime,
)
root = self._checkout(tmp_path)
# Neither venv nor .venv has an interpreter -> repair is not applicable.
assert _default_live_venv(root) == root / "venv"
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "not-applicable"