hermes-agent/tests/test_install_sh_setup_wizard_tty_probe.py
briandevans 20c9340c34 fix(install): probe /dev/tty by opening it, not bare existence (#16746)
In Docker builds the `/dev/tty` device node is present in the mount
namespace, so `[ -e /dev/tty ]` returns true — but opening it fails
with `ENXIO: No such device or address`. Under the old gate the
"no terminal available" skip never triggered, the setup wizard ran,
and the build aborted a few lines later when bash tried `< /dev/tty`:

    /tmp/install.sh: line 1347: /dev/tty: No such device or address

Replace the existence check with `(: </dev/tty) 2>/dev/null`, which
actually attempts to open /dev/tty in a subshell. The probe succeeds
when piped from `curl | bash` on a real terminal (the wizard's intended
use case) and fails cleanly in Docker build / CI contexts so the skip
kicks in before the redirect can crash.

Add a regression test that statically asserts run_setup_wizard does not
gate on the bare existence check and that the open-based probe is in
place.

Fixes #16746.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 06:45:55 -07:00

41 lines
1.7 KiB
Python

"""Regression for #16746: setup-wizard tty gate must actually open /dev/tty.
In a Docker build, ``/dev/tty`` exists as a device node (so ``[ -e /dev/tty ]``
returns true) but opening it fails with ``ENXIO: No such device or address``.
Under the old gate the wizard proceeded past the "no terminal available" skip
and then crashed on the ``< /dev/tty`` redirect a few lines later, aborting
the entire image build. The fix replaces the bare existence check with an
open-based probe so the skip kicks in correctly.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
def _extract_run_setup_wizard() -> str:
"""Return the body of run_setup_wizard() as a single string."""
text = INSTALL_SH.read_text()
start = text.index("run_setup_wizard()")
# The next top-level function follows immediately; use it as the end marker.
end = text.index("\nmaybe_start_gateway()", start)
return text[start:end]
def test_run_setup_wizard_does_not_use_bare_existence_check() -> None:
body = _extract_run_setup_wizard()
assert "[ -e /dev/tty ]" not in body, (
"run_setup_wizard guards on `[ -e /dev/tty ]`, which is true in Docker "
"builds where the device node exists but cannot be opened (ENXIO). "
"Use an open-based probe such as `(: </dev/tty) 2>/dev/null` so the "
"skip kicks in before the wizard tries to read from /dev/tty. See #16746."
)
def test_run_setup_wizard_uses_open_based_tty_probe() -> None:
body = _extract_run_setup_wizard()
assert "(: </dev/tty)" in body, (
"run_setup_wizard must probe /dev/tty by actually opening it before "
"running the wizard. See #16746."
)