Files
hermes-agent/tests/pm/test_node_libatomic_install_boundary.py
kshitijk4poor 47e6c2ebb5 fix(update): keep the completion parent stdlib-only around the libatomic pre-install
run_completion runs in the OLD process after the checkout swap, and its
contract is never to import application helpers from the replacement tree.
Importing pm.libatomic there crashed the update when the new tree lacked
the module (downgrade or older pin) and on architectures where
current_target() raises. Run the pre-install as its own process from the
new tree, ignore its outcome, and let the in-lock repair report.

Also: gate on an openable /dev/tty first (cheapest check, and the detached
child exits immediately), treat an unsupported architecture as "skip",
bound the sudo -n probe, and drop the timeout on the interactive install
so a slow prompt or mirror is never killed mid-dpkg (it runs outside PM's
lock). Found in the final-head review of #132664.
2026-10-04 19:19:41 +05:30

119 lines
4.3 KiB
Python

"""Regression tests for #124926: libatomic repair belongs to install, not verify."""
from pathlib import Path
import pytest
from pm.packages import Nodejs
_LOADER = (
"node: error while loading shared libraries: libatomic.so.1: "
"cannot open shared object file: No such file or directory"
)
def _node_script(path: Path, body: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
path.chmod(0o755)
@pytest.mark.platforms("posix")
def test_staged_repair_installs_libatomic_then_reprobes(tmp_path, monkeypatch):
node = tmp_path / "bin" / "node"
_node_script(node, f'#!/bin/sh\necho "{_LOADER}" >&2\nexit 127\n')
monkeypatch.setattr("pm.packages.current_target", lambda: "linux-x64")
calls = {"n": 0}
def install():
calls["n"] += 1
_node_script(node, "#!/bin/sh\necho v26.7.0\n")
return True, "install with sudo dnf install -y libatomic"
monkeypatch.setattr("pm.libatomic.try_install_libatomic", install)
package = Nodejs()
first = package.verify(tmp_path, "linux-x64")
repaired = package.repair_staged_verification(tmp_path, "linux-x64", first)
assert calls["n"] == 1
assert repaired == ("", "")
def test_auto_repair_never_reads_a_tty(monkeypatch):
from pm import libatomic
monkeypatch.setattr(libatomic, "_ATTEMPT", None)
monkeypatch.setattr(libatomic, "_is_root", lambda: False)
monkeypatch.setattr(libatomic, "_host_install_command", lambda: ("dnf", "install", "-y", "libatomic"))
monkeypatch.setattr(libatomic.shutil, "which", lambda name: "/usr/bin/sudo" if name == "sudo" else None)
captured = {}
def run(argv, **kwargs):
captured["argv"] = argv
captured.update(kwargs)
return type("Result", (), {"returncode": 1})()
monkeypatch.setattr(libatomic.subprocess, "run", run)
attempted, _remedy = libatomic.try_install_libatomic()
assert attempted is False
assert captured["argv"][:2] == ["/usr/bin/sudo", "-n"]
assert captured["stdin"] is libatomic.subprocess.DEVNULL
@pytest.mark.platforms("linux")
@pytest.mark.parametrize("flags", ["", "NON_INTERACTIVE=true"])
def test_installer_sudo_warmup_honours_non_interactive(tmp_path, flags):
"""The prerequisites warm-up may prompt for sudo, but never under --non-interactive."""
import os
import pty
script = Path(__file__).resolve().parents[2] / "scripts" / "install.sh"
bin_dir = tmp_path / "bin"
log = tmp_path / "sudo.log"
_node_script(bin_dir / "ldconfig", "#!/bin/sh\nexit 0\n")
_node_script(bin_dir / "id", "#!/bin/sh\necho 1000\n")
_node_script(bin_dir / "sudo", f'#!/bin/sh\necho "$*" >> "{log}"\nexit 0\n')
probe = f'source "$1" --manifest\n{flags}\nuv_bootstrap_target() {{ echo linux-x64; }}\nstage_prerequisites\n'
env = dict(os.environ, PATH=f"{bin_dir}{os.pathsep}{os.environ['PATH']}", HOME=str(tmp_path))
# A real controlling terminal: has_terminal() and `sudo -v </dev/tty` open it.
pid, fd = pty.fork()
if pid == 0:
os.execvpe("bash", ["bash", "-c", probe, "probe", str(script)], env)
while True:
try:
if not os.read(fd, 4096):
break
except OSError:
break
os.waitpid(pid, 0)
prompted = log.is_file() and "-v" in log.read_text().split()
assert prompted is (flags == "")
def test_update_completion_installs_before_detaching(monkeypatch):
"""run_completion's child has no controlling terminal; the pre-install runs first, in-session."""
from hermes_cli import update_completion
calls = []
monkeypatch.setattr(update_completion.subprocess, "run",
lambda argv, **kw: calls.append(("run", argv, kw)))
def popen(argv, **kw):
calls.append(("popen", argv, kw))
raise RuntimeError("stop after spawn")
monkeypatch.setattr(update_completion.subprocess, "Popen", popen)
with pytest.raises(RuntimeError, match="stop after spawn"):
update_completion.run_completion({"source": "/nonexistent", "home": "/nonexistent",
"receipt": {"update_id": "u"}})
assert [kind for kind, _argv, _kw in calls] == ["run", "popen"]
_kind, argv, kw = calls[0]
assert "install_before_lock" in " ".join(argv)
assert "start_new_session" not in kw and kw.get("check") is False