From 875ada68116aa2fb08c0afd6baa136cfd030835b Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Tue, 8 Sep 2026 16:34:00 +0800 Subject: [PATCH] fix(gateway): guard display config reads against present-but-null values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A profile config with a bare `display:` key (present-but-null) made `user_config.get("display", {})` return None — the {} default only applies when the key is missing — so the chained `.get("memory_notifications")` in _wire_turn_agent_callbacks raised AttributeError on every real gateway turn (Discord / cron). Oneshot turns bypass this wiring, which masked the crash during smoke tests. Use the same `or {}` guard the other gateway display readers (display_config.py, runtime_footer.py) already apply, and fall back to the documented default "on". Fixes #105674 --- gateway/run_turn_runner.py | 4 +- .../gateway/test_display_null_turn_wiring.py | 68 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/gateway/test_display_null_turn_wiring.py diff --git a/gateway/run_turn_runner.py b/gateway/run_turn_runner.py index b9ba066790..0f80058905 100644 --- a/gateway/run_turn_runner.py +++ b/gateway/run_turn_runner.py @@ -1176,7 +1176,9 @@ class TurnRunner: if pdc is not None: pdc[ctx.session_key] = bg_release # display.memory_notifications: off | on (generic "💾 Memory updated", default) | verbose. - mem_notif = ctx.user_config.get("display", {}).get("memory_notifications") + # `display:` present-but-null yields None, not the {} default (same `or {}` guard as + # display_config.py / runtime_footer.py). + mem_notif = (ctx.user_config.get("display") or {}).get("memory_notifications") if isinstance(mem_notif, bool): mem_notif = "on" if mem_notif else "off" agent.memory_notifications = str(mem_notif).lower() if mem_notif else "on" diff --git a/tests/gateway/test_display_null_turn_wiring.py b/tests/gateway/test_display_null_turn_wiring.py new file mode 100644 index 0000000000..8279a72722 --- /dev/null +++ b/tests/gateway/test_display_null_turn_wiring.py @@ -0,0 +1,68 @@ +"""A profile config with `display: null` must not crash turn wiring. + +`user_config.get("display", {})` returns None when the key is present but null +(bare `display:` in YAML), so the chained `.get("memory_notifications")` raised +AttributeError on every real gateway turn (#105674). Oneshot turns bypass +`_wire_turn_agent_callbacks`, which masked the regression during smoke tests. +""" + +from __future__ import annotations + +import types + +from gateway.run_turn_runner import TurnRunner + + +def _wire(user_config): + """Run `_wire_turn_agent_callbacks` over minimal fakes; return the agent.""" + agent = types.SimpleNamespace() + ctx = types.SimpleNamespace( + progress_callback=None, + native_tool_start_callback=None, + voice_ack_callback=None, + _voice_ack_guild=[None], + _native_slack_task_cards=False, + native_tool_complete_callback=None, + _step_callback_sync=None, + _hooks_ref=types.SimpleNamespace(loaded_hooks=[]), + _status_callback_sync=None, + _event_callback_sync=None, + _status_adapter=None, + session_key="", + user_config=user_config, + _thinking_enabled=False, + agent_holder=[None], + tools_holder=[None], + process_task_id=None, + process_baseline=None, + run_generation=0, + ) + holder = types.SimpleNamespace( + _ctx=ctx, + _runner=types.SimpleNamespace( + _service_tier=None, + _consume_pending_turn_sidecar_notes=lambda key: [], + ), + _make_bg_review_callbacks=lambda: (lambda message: None, lambda: None), + _merge_turn_request_overrides=TurnRunner._merge_turn_request_overrides, + _clarify_callback_sync=lambda *a, **k: None, + _notice_callback_sync=lambda *a, **k: None, + _attach_session_title_callback=lambda agent, ctx: None, + ) + TurnRunner._wire_turn_agent_callbacks(holder, agent, {}, None, None, None, False) + return agent + + +def test_present_but_null_display_falls_back_to_on(): + agent = _wire({"display": None}) + assert agent.memory_notifications == "on" + + +def test_missing_display_falls_back_to_on(): + agent = _wire({}) + assert agent.memory_notifications == "on" + + +def test_memory_notifications_setting_still_applies(): + agent = _wire({"display": {"memory_notifications": "verbose"}}) + assert agent.memory_notifications == "verbose"