diff --git a/gateway/config.py b/gateway/config.py index a85221f5c2f8..ed59c43ff632 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -717,9 +717,35 @@ class StreamingConfig: def from_dict(cls, data: Dict[str, Any]) -> "StreamingConfig": if not isinstance(data, dict) or not data: return cls() + + # ``mode`` is an ergonomic alias for the transport that ALSO implies + # ``enabled``. A config like ``streaming: {mode: auto}`` reads as + # "turn streaming on, transport=auto" — matching the natural intent + # of someone enabling streaming without also spelling out + # ``enabled: true``. Without this, ``mode`` was silently ignored and + # streaming stayed disabled (``enabled`` defaults to False), which is + # a surprising footgun: the whole reply buffers and sends at once. + # ``mode: off`` disables streaming; an explicit ``enabled`` key always + # wins so callers can force either state. + raw_transport = data.get("transport") + raw_mode = data.get("mode") + transport = raw_transport if raw_transport is not None else raw_mode + if transport is None: + transport = "auto" + transport = str(transport).strip().lower() or "auto" + + if "enabled" in data: + enabled = _coerce_bool(data.get("enabled"), False) + elif raw_mode is not None or raw_transport is not None: + # A mode/transport was given without an explicit enabled flag: + # infer enabled from the transport ("off" = disabled, else on). + enabled = transport != "off" + else: + enabled = False + return cls( - enabled=_coerce_bool(data.get("enabled"), False), - transport=data.get("transport", "auto"), + enabled=enabled, + transport=transport, edit_interval=_coerce_float( data.get("edit_interval"), DEFAULT_STREAMING_EDIT_INTERVAL, ), diff --git a/tests/test_gateway_streaming_nested_config.py b/tests/test_gateway_streaming_nested_config.py index d69d6b3c6018..cead58fd4625 100644 --- a/tests/test_gateway_streaming_nested_config.py +++ b/tests/test_gateway_streaming_nested_config.py @@ -41,3 +41,71 @@ class TestStreamingConfigNested: }) assert cfg.streaming.enabled is True assert cfg.streaming.transport == "edit" + + +class TestStreamingModeAlias: + """``streaming: {mode: ...}`` is an alias that also implies ``enabled``. + + Regression for a live config footgun: ``streaming: {mode: auto}`` was + silently ignored (mode was never read), so streaming stayed disabled and + the whole reply buffered before the first Telegram send. + """ + + def test_mode_auto_enables_streaming(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"mode": "auto"}) + assert sc.enabled is True + assert sc.transport == "auto" + + def test_mode_edit_enables_streaming(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"mode": "edit"}) + assert sc.enabled is True + assert sc.transport == "edit" + + def test_mode_off_disables_streaming(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"mode": "off"}) + assert sc.enabled is False + assert sc.transport == "off" + + def test_mode_with_extra_keys_still_enables(self): + """Real-world block: mode plus unrelated preloader_frames.""" + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict( + {"mode": "auto", "preloader_frames": ["a", "b"]} + ) + assert sc.enabled is True + assert sc.transport == "auto" + + def test_explicit_enabled_overrides_mode(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"mode": "auto", "enabled": False}) + assert sc.enabled is False + # transport still resolves from mode + assert sc.transport == "auto" + + def test_transport_takes_precedence_over_mode(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"mode": "off", "transport": "draft"}) + assert sc.transport == "draft" + assert sc.enabled is True + + def test_empty_block_stays_disabled(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({}) + assert sc.enabled is False + + def test_transport_only_enables(self): + from gateway.config import StreamingConfig + + sc = StreamingConfig.from_dict({"transport": "edit"}) + assert sc.enabled is True + assert sc.transport == "edit"