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.
This commit is contained in:
agessaman
2026-07-24 08:33:36 -07:00
parent 95555e0c12
commit 6e5d02999b
2 changed files with 39 additions and 0 deletions
+5
View File
@@ -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)
+34
View File
@@ -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)