"""Regression for #16746: install.sh /dev/tty gates must actually open /dev/tty. In a Docker build, ``/dev/tty`` exists as a device node (so a bare ``-e`` existence test returns true) but opening it fails with ``ENXIO: No such device or address``. Under the old gates the script 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 every existence-based check that guards a subsequent ``< /dev/tty`` redirect with an open-based probe so the skip kicks in correctly. This module covers all three affected functions: ``run_setup_wizard()`` (the reproducer in #16746), ``install_system_packages()`` (the apt sudo prompt fallback), and ``maybe_start_gateway()`` (the gateway-install gate). """ from __future__ import annotations import re from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parent.parent INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" # Every function in scripts/install.sh that previously gated on a bare # ``[ -e /dev/tty ]`` check before redirecting stdin from ``/dev/tty``. GATED_FUNCTIONS = ("run_setup_wizard", "install_system_packages", "maybe_start_gateway") def _extract_function_body(name: str) -> str: """Return the body of ``()`` as a single string. Anchored to ``()`` and a top-of-line ``}`` so the helper keeps working if neighbouring functions are renamed. """ text = INSTALL_SH.read_text() match = re.search( rf"^{re.escape(name)}\(\)\s*\{{\s*\n(?P.*?)^\}}", text, re.MULTILINE | re.DOTALL, ) assert match is not None, f"{name}() not found in scripts/install.sh" return match["body"] @pytest.mark.parametrize("fn_name", GATED_FUNCTIONS) def test_tty_gate_does_not_use_existence_only_check(fn_name: str) -> None: """The bare ``-e`` test is the bug — no spelling of it should remain.""" body = _extract_function_body(fn_name) # Cover ``[ -e /dev/tty ]``, ``[ -e "/dev/tty" ]``, ``test -e /dev/tty`` # and friends, with arbitrary surrounding whitespace. pattern = re.compile( r"""( \[\s*-e\s+["']?/dev/tty["']?\s*\] | \btest\s+-e\s+["']?/dev/tty["']? )""", re.VERBOSE, ) match = pattern.search(body) assert match is None, ( f"{fn_name} contains an existence-only check on /dev/tty " f"({match.group(0)!r}). Bare `-e` tests pass in Docker builds " "where the device node is in the mount namespace but cannot be " "opened (ENXIO). Use an open-based probe (e.g. " "`(: /dev/null` or `exec 3 None: """The gate must actually attempt to open ``/dev/tty``. Any ``if``/``if !``/``elif`` whose condition opens ``/dev/tty`` for input counts: ``(: