From fbfe89871bb2154547ce6fc01c097b7a4e136962 Mon Sep 17 00:00:00 2001 From: harjoth Date: Fri, 10 Jul 2026 14:16:05 -0700 Subject: [PATCH] fix(cli): guarantee hard exit after cleanup interruption --- hermes_cli/main.py | 9 ++++-- tests/hermes_cli/test_tui_resume_flow.py | 41 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index e2c2808b7b91..b7c7c1291231 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -165,8 +165,13 @@ def _run_and_exit_oneshot( except Exception: pass rc = 1 - _cleanup_oneshot_runtime() - _exit_after_oneshot(rc) + try: + _cleanup_oneshot_runtime() + finally: + # The hard exit is the safety boundary for #43055. Even an interrupt + # during best-effort cleanup must not fall back into interpreter + # finalization, where the reported native SIGABRT occurs. + _exit_after_oneshot(rc) def _set_process_title() -> None: diff --git a/tests/hermes_cli/test_tui_resume_flow.py b/tests/hermes_cli/test_tui_resume_flow.py index dae98be57fc2..3d336d8d66f4 100644 --- a/tests/hermes_cli/test_tui_resume_flow.py +++ b/tests/hermes_cli/test_tui_resume_flow.py @@ -800,6 +800,25 @@ def test_run_and_exit_oneshot_routes_system_exit_to_hard_exit(monkeypatch, main_ assert exits == [2] +def test_run_and_exit_oneshot_routes_bare_system_exit_to_zero(monkeypatch, main_mod): + exits = [] + + def fake_run_oneshot(*_args, **_kwargs): + raise SystemExit + + monkeypatch.setitem( + sys.modules, + "hermes_cli.oneshot", + types.SimpleNamespace(run_oneshot=fake_run_oneshot), + ) + monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None) + monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc)) + + main_mod._run_and_exit_oneshot("hello") + + assert exits == [None] + + def test_run_and_exit_oneshot_prints_system_exit_message( monkeypatch, capsys, main_mod ): @@ -884,6 +903,28 @@ def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises( assert events == ["aux", "exit:0"] +def test_run_and_exit_oneshot_hard_exits_when_cleanup_is_interrupted( + monkeypatch, main_mod +): + def _raise_keyboard_interrupt(): + raise KeyboardInterrupt + + monkeypatch.setitem( + sys.modules, + "hermes_cli.oneshot", + types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0), + ) + monkeypatch.setattr( + main_mod, "_cleanup_oneshot_runtime", _raise_keyboard_interrupt + ) + monkeypatch.setattr(main_mod, "_exit_after_oneshot", _raise_exit) + + with pytest.raises(SystemExit) as exc: + main_mod._run_and_exit_oneshot("hello") + + assert exc.value.code == 0 + + def test_run_and_exit_oneshot_routes_keyboard_interrupt_to_130( monkeypatch, main_mod ):