fix(console): handle string SystemExit code in _capture_output

A dispatched console handler that calls sys.exit("message") or
raise SystemExit("message") sets exc.code to a string. int(exc.code or 0)
then raises ValueError, which is not a ConsoleCommandError, so it escapes
execute()'s handler and crashes the local REPL on an ordinary user mistake
(e.g. removing a credential that does not exist). Treat a string exit code
as a status-1 failure carrying that message.
This commit is contained in:
golldyck
2026-07-04 22:09:43 +03:00
committed by kshitij
parent aec331899e
commit 652ebc5899
2 changed files with 40 additions and 2 deletions

View File

@@ -57,16 +57,24 @@ def _capture_output(fn: Callable[[], object]) -> str:
stdout = io.StringIO()
stderr = io.StringIO()
code = 0
message = ""
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
try:
result = fn()
if isinstance(result, int) and result:
raise SystemExit(result)
except SystemExit as exc:
code = int(exc.code or 0)
# sys.exit("msg") / raise SystemExit("msg") is the standard non-zero-exit idiom:
# exc.code is the message string, not an int. int() would raise ValueError here,
# which escapes execute()'s ConsoleCommandError handler and crashes the REPL.
if isinstance(exc.code, str):
message = exc.code
code = 1
else:
code = int(exc.code or 0)
text = stdout.getvalue() + stderr.getvalue()
if code:
raise ConsoleCommandError(text.strip() or f"Command exited with status {code}")
raise ConsoleCommandError(message.strip() or text.strip() or f"Command exited with status {code}")
return text.rstrip()

View File

@@ -309,3 +309,33 @@ def test_repl_runs_non_interactive_lines_without_prompts(_isolate_hermes_home):
assert stderr.getvalue() == ""
def test_capture_output_surfaces_string_exit_code_as_command_error():
from hermes_cli.console_engine import ConsoleCommandError, _capture_output
def _boom():
sys.exit("No credential matching \"nope\".")
with pytest.raises(ConsoleCommandError) as exc_info:
_capture_output(_boom)
assert "No credential matching" in str(exc_info.value)
def test_capture_output_preserves_integer_exit_code_message():
from hermes_cli.console_engine import ConsoleCommandError, _capture_output
with pytest.raises(ConsoleCommandError) as exc_info:
_capture_output(lambda: sys.exit(3))
assert "status 3" in str(exc_info.value)
def test_execute_handler_string_exit_returns_error_not_crash(_isolate_hermes_home):
result = HermesConsoleEngine().execute(
"auth remove openrouter __no_such_credential__", confirmed=True
)
assert result.status == "error"
assert result.output