diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 7f81072515f7..efd16c3e1965 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -4998,7 +4998,8 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False, fo traceback=_traceback.format_exc(), ) print("\nGateway stopped.") - return + _hard_exit_after_gateway_teardown(0) + return # unreachable in production (os._exit); guard for test stubs except SystemExit as e: _exit_diag( "asyncio.run.SystemExit", diff --git a/tests/hermes_cli/test_gateway.py b/tests/hermes_cli/test_gateway.py index b858f64233d4..459fc4c0975f 100644 --- a/tests/hermes_cli/test_gateway.py +++ b/tests/hermes_cli/test_gateway.py @@ -63,6 +63,10 @@ def test_run_gateway_exits_cleanly_on_keyboard_interrupt(monkeypatch, capsys): _install_fake_gateway_run(monkeypatch, fake_start_gateway) monkeypatch.setattr(gateway.asyncio, "run", fake_asyncio_run) + # KeyboardInterrupt now uses the same hard-exit backstop as all other + # exit paths (instead of a bare ``return``). The test stub's + # _exit_after_graceful_shutdown is a no-op for code 0, so run_gateway() + # returns normally — but the real implementation would call os._exit(0). gateway.run_gateway() out = capsys.readouterr().out diff --git a/tests/hermes_cli/test_gateway_run_hard_exit.py b/tests/hermes_cli/test_gateway_run_hard_exit.py index be69b0915e77..3edf8cfdb1d0 100644 --- a/tests/hermes_cli/test_gateway_run_hard_exit.py +++ b/tests/hermes_cli/test_gateway_run_hard_exit.py @@ -85,3 +85,23 @@ def test_run_gateway_hard_exits_after_failed_return(monkeypatch): gateway_cli.run_gateway() assert excinfo.value.code == 1 + + +def test_run_gateway_hard_exits_after_keyboard_interrupt(monkeypatch): + """KeyboardInterrupt (console Ctrl+C) must also hard-exit, not return. + + A bare ``return`` would let Python finalization join non-daemon worker + threads, the same wedge this backstop prevents on the other exit paths. + """ + gateway_cli = _prepare(monkeypatch) + + def _fake_run(coro): + coro.close() + raise KeyboardInterrupt() + + monkeypatch.setattr(gateway_cli.asyncio, "run", _fake_run) + + with pytest.raises(_HardExitObserved) as excinfo: + gateway_cli.run_gateway() + + assert excinfo.value.code == 0