fix(update): let the Windows repoint run; refuse a minor-line jump

The self-lock/holder preflight (#99711) deferred the repair on the theory
that the updater's own mapped python.exe makes the venv rename impossible.
Live on windows-latest, a process executing from venv\Scripts\python.exe
(and the repo's real .venv with cp311 .pyd extensions loaded) does NOT block
the rename; what blocks it with WinError 5 is any ordinary handle under the
tree: a process cwd, an open file, a sync client. Since sys.executable is
always under the venv on Windows, the deferral fired on every run and no
Windows install could repair from `hermes update`. Retire it and its tests;
the pyvenv.cfg repoint needs no rename and has none of that exposure. The
wine2e lane now runs the cutover probes instead.

The repoint keeps the live venv's site-packages, so provisioning's
fall-forward to the next minor (3.11 -> 3.12, #76106) must not be pointed at
a cp311 tree: refuse before touching pyvenv.cfg. The live Windows test holds
the venv the way the field does (a child with cwd inside it), asserts the
rename path fails (the symptom) and the repoint succeeds; the rollback and
minor-guard tests run on every host.
This commit is contained in:
teknium1
2026-09-17 00:18:14 -07:00
committed by Teknium
parent 5a49d1de3d
commit 290bdc3c76
4 changed files with 92 additions and 370 deletions

View File

@@ -60,7 +60,7 @@ jobs:
tests/hermes_cli/test_venv_holder_windows_live.py \
tests/hermes_cli/test_taskkill_identity_windows_live.py \
tests/hermes_cli/test_git_trampoline_windows_live.py \
"tests/hermes_cli/test_managed_uv.py::TestWindowsRuntimeSelfLock" \
tests/hermes_cli/test_managed_uv_windows_cutover.py \
-o addopts= -v -p no:cacheprovider
- name: Run Telegram CLOSE-WAIT reconnect live E2E (#87057)

View File

@@ -5,8 +5,8 @@ is shared. Runtime repair therefore uses an install-scoped store under
``<checkout>/.hermes-runtime/python``. A vulnerable interpreter is never reinstalled in place: a
new immutable Python generation is provisioned and a relocatable sibling venv built and smoke-tested
from it. POSIX installs cut over with same-filesystem directory renames; Windows installs
atomically repoint the live venv's ``pyvenv.cfg`` at the new generation instead, because the
updater itself executes from that venv and Windows refuses to rename it.
atomically repoint the live venv's ``pyvenv.cfg`` at the new generation instead, because any
open handle under the venv (cwd, open file, sync client) makes Windows refuse the rename.
"""
from __future__ import annotations
@@ -779,18 +779,33 @@ def _cut_over_windows_runtime_config(
candidate: Path,
*,
live: Path,
current: SQLiteRuntimeInfo,
candidate_info: SQLiteRuntimeInfo,
) -> tuple[bool, bool, SQLiteRuntimeInfo | None, str]:
"""Repoint a live Windows venv without renaming its locked directory.
"""Repoint a live Windows venv instead of renaming its directory.
Windows refuses to rename a directory while any handle is open inside it: a process whose
cwd is under the venv, an open file, an Explorer window, a sync client (OneDrive) or a
scanner. The updater cannot enumerate those holders, and the park rename in
``_cut_over_candidate`` failed with ``WinError 5`` in the field on every retry (#93032).
Mapped executable images do NOT block the rename (proven live on windows-latest), so the
updater running from the venv was never the problem.
``venv\\Scripts\\python.exe`` is a launcher that reads ``home`` from ``pyvenv.cfg`` on every
start, so replacing that text file redirects every fresh process to the candidate generation
while the updater keeps executing from the directory Windows refuses to rename (#93032).
start, so atomically replacing that one file redirects every fresh process to the candidate
generation with no directory rename at all.
The live venv keeps its own ``site-packages``, so the candidate must stay on the same
``major.minor`` line: compiled extensions built for one minor do not import under the next.
The second return value reports whether the live config still references the candidate
generation. Callers must preserve that generation if a failed smoke test could not restore
the original config.
"""
if current.python_version[:2] != candidate_info.python_version[:2]:
return False, False, None, (
f"a Python {_dotted(candidate_info.python_version[:2])} runtime cannot be repointed "
f"under a {_dotted(current.python_version[:2])} venv's site-packages")
live_config = live / "pyvenv.cfg"
candidate_config = candidate / "pyvenv.cfg"
try:
@@ -867,72 +882,6 @@ def _release_repair_lock(lock: _RepairLock) -> None:
def _windows_runtime_holders() -> tuple[bool, str]:
if platform.system() != "Windows":
return False, ""
main_module = sys.modules.get("hermes_cli.main")
detector = getattr(main_module, "_detect_venv_python_processes", None)
if detector is None:
return True, "cannot verify Windows venv holders from this update context"
try:
holders = detector()
except Exception as exc:
return True, f"could not verify Windows venv holders: {exc}"
if holders:
pids = ", ".join(str(item[0]) for item in holders[:6])
return True, f"other Hermes processes still hold the venv (PID {pids})"
return False, ""
def _windows_runtime_self_lock(live: Path) -> tuple[bool, str]:
"""Detect the one holder the generic scan is blind to: THIS process.
``_detect_venv_python_processes`` excludes the calling process and its ancestors on purpose
(``hermes update`` itself runs from the venv python), which is correct for the dependency-sync
path where only a *loaded* ``.pyd`` image blocks the rewrite and a fresh child dodges it.
For the whole-venv park rename that exemption is fatal: Windows keeps the image of any executable a
running process was started from mapped until that process exits, so a directory containing the
updater's own ``python.exe`` (or a waiting ``hermes.exe`` launcher ancestor) can never be renamed from
inside the updater. The retry loop in ``_cut_over_candidate`` cannot help against that — the lock is
structural, not transient (#93032).
"""
if platform.system() != "Windows":
return False, ""
try:
live_res = str(live.resolve())
except OSError:
live_res = str(live)
live_res = live_res.lower().rstrip(os.sep) + os.sep
def _under_live(path_value: str | None) -> bool:
if not path_value:
return False
try:
resolved = str(Path(path_value).resolve()).lower()
except (OSError, ValueError):
resolved = str(path_value).lower()
return resolved.startswith(live_res)
why = "Windows cannot rename a directory while a process executes from inside it"
exe = sys.executable
if _under_live(exe):
return True, f"the updater itself runs from the live venv it must replace ({exe}); {why}"
# Belt-and-braces: the venv\Scripts\hermes.exe launcher stays mapped while it waits for this
# child, so an ancestor started from the venv blocks the rename too.
with contextlib.suppress(Exception):
import psutil
for anc in psutil.Process().parents():
try:
anc_exe = anc.exe()
except Exception:
continue
if _under_live(anc_exe):
return True, (
f"ancestor process PID {anc.pid} runs from the live venv ({anc_exe}); {why}")
return False, ""
def _uv_version_string(uv_bin: str) -> str:
"""Return ``uv --version`` output, or ``""`` when it cannot be read."""
try:
@@ -1017,34 +966,6 @@ def _result(
def _repair_windows_preflight(
root: Path, live: Path, current: SQLiteRuntimeInfo) -> RuntimeRepairResult | None:
"""Defer the repair when Windows holders make the venv rename impossible; else ``None``."""
blocked, detail = _windows_runtime_holders()
if blocked:
print(f" ⚠ SQLite runtime repair deferred: {detail}")
return _result("skipped", current, detail)
self_locked, self_detail = _windows_runtime_self_lock(live)
if self_locked:
# Structural, not transient: this process maps the live venv's own executable, so the
# park rename fails identically on every run. Defer BEFORE provisioning — a candidate
# staged for a cutover that can never run only leaks an incomplete generation.
for line in (
f" ⚠ SQLite runtime repair deferred: {self_detail}.",
# See #93032.
" Retrying `hermes update` from inside this venv cannot help: "
"the mapped executable is released only when this process exits.",
" To complete the repair, run the updater from an interpreter "
"that lives outside this venv, e.g.:",
f" cd {root}",
" <system Python> -m hermes_cli.main update",
" Sessions stay protected meanwhile: Hermes keeps databases "
"out of WAL mode on this SQLite build."):
print(line)
return _result("skipped", current, self_detail)
return None
def _repair_under_lock(
uv_bin: str, *, root: Path, live: Path, live_python: Path, runtime_root: Path
) -> RuntimeRepairResult:
@@ -1084,7 +1005,7 @@ def _repair_under_lock(
generation_in_use = False
if platform.system() == "Windows":
cut_over, generation_in_use, final_info, cutover_detail = _cut_over_windows_runtime_config(
candidate, live=live)
candidate, live=live, current=current, candidate_info=candidate_info)
else:
cut_over, backup, final_info, cutover_detail = _cut_over_candidate(
candidate, project_root=root, live=live)
@@ -1116,8 +1037,8 @@ def repair_vulnerable_runtime(
Every failure before cutover leaves the live venv untouched. POSIX cuts over with directory
renames and restores the parked venv synchronously on failure; Windows repoints the live
venv's ``pyvenv.cfg`` instead (the updater runs from that venv, so the directory can never
be renamed) and restores the original config on a failed smoke.
venv's ``pyvenv.cfg`` instead (any open handle under the venv makes a directory rename fail
there) and restores the original config on a failed smoke.
"""
root = Path(project_root) if project_root is not None else _PROJECT_ROOT
live = Path(venv_dir) if venv_dir is not None else _default_live_venv(root)
@@ -1134,9 +1055,6 @@ def repair_vulnerable_runtime(
# See #73109.
_sweep_stale_runtime_backups(live, root=root)
return _result("safe", current, sqlite_after=current.sqlite_version_string)
deferred = _repair_windows_preflight(root, live, current)
if deferred is not None:
return deferred
runtime_root = root / _RUNTIME_DIR_NAME
lock = _acquire_repair_lock(runtime_root)
if lock is None:

View File

@@ -1479,148 +1479,3 @@ class TestVenvPythonUpdateBoundary:
expected = Path("/opt/hermes/venv/Scripts/python.exe") \
if sys.platform == "win32" else Path("/opt/hermes/venv/bin/python")
assert _venv_python(Path("/opt/hermes/venv")) == expected
class TestWindowsRuntimeSelfLock:
"""The repair pre-flight must see the ONE holder the generic scan hides:
the updater itself (#93032).
A CLI ``hermes update`` runs from the venv's own python, and
``_detect_venv_python_processes`` excludes the calling process and its
ancestors on purpose (correct for the dependency-sync path). For the
whole-venv park rename that exemption is fatal on Windows: a directory
containing an executable mapped by a running process cannot be renamed,
so the cutover retries burn out against a lock that cannot be released
while the updater lives. The repair must detect the self-lock and defer
with honest guidance instead of provisioning a candidate for a doomed
rename.
"""
def _checkout(self, tmp_path):
root, live, sentinel = _make_runtime_install(tmp_path)
# Windows-layout interpreter so sys.executable can point inside the
# live venv on any host (the detector only string-compares paths).
scripts_python = live / "Scripts" / "python.exe"
scripts_python.parent.mkdir(parents=True, exist_ok=True)
scripts_python.write_text("live interpreter", encoding="utf-8")
return root, live, sentinel, scripts_python
def test_self_lock_defers_repair_before_provisioning(
self, tmp_path, monkeypatch, capsys
):
"""Regression for #93032: pre-fix, the repair walks straight into the
doomed rename (provisioning + cutover) whenever the updater itself
maps the live venv; the park then fails with WinError 5 and the user
gets the misleading 'next update will retry' message forever."""
from hermes_cli import managed_uv
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel, scripts_python = self._checkout(tmp_path)
current = _runtime_info(scripts_python, (3, 50, 4))
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(sys, "executable", str(scripts_python))
with patch(
"hermes_cli.managed_uv._windows_runtime_holders",
return_value=(False, ""),
), \
patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation"
) as mock_install:
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "skipped"
assert "live venv" in result.detail
assert mock_install.call_count == 0, (
"a self-locked updater must not provision a candidate it can "
"never cut over"
)
assert sentinel.read_text(encoding="utf-8") == "live"
assert not (root / ".hermes-runtime").exists()
out = capsys.readouterr().out
assert "SQLite runtime repair deferred" in out
assert "will retry" not in out, (
"the structural self-lock must not promise that retrying helps"
)
assert "outside" in out, "the deferral must point at an escape hatch"
def test_non_self_locked_repair_proceeds(self, tmp_path, monkeypatch):
"""The guard must fail OPEN when the updater runs from outside the
venv — an always-firing deferral would recreate the never-converging
loop this fix removes (#86735 class)."""
from hermes_cli import managed_uv
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel, scripts_python = self._checkout(tmp_path)
current = _runtime_info(scripts_python, (3, 50, 4))
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(
sys, "executable", str(tmp_path / "outside" / "python.exe")
)
with patch(
"hermes_cli.managed_uv._windows_runtime_holders",
return_value=(False, ""),
), \
patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
return_value=None,
) as mock_install:
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "failed"
assert "provision" in result.detail
mock_install.assert_called_once()
assert sentinel.read_text(encoding="utf-8") == "live"
def test_self_lock_is_a_noop_off_windows(self, tmp_path, monkeypatch):
"""POSIX renames work while the updater maps the venv, so the guard
must stay Windows-only."""
from hermes_cli import managed_uv
root, live, sentinel, scripts_python = self._checkout(tmp_path)
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Linux")
monkeypatch.setattr(sys, "executable", str(scripts_python))
locked, detail = managed_uv._windows_runtime_self_lock(live)
assert (locked, detail) == (False, "")
def test_venv_launcher_ancestor_is_a_self_lock(self, tmp_path, monkeypatch):
r"""The venv\Scripts\hermes.exe shim stays mapped while it waits for
this child — an ancestor running from the venv blocks the rename too."""
from hermes_cli import managed_uv
root, live, sentinel, scripts_python = self._checkout(tmp_path)
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(
sys, "executable", str(tmp_path / "outside" / "python.exe")
)
class _FakeProc:
def __init__(self, pid, exe):
self.pid = pid
self._exe = exe
def exe(self):
return self._exe
fake_psutil = SimpleNamespace(
Process=lambda: SimpleNamespace(
parents=lambda: [_FakeProc(999, str(scripts_python))],
),
)
with patch.dict(sys.modules, {"psutil": fake_psutil}):
locked, detail = managed_uv._windows_runtime_self_lock(live)
assert locked
assert "999" in detail

View File

@@ -1,4 +1,9 @@
"""Native-Windows coverage for managed runtime cutover."""
"""Windows runtime cutover repoints ``pyvenv.cfg`` instead of renaming the live venv (#93032).
Windows refuses to rename a directory while any handle is open under it (a process cwd, an open
file, a sync client); the POSIX park-rename then fails with ``WinError 5``. Replacing the venv's
``pyvenv.cfg`` redirects every fresh process to the new generation with no directory rename.
"""
from __future__ import annotations
@@ -6,63 +11,65 @@ import subprocess
import venv
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
def _python(venv_dir: Path) -> Path:
return venv_dir / "Scripts" / "python.exe"
def _info(exe: Path, version: tuple[int, int, int], sqlite: str) -> SQLiteRuntimeInfo:
major, minor, patch = (int(x) for x in sqlite.split("."))
return SQLiteRuntimeInfo(
executable=exe, base_prefix=exe.parent, python_version=version,
sqlite_version=(major, minor, patch),
sqlite_version_string=sqlite, sqlite_source_id=sqlite)
@pytest.mark.windows_only
def test_runtime_config_cutover_does_not_rename_running_venv(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def test_runtime_config_cutover_repoints_a_running_venv(tmp_path: Path, monkeypatch) -> None:
from hermes_cli import managed_uv
live = tmp_path / "venv"
candidate = tmp_path / "candidate"
venv.EnvBuilder(with_pip=False).create(live)
venv.EnvBuilder(with_pip=False).create(candidate)
candidate_config = candidate / "pyvenv.cfg"
replacement = candidate_config.read_bytes() + b"runtime-cutover = candidate\n"
candidate_config.write_bytes(replacement)
info = SimpleNamespace(sqlite_version_string="3.53.1")
monkeypatch.setattr(
managed_uv,
"_smoke_candidate_venv",
lambda target: (True, "", info),
)
monkeypatch.setattr(managed_uv, "_smoke_candidate_venv", lambda target: (True, "", info))
same = (3, 11, 15)
current = _info(_python(live), same, "3.50.4")
fixed = _info(candidate / "python.exe", same, "3.53.1")
# A holder the updater cannot see or evict: a process whose cwd is inside the venv. (A process
# merely executing from the venv does not block the rename; proven live on windows-latest.)
# The child opens its cwd handle during its own startup, so wait for it to report ready.
process = subprocess.Popen(
[str(_python(live)), "-c", "import time; time.sleep(30)"],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
[str(_python(live)), "-c", "import time; print('ready', flush=True); time.sleep(30)"],
cwd=str(live / "Scripts"), stdout=subprocess.PIPE, text=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try:
ok, runtime_in_use, final_info, detail = (
managed_uv._cut_over_windows_runtime_config(candidate, live=live)
)
assert process.poll() is None
assert ok is True
assert runtime_in_use is True
assert final_info is info
assert detail == ""
assert (live / "pyvenv.cfg").read_bytes() == replacement
assert process.stdout.readline().strip() == "ready"
# The symptom: the directory rename the POSIX cutover relies on fails with WinError 5.
with pytest.raises(OSError):
managed_uv._rename_with_retry(live, live.with_name("venv.parked"))
assert live.is_dir()
ok, generation_in_use, final_info, detail = managed_uv._cut_over_windows_runtime_config(
candidate, live=live, current=current, candidate_info=fixed)
assert process.poll() is None
assert (ok, generation_in_use, final_info, detail) == (True, True, info, "")
assert (live / "pyvenv.cfg").read_bytes() == replacement
probe = subprocess.run(
[str(_python(live)), "-I", "-c", "print('repointed')"],
capture_output=True,
text=True,
check=False,
timeout=10,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
assert probe.returncode == 0
assert probe.stdout.strip() == "repointed"
capture_output=True, text=True, check=False, timeout=10,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
assert probe.returncode == 0 and probe.stdout.strip() == "repointed"
finally:
process.terminate()
try:
@@ -72,11 +79,7 @@ def test_runtime_config_cutover_does_not_rename_running_venv(
process.wait(timeout=5)
@pytest.mark.windows_only
def test_runtime_config_cutover_rolls_back_failed_live_smoke(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def test_runtime_config_cutover_rolls_back_failed_live_smoke(tmp_path: Path, monkeypatch) -> None:
from hermes_cli import managed_uv
live = tmp_path / "venv"
@@ -87,93 +90,39 @@ def test_runtime_config_cutover_rolls_back_failed_live_smoke(
(live / "pyvenv.cfg").write_bytes(original)
(candidate / "pyvenv.cfg").write_bytes(b"home = new-runtime\n")
monkeypatch.setattr(
managed_uv,
"_smoke_candidate_venv",
lambda target: (False, "core import smoke failed", None),
)
managed_uv, "_smoke_candidate_venv", lambda target: (False, "core import smoke failed", None))
same = (3, 11, 15)
ok, runtime_in_use, info, detail = managed_uv._cut_over_windows_runtime_config(
candidate, live=live
)
ok, generation_in_use, info, detail = managed_uv._cut_over_windows_runtime_config(
candidate, live=live,
current=_info(_python(live), same, "3.50.4"),
candidate_info=_info(candidate / "python.exe", same, "3.53.1"))
assert ok is False
assert runtime_in_use is False
assert info is None
assert (ok, generation_in_use, info) == (False, False, None)
assert detail == "post-cutover smoke failed: core import smoke failed"
assert (live / "pyvenv.cfg").read_bytes() == original
@pytest.mark.windows_only
def test_runtime_repair_uses_config_cutover_on_windows(tmp_path: Path) -> None:
from hermes_cli.managed_uv import repair_vulnerable_runtime
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
def test_runtime_config_cutover_refuses_a_minor_line_jump(tmp_path: Path, monkeypatch) -> None:
"""The live venv keeps its cp311 site-packages, so a 3.12 generation must never be pointed at it."""
from hermes_cli import managed_uv
root = tmp_path / "checkout"
root.mkdir()
(root / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
live = root / "venv"
_python(live).parent.mkdir(parents=True)
_python(live).write_bytes(b"live")
(live / "pyvenv.cfg").write_text("home = old-runtime\n", encoding="utf-8")
live = tmp_path / "venv"
candidate = tmp_path / "candidate"
live.mkdir()
candidate.mkdir()
original = b"home = old-runtime\n"
(live / "pyvenv.cfg").write_bytes(original)
(candidate / "pyvenv.cfg").write_bytes(b"home = new-runtime\n")
smoked = []
monkeypatch.setattr(managed_uv, "_smoke_candidate_venv", lambda target: smoked.append(target))
generation = root / ".hermes-runtime" / "python" / "generation-test"
candidate_python = generation / "python.exe"
candidate_python.parent.mkdir(parents=True)
candidate_python.write_bytes(b"candidate")
candidate = root / ".hermes-runtime" / "venv-candidate-test"
_python(candidate).parent.mkdir(parents=True)
_python(candidate).write_bytes(b"candidate")
(candidate / "pyvenv.cfg").write_text(
f"home = {generation}\n",
encoding="utf-8",
)
ok, generation_in_use, info, detail = managed_uv._cut_over_windows_runtime_config(
candidate, live=live,
current=_info(_python(live), (3, 11, 15), "3.50.4"),
candidate_info=_info(candidate / "python.exe", (3, 12, 4), "3.53.1"))
current = SQLiteRuntimeInfo(
executable=_python(live),
base_prefix=live,
python_version=(3, 11, 15),
sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4",
sqlite_source_id="old",
)
fixed = SQLiteRuntimeInfo(
executable=candidate_python,
base_prefix=generation,
python_version=(3, 11, 15),
sqlite_version=(3, 53, 1),
sqlite_version_string="3.53.1",
sqlite_source_id="new",
)
with (
patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
side_effect=[current, current],
),
patch(
"hermes_cli.managed_uv._windows_runtime_holders",
return_value=(False, ""),
),
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
return_value=(generation, candidate_python, fixed),
),
patch(
"hermes_cli.managed_uv._stage_candidate_venv",
return_value=candidate,
),
patch(
"hermes_cli.managed_uv._cut_over_windows_runtime_config",
return_value=(True, True, fixed, ""),
) as windows_cutover,
patch("hermes_cli.managed_uv._cut_over_candidate") as directory_cutover,
):
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "repaired"
assert result.sqlite_before == "3.50.4"
assert result.sqlite_after == "3.53.1"
windows_cutover.assert_called_once_with(candidate, live=live)
directory_cutover.assert_not_called()
assert not candidate.exists()
assert generation.exists()
assert (ok, generation_in_use, info) == (False, False, None)
assert "3.12" in detail and "3.11" in detail
assert (live / "pyvenv.cfg").read_bytes() == original
assert smoked == []