diff --git a/scripts/install.sh b/scripts/install.sh index 8e8b4d9a..53d8e081 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1330,7 +1330,12 @@ run_setup_wizard() { # The setup wizard reads from /dev/tty, so it works even when the # install script itself is piped (curl | bash). Only skip if no # terminal is available at all (e.g. Docker build, CI). - if ! [ -e /dev/tty ]; then + # + # Probe by actually opening /dev/tty: a bare existence test passes + # in Docker builds where the device node is in the mount namespace + # but opening fails with ENXIO, so the wizard would proceed and + # then crash on `< /dev/tty` below. + if ! (: /dev/null; then log_info "Setup wizard skipped (no terminal available). Run 'hermes setup' after install." return 0 fi diff --git a/tests/test_install_sh_setup_wizard_tty_probe.py b/tests/test_install_sh_setup_wizard_tty_probe.py new file mode 100644 index 00000000..dbe70538 --- /dev/null +++ b/tests/test_install_sh_setup_wizard_tty_probe.py @@ -0,0 +1,41 @@ +"""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/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 "(: