mirror of
https://github.com/usestrix/strix.git
synced 2026-10-05 02:41:38 +00:00
fix(cli): let --fail-on through when no terminal is attached
parse_arguments() rejected --fail-on without -n before main() could switch to headless, so a CI job running `strix -t x --fail-on high` without -n stopped at an argument error instead of scanning. The terminal check now lives in cli_args as terminal_attached(); the parser only enforces the headless-only flag when a terminal is attached and the TUI would open.
This commit is contained in:
parent
1ee5c2bcc6
commit
fc85e0061e
4 changed files with 61 additions and 19 deletions
|
|
@ -342,7 +342,9 @@ Strix Cloud:
|
|||
if args.update:
|
||||
sys.exit(0 if self_update() else 1)
|
||||
|
||||
if args.fail_on and not args.non_interactive:
|
||||
if args.fail_on and not args.non_interactive and terminal_attached():
|
||||
# Without a terminal main() switches to headless anyway, so the
|
||||
# flag is only out of place when the TUI would actually open.
|
||||
parser.error("--fail-on only applies to headless runs; add -n/--non-interactive.")
|
||||
|
||||
if args.instruction and args.instruction_file:
|
||||
|
|
@ -414,6 +416,13 @@ Strix Cloud:
|
|||
return args
|
||||
|
||||
|
||||
def terminal_attached() -> bool:
|
||||
"""Whether the TUI can take over the terminal: a tty on both ends, not dumb."""
|
||||
if os.environ.get("TERM", "").strip().lower() == "dumb":
|
||||
return False
|
||||
return all(hasattr(stream, "isatty") and stream.isatty() for stream in (sys.stdin, sys.stdout))
|
||||
|
||||
|
||||
class ResumeError(ValueError):
|
||||
"""A prior run cannot be resumed as recorded; the message names why."""
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import argparse
|
|||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -24,6 +23,7 @@ from strix.interface.cli_args import (
|
|||
load_resume_state,
|
||||
parse_arguments,
|
||||
resume_run_list_message,
|
||||
terminal_attached,
|
||||
)
|
||||
from strix.interface.environment import (
|
||||
check_docker_installed,
|
||||
|
|
@ -350,13 +350,6 @@ def _print_cli_error(message: str) -> None:
|
|||
HEADLESS_HINT = "If Strix runs without a terminal (CI, nohup, pipes), pass -n to run headless."
|
||||
|
||||
|
||||
def _terminal_attached() -> bool:
|
||||
"""Whether the TUI can take over the terminal: a tty on both ends, not dumb."""
|
||||
if os.environ.get("TERM", "").strip().lower() == "dumb":
|
||||
return False
|
||||
return all(hasattr(stream, "isatty") and stream.isatty() for stream in (sys.stdin, sys.stdout))
|
||||
|
||||
|
||||
def _fall_back_to_headless(args: argparse.Namespace) -> None:
|
||||
"""Run headless when there is no terminal for the TUI to attach to.
|
||||
|
||||
|
|
@ -366,7 +359,7 @@ def _fall_back_to_headless(args: argparse.Namespace) -> None:
|
|||
enter a target, so stop with the fix instead. A bare ``--resume`` is left
|
||||
to the picker, which already explains itself without a terminal.
|
||||
"""
|
||||
if args.non_interactive or args.resume_picker or _terminal_attached():
|
||||
if args.non_interactive or args.resume_picker or terminal_attached():
|
||||
return
|
||||
if args.needs_setup:
|
||||
report_error("no_terminal_for_setup")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pytest
|
|||
|
||||
|
||||
cli_main: Any = importlib.import_module("strix.interface.main")
|
||||
cli_args: Any = importlib.import_module("strix.interface.cli_args")
|
||||
|
||||
|
||||
def _stub_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
@ -104,9 +105,23 @@ def test_parse_fail_on_requires_non_interactive(
|
|||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
_stub_settings(monkeypatch)
|
||||
monkeypatch.setattr(cli_args, "terminal_attached", lambda: True)
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "-t", "https://test.com/", "--fail-on", "high"])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
cli_main.parse_arguments()
|
||||
|
||||
assert "--fail-on only applies to headless runs" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_parse_fail_on_without_a_terminal_waits_for_the_headless_fallback(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_stub_settings(monkeypatch)
|
||||
monkeypatch.setattr(cli_args, "terminal_attached", lambda: False)
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "-t", "https://test.com/", "--fail-on", "high"])
|
||||
|
||||
args = cli_main.parse_arguments()
|
||||
|
||||
assert args.fail_on == "high"
|
||||
assert args.non_interactive is False
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pytest
|
|||
|
||||
|
||||
cli_main: Any = importlib.import_module("strix.interface.main")
|
||||
cli_args: Any = importlib.import_module("strix.interface.cli_args")
|
||||
report_state_module: Any = importlib.import_module("strix.report.state")
|
||||
interactive: Any = importlib.import_module("strix.interface.interactive")
|
||||
tui_runtime: Any = importlib.import_module("strix.interface.tui.runtime")
|
||||
|
|
@ -42,7 +43,7 @@ def _launch(
|
|||
calls.append("cli")
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "--target", "https://example.com"])
|
||||
monkeypatch.setattr(cli_main, "_terminal_attached", lambda: terminal)
|
||||
monkeypatch.setattr(cli_main, "terminal_attached", lambda: terminal)
|
||||
monkeypatch.setattr("strix.interface.cli.run_cli", run_cli)
|
||||
monkeypatch.setattr(cli_main, "report_error", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(cli_main, "_print_error_panel", lambda title, _msg: calls.append(title))
|
||||
|
|
@ -121,7 +122,7 @@ def test_tui_startup_failure_marks_the_prepared_run_failed(monkeypatch: pytest.M
|
|||
raise cli_main.InteractiveSetupUnavailableError("no sidecar")
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "--target", "https://example.com"])
|
||||
monkeypatch.setattr(cli_main, "_terminal_attached", lambda: True)
|
||||
monkeypatch.setattr(cli_main, "terminal_attached", lambda: True)
|
||||
monkeypatch.setattr(cli_main, "setup_console_logging", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "start_import_warmup", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "parse_arguments", lambda: args)
|
||||
|
|
@ -160,6 +161,30 @@ def test_no_terminal_with_a_target_runs_headless_with_a_notice(
|
|||
assert "No terminal attached, running headless (same as -n)." in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_no_terminal_fallback_keeps_the_fail_on_gate(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls: list[str] = []
|
||||
report_state = SimpleNamespace(
|
||||
cleanup=lambda status: calls.append(f"cleanup:{status}"),
|
||||
vulnerability_reports=[{"id": "vuln-0001", "severity": "high"}],
|
||||
)
|
||||
args = argparse.Namespace(
|
||||
non_interactive=False,
|
||||
needs_setup=False,
|
||||
resume_picker=False,
|
||||
run_name="run",
|
||||
fail_on="high",
|
||||
)
|
||||
monkeypatch.setattr(cli_main.posthog, "end", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(cli_main.scarf, "end", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(report_state_module, "get_global_report_state", lambda: report_state)
|
||||
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
_launch(monkeypatch, needs_setup=False, terminal=False, args=args)
|
||||
|
||||
assert exit_info.value.code == 2
|
||||
assert args.non_interactive is True
|
||||
|
||||
|
||||
def test_no_terminal_without_a_target_stops_with_the_headless_hint(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -167,7 +192,7 @@ def test_no_terminal_without_a_target_stops_with_the_headless_hint(
|
|||
args = argparse.Namespace(
|
||||
non_interactive=False, needs_setup=True, resume_picker=False, run_name=None, fail_on=None
|
||||
)
|
||||
monkeypatch.setattr(cli_main, "_terminal_attached", lambda: False)
|
||||
monkeypatch.setattr(cli_main, "terminal_attached", lambda: False)
|
||||
monkeypatch.setattr(cli_main, "report_error", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
cli_main, "_print_error_panel", lambda title, msg: panels.append((title, msg))
|
||||
|
|
@ -191,7 +216,7 @@ def test_no_terminal_leaves_a_bare_resume_to_the_picker(monkeypatch: pytest.Monk
|
|||
args = argparse.Namespace(
|
||||
non_interactive=False, needs_setup=False, resume_picker=True, run_name=None, fail_on=None
|
||||
)
|
||||
monkeypatch.setattr(cli_main, "_terminal_attached", lambda: False)
|
||||
monkeypatch.setattr(cli_main, "terminal_attached", lambda: False)
|
||||
|
||||
cli_main._fall_back_to_headless(args)
|
||||
|
||||
|
|
@ -207,18 +232,18 @@ def test_terminal_attached_needs_a_tty_on_both_ends_and_a_real_term(
|
|||
monkeypatch.delenv("TERM", raising=False)
|
||||
monkeypatch.setattr(sys, "stdin", stream(tty=True))
|
||||
monkeypatch.setattr(sys, "stdout", stream(tty=True))
|
||||
assert cli_main._terminal_attached() is True
|
||||
assert cli_args.terminal_attached() is True
|
||||
|
||||
monkeypatch.setattr(sys, "stdout", stream(tty=False))
|
||||
assert cli_main._terminal_attached() is False
|
||||
assert cli_args.terminal_attached() is False
|
||||
|
||||
monkeypatch.setattr(sys, "stdout", stream(tty=True))
|
||||
monkeypatch.setattr(sys, "stdin", stream(tty=False))
|
||||
assert cli_main._terminal_attached() is False
|
||||
assert cli_args.terminal_attached() is False
|
||||
|
||||
monkeypatch.setattr(sys, "stdin", stream(tty=True))
|
||||
monkeypatch.setenv("TERM", "dumb")
|
||||
assert cli_main._terminal_attached() is False
|
||||
assert cli_args.terminal_attached() is False
|
||||
|
||||
|
||||
def test_tui_process_dying_after_startup_prints_a_panel_instead_of_a_traceback(
|
||||
|
|
@ -235,7 +260,7 @@ def test_tui_process_dying_after_startup_prints_a_panel_instead_of_a_traceback(
|
|||
raise cli_main.InteractiveInterfaceExitedError("Bubble Tea TUI exited with status 1")
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["strix", "--target", "https://example.com"])
|
||||
monkeypatch.setattr(cli_main, "_terminal_attached", lambda: True)
|
||||
monkeypatch.setattr(cli_main, "terminal_attached", lambda: True)
|
||||
monkeypatch.setattr(cli_main, "setup_console_logging", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "start_import_warmup", lambda: None)
|
||||
monkeypatch.setattr(cli_main, "parse_arguments", lambda: args)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue