From 6e5d02999ba43b046beb217e43815d47d7a7d8bf Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 24 Jul 2026 08:33:36 -0700 Subject: [PATCH] fix(repeater): report identity config errors without a stack trace A configured-identity collision (IdentityConfigurationError from the startup preflight) is an actionable config problem, not a crash, but the top-level fatal handler logged every exception with exc_info=True, burying the message under a full traceback. Catch IdentityConfigurationError in main() and log just the message before exiting 1; unexpected errors still get the traceback. --- repeater/main.py | 5 +++++ tests/test_main_py_coverage.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/repeater/main.py b/repeater/main.py index a2349de..aa3f2b0 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -1773,6 +1773,11 @@ def main(): asyncio.run(daemon.run()) except KeyboardInterrupt: logger.info("Repeater stopped") + except IdentityConfigurationError as e: + # A misconfigured local identity is an actionable config problem, not a + # crash: report just the message so the fix is obvious, no stack trace. + logger.error("Identity configuration error: %s", e) + sys.exit(1) except Exception as e: logger.error(f"Fatal error: {e}", exc_info=True) sys.exit(1) diff --git a/tests/test_main_py_coverage.py b/tests/test_main_py_coverage.py index 94c1774..85f28e5 100644 --- a/tests/test_main_py_coverage.py +++ b/tests/test_main_py_coverage.py @@ -1,9 +1,11 @@ +import logging from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from repeater.companion.constants import STATS_TYPE_CORE, STATS_TYPE_PACKETS, STATS_TYPE_RADIO +from repeater.identity_manager import IdentityConfigurationError from repeater.main import RepeaterDaemon from repeater.main import main as repeater_main from openhop_core.node.dispatcher import Dispatcher @@ -533,3 +535,35 @@ def test_main_entrypoint_success_and_fatal_paths(monkeypatch): repeater_main() exit_mock.assert_called_once_with(1) + + +def test_main_identity_config_error_exits_cleanly_without_traceback(caplog): + """A configured-identity collision exits 1 with a clean message and no + stack-trace dump (regression: the fatal handler used to log exc_info=True + for every exception).""" + + class _Args: + config = "/tmp/test.yaml" + log_level = None + + fake_daemon = SimpleNamespace(run=MagicMock(return_value=object())) + err = IdentityConfigurationError( + "Local identity 'companion:B' (hash=0x77) conflicts with 'companion:A'" + ) + + with ( + patch("argparse.ArgumentParser.parse_args", return_value=_Args()), + patch("repeater.main.load_config", return_value=_base_config()), + patch("repeater.main.RepeaterDaemon", return_value=fake_daemon), + patch("asyncio.run", side_effect=err), + patch("sys.exit", side_effect=SystemExit(1)) as exit_mock, + caplog.at_level(logging.ERROR), + ): + with pytest.raises(SystemExit): + repeater_main() + + exit_mock.assert_called_once_with(1) + config_errors = [r for r in caplog.records if "Identity configuration error" in r.getMessage()] + assert len(config_errors) == 1 + assert config_errors[0].exc_info is None # no traceback attached + assert not any("Fatal error" in r.getMessage() for r in caplog.records)