From 96f64d00bc53aef5dc70299b5e50d655304ae04a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:45:34 -0400 Subject: [PATCH] tui3: guard against non-interactive stdin with TTY check Add _stdin_is_tty() helper and check at start of _launch_tui3_or_exit. When stdin is not a TTY, emit a clear error message and exit 1 instead of propagating an EOF exception from seed setup prompts. Add test for non-interactive stdin error path; patch _stdin_is_tty in existing tests that expect tui3 launch to proceed. Fixes SeedPass-6d26 --- src/seedpass/cli/__init__.py | 10 ++++++++++ src/tests/test_typer_cli.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/seedpass/cli/__init__.py b/src/seedpass/cli/__init__.py index efbd3003..4a00a2ae 100644 --- a/src/seedpass/cli/__init__.py +++ b/src/seedpass/cli/__init__.py @@ -154,11 +154,21 @@ def _launch_legacy_tui(*, fingerprint: Optional[str]) -> typer.Exit: return typer.Exit(legacy_main(fingerprint=fingerprint)) +def _stdin_is_tty() -> bool: + return sys.stdin.isatty() + + def _launch_tui3_or_exit( *, fingerprint: Optional[str], services: Optional[dict[str, object]] = None, ) -> None: + if not _stdin_is_tty(): + typer.echo( + "seedpass tui3: requires an interactive terminal (stdin is not a TTY).", + err=True, + ) + raise typer.Exit(1) kwargs: dict[str, object] = {"fingerprint": fingerprint} if services is not None: kwargs.update( diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index 2f082613..535d9125 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -1021,6 +1021,7 @@ def test_root_tui3_forward_fingerprint(monkeypatch): called["factory"] = kwargs.get("entry_service_factory") return True + monkeypatch.setattr(cli, "_stdin_is_tty", lambda: True) monkeypatch.setattr(cli, "launch_tui3", fake_launch) result = runner.invoke(app, ["--fingerprint", "abc"]) assert result.exit_code == 0 @@ -1170,6 +1171,7 @@ def test_tui2_fallback_legacy_strips_subcommand_argv(monkeypatch): def test_root_tui3_unavailable_returns_error(monkeypatch): + monkeypatch.setattr(cli, "_stdin_is_tty", lambda: True) monkeypatch.setattr(cli, "launch_tui3", lambda **_: False) result = runner.invoke(app, ["--fingerprint", "abc"]) assert result.exit_code == 1 @@ -1179,6 +1181,7 @@ def test_root_tui3_unavailable_returns_error(monkeypatch): def test_root_tui3_does_not_preflight_entry_service(monkeypatch): + monkeypatch.setattr(cli, "_stdin_is_tty", lambda: True) monkeypatch.setattr( cli, "_get_entry_service", @@ -1189,6 +1192,14 @@ def test_root_tui3_does_not_preflight_entry_service(monkeypatch): assert result.exit_code == 0 +def test_tui3_non_interactive_stdin_exits_with_error(monkeypatch): + """tui3 must exit cleanly with an error message when stdin is not a TTY.""" + monkeypatch.setattr(cli, "_stdin_is_tty", lambda: False) + result = runner.invoke(app, ["tui3"]) + assert result.exit_code == 1 + assert "not a TTY" in result.stderr + + def test_tui2_preflight_failure_without_fallback(monkeypatch): monkeypatch.setattr( cli,