From a0972b9748585944c30d87dcee9c571b75a77389 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Thu, 9 Jul 2026 20:43:55 -0700 Subject: [PATCH] fix: widen None-deref guards to config-derived sibling sites + tests Sibling sites of the salvaged #55997 fix, all reading user-editable config values through .get(key, '').method(): MoA slot provider/model labels, gateway quick-command alias targets (2 sites), gateway.proxy_url, and gateway.relay_url. Regression tests for the contributor's two sites plus the MoA labels. --- agent/moa_loop.py | 2 +- gateway/relay/__init__.py | 3 ++- gateway/run.py | 7 ++--- tests/agent/test_none_deref_guards.py | 38 +++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/agent/test_none_deref_guards.py diff --git a/agent/moa_loop.py b/agent/moa_loop.py index 9700f4abe85..cd7fa3ea4ea 100644 --- a/agent/moa_loop.py +++ b/agent/moa_loop.py @@ -120,7 +120,7 @@ _REFERENCE_SYSTEM_PROMPT = ( def _slot_label(slot: dict[str, str]) -> str: - return f"{slot.get('provider', '').strip()}:{slot.get('model', '').strip()}" + return f"{(slot.get('provider') or '').strip()}:{(slot.get('model') or '').strip()}" def _slot_runtime(slot: dict[str, str]) -> dict[str, Any]: diff --git a/gateway/relay/__init__.py b/gateway/relay/__init__.py index 0c64aaedeb5..87326c4e4a0 100644 --- a/gateway/relay/__init__.py +++ b/gateway/relay/__init__.py @@ -36,7 +36,8 @@ def relay_url() -> Optional[str]: from gateway.run import _load_gateway_config # late import to avoid cycle cfg = _load_gateway_config() - url = (cfg.get("gateway") or {}).get("relay_url", "").strip() + url = (cfg.get("gateway") or {}).get("relay_url") + url = (url or "").strip() if url: return url.rstrip("/") except Exception: # noqa: BLE001 - config absence/parse must never crash registration diff --git a/gateway/run.py b/gateway/run.py index 313d9a14825..59f3c8a44ca 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -9609,7 +9609,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if isinstance(quick_commands, dict) and command in quick_commands: qcmd = quick_commands[command] if qcmd.get("type") == "alias": - target = qcmd.get("target", "").strip() + target = (qcmd.get("target") or "").strip() if target: target = target if target.startswith("/") else f"/{target}" target_command = target.lstrip("/") @@ -10021,7 +10021,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew else: return f"Quick command '/{command}' has no command defined." elif qcmd.get("type") == "alias": - target = qcmd.get("target", "").strip() + target = (qcmd.get("target") or "").strip() if target: target = target if target.startswith("/") else f"/{target}" target_command = target.lstrip("/") @@ -16523,7 +16523,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if url: return url.rstrip("/") cfg = _load_gateway_config() - url = (cfg.get("gateway") or {}).get("proxy_url", "").strip() + url = (cfg.get("gateway") or {}).get("proxy_url") + url = (url or "").strip() if url: return url.rstrip("/") return None diff --git a/tests/agent/test_none_deref_guards.py b/tests/agent/test_none_deref_guards.py new file mode 100644 index 00000000000..58ff72c4566 --- /dev/null +++ b/tests/agent/test_none_deref_guards.py @@ -0,0 +1,38 @@ +"""Regression tests for None-dereference guards on ``.get(key, "").method()`` +patterns (#55997 salvage + config-derived sibling sites). + +``dict.get(key, default)`` only returns the default when the key is ABSENT; +a present-but-null value sails through as None and crashes any chained +method call. +""" + +from agent.anthropic_adapter import _convert_user_message +from agent.moa_loop import _slot_label + + +class TestAnthropicNullTextBlock: + def test_null_text_block_treated_as_empty(self): + """A text block with ``text: null`` must not crash the empty-message + check in _convert_user_message (#55997).""" + result = _convert_user_message([{"type": "text", "text": None}]) + # Must not raise; result is a valid user message dict + assert result["role"] == "user" + + def test_mixed_null_and_real_text_blocks(self): + result = _convert_user_message( + [ + {"type": "text", "text": None}, + {"type": "text", "text": "hello"}, + ] + ) + assert result["role"] == "user" + + +class TestMoaSlotLabelNullFields: + def test_null_provider_and_model(self): + """MoA slot with ``provider: null`` in config.yaml must not crash + label construction.""" + assert _slot_label({"provider": None, "model": None}) == ":" # type: ignore[arg-type] + + def test_normal_slot(self): + assert _slot_label({"provider": "openrouter", "model": "m"}) == "openrouter:m"