test(cron): two invariants plus the Windows lane marker (#122183)
platforms("windows") instead of a bare skipif: list_os_marked_tests.py
gates the Windows lane on that marker, so the skipif version ran on no
CI lane at all -- the antipattern root AGENTS.md calls out.
Four tests collapse to the two invariants that actually matter: the
committed generation wins, and no-generation/unreadable-record both
fall through to the handed venv. The legacy non-managed test is gone --
it passes on main by design, and the fall-through test's positive
control now covers that path inside the managed arm.
(cherry picked from commit 30c649f16bce08eeaabd5a407ae0429cf422556a)
This commit is contained in:
124
tests/cron/test_cron_windows_venv_abi.py
Normal file
124
tests/cron/test_cron_windows_venv_abi.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Windows cron scripts resolve their dependency tree, and fall through when there is none.
|
||||
|
||||
``cron/scheduler_script.py::_windows_cron_python_invocation`` answers "which interpreter and
|
||||
which site-packages?" for a cron script child. On a PM-managed install it read the dependency
|
||||
tree through ``pm.environments.selected_venv``, which falls back to ``base_venv`` and answers
|
||||
the leftover pre-PM ``<root>/venv`` when no generation is recorded. That tree is built for
|
||||
whichever interpreter created it, so overlaying it on the managed store Python loads a cp311
|
||||
``pydantic_core`` on 3.14 and every script dies with
|
||||
``ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'`` — the cron sibling of
|
||||
the gateway crash in #122183/#123650, which no PR covered.
|
||||
|
||||
Two invariants:
|
||||
|
||||
1. With a generation committed, that generation is the tree the child gets — never the in-tree
|
||||
venv.
|
||||
2. With NO generation, or a record too corrupt to read, the managed branch falls through to the
|
||||
uv overlay: the handed venv keeps its own interpreter and its own site-packages. Handing back
|
||||
the bare store Python instead would be worse than main (``_require_own_dependencies`` refuses
|
||||
it with "hermes pm repair" and a cron script has no bootstrap to refuse cleanly), and it
|
||||
strips the overlay's site-packages entry, warning on every spawn.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.platforms("windows")
|
||||
|
||||
|
||||
def _write_venv(venv: Path, base: Path, *, version: str | None = None) -> Path:
|
||||
"""A fake Windows venv with the ``Scripts/python.exe`` shim a cron child is handed."""
|
||||
(venv / "Lib" / "site-packages").mkdir(parents=True, exist_ok=True)
|
||||
(venv / "Scripts").mkdir(parents=True, exist_ok=True)
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
(venv / "Scripts" / "python.exe").write_text("", encoding="utf-8")
|
||||
(base / "python.exe").write_text("", encoding="utf-8")
|
||||
lines = [f"home = {base}", "uv = 0.11.14"]
|
||||
if version:
|
||||
lines.append(f"version_info = {version}")
|
||||
(venv / "pyvenv.cfg").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return venv
|
||||
|
||||
|
||||
def _site_packages_in(env_overlay: dict) -> Path | None:
|
||||
for entry in env_overlay.get("PYTHONPATH", "").split(os.pathsep):
|
||||
if Path(entry).name == "site-packages":
|
||||
return Path(entry)
|
||||
return None
|
||||
|
||||
|
||||
def _raise_runtime_error(_root):
|
||||
raise RuntimeError("cannot read dependency environment: facts.json")
|
||||
|
||||
|
||||
def test_managed_install_uses_the_committed_generation_not_the_in_tree_venv(
|
||||
tmp_path, monkeypatch,
|
||||
):
|
||||
"""The committed generation wins; the leftover pre-PM venv never reaches the child."""
|
||||
from cron import scheduler_script as sched_script
|
||||
|
||||
stale = _write_venv(tmp_path / "repo" / "venv", tmp_path / "base")
|
||||
committed = _write_venv(
|
||||
tmp_path / "installs" / "gen" / "venv", tmp_path / "genbase",
|
||||
version=f"{sys.version_info[0]}.{sys.version_info[1]}.7",
|
||||
)
|
||||
store = tmp_path / "store" / "python.exe"
|
||||
store.parent.mkdir(parents=True)
|
||||
store.write_text("", encoding="utf-8")
|
||||
child = _write_venv(tmp_path / "child", tmp_path / "childbase")
|
||||
|
||||
monkeypatch.setattr("hermes_cli._launchers.resolve_store_python", lambda _root: store)
|
||||
monkeypatch.setattr("pm.environments.committed_venv", lambda _root: committed)
|
||||
monkeypatch.setattr("pm.environments.selected_venv", lambda _root: stale)
|
||||
|
||||
interpreter, env_overlay = sched_script._windows_cron_python_invocation(
|
||||
str(child / "Scripts" / "python.exe")
|
||||
)
|
||||
|
||||
assert interpreter == str(store)
|
||||
overlay = _site_packages_in(env_overlay)
|
||||
assert overlay is not None, "the committed generation must supply a site-packages entry"
|
||||
assert overlay == committed / "Lib" / "site-packages"
|
||||
assert not overlay.is_relative_to(stale)
|
||||
|
||||
|
||||
def test_managed_install_without_a_generation_falls_through_to_the_handed_venv(
|
||||
tmp_path, monkeypatch,
|
||||
):
|
||||
"""No generation, or an unreadable record: the handed venv keeps its own packages.
|
||||
|
||||
Both arms must land identically — the child gets its own base interpreter and its own
|
||||
site-packages, not the bare store Python, which cannot serve a script with nothing
|
||||
committed. Positive control for the fall-through: the handed venv here IS a uv venv
|
||||
carrying its own site-packages, exactly the case that worked on main.
|
||||
"""
|
||||
from cron import scheduler_script as sched_script
|
||||
|
||||
stale = _write_venv(tmp_path / "repo" / "venv", tmp_path / "base")
|
||||
store = tmp_path / "store" / "python.exe"
|
||||
store.parent.mkdir(parents=True)
|
||||
store.write_text("", encoding="utf-8")
|
||||
|
||||
for label, committed in (("none", lambda _root: None),
|
||||
("unreadable", _raise_runtime_error)):
|
||||
case = tmp_path / label
|
||||
child = _write_venv(case / "child", case / "childbase")
|
||||
monkeypatch.setattr("hermes_cli._launchers.resolve_store_python",
|
||||
lambda _root, s=store: s)
|
||||
monkeypatch.setattr("pm.environments.committed_venv", committed)
|
||||
monkeypatch.setattr("pm.environments.selected_venv", lambda _root, t=stale: t)
|
||||
|
||||
interpreter, env_overlay = sched_script._windows_cron_python_invocation(
|
||||
str(child / "Scripts" / "python.exe")
|
||||
)
|
||||
|
||||
assert interpreter == str(case / "childbase" / "python.exe"), label
|
||||
assert env_overlay["VIRTUAL_ENV"] == str(child), label
|
||||
overlay = _site_packages_in(env_overlay)
|
||||
assert overlay is not None, f"{label}: the overlay must carry site-packages"
|
||||
assert overlay == child / "Lib" / "site-packages", label
|
||||
assert not overlay.is_relative_to(stale), label
|
||||
assert interpreter != str(store), label
|
||||
Reference in New Issue
Block a user