From 67b7341046a81f62326333e861e3cc6b0af16340 Mon Sep 17 00:00:00 2001 From: mayankbohradev Date: Thu, 13 Aug 2026 20:33:24 +0530 Subject: [PATCH] fix: route Speak V2 websocket through custom transport_factory `_TARGET_MODULES` in transport.py lists the modules that `install_transport()` patches so a user-supplied `transport_factory` replaces the default `websockets` transport. The Speak V2 websocket client shipped in 7.7.0 and binds the same patched symbols, but its two modules were never added to the list. Effect: a caller passing `transport_factory` gets custom-transport routing for Listen v1/v2, Speak v1 and Agent v1, while Speak V2 opens a direct connection to api.deepgram.com. There is no error or warning, so traffic intended to flow through a proxied or custom-hosted transport silently bypasses it. The existing transport tests could not catch this. They iterate `_TARGET_MODULES` itself, so they verify the list is internally consistent but cannot detect a module missing from it -- the missing entry is never iterated. Left as-is, the next regen that adds a websocket client reintroduces the same silent bypass. Changes: - add deepgram.speak.v2.raw_client and deepgram.speak.v2.client to `_TARGET_MODULES` (and correct the now-stale count in the comment) - add a completeness guard that discovers websocket modules from the package source, independently of `_TARGET_MODULES`, and asserts every discovered module is registered Verified: the new guard fails on the unpatched tree naming both Speak V2 modules, and passes after the fix. 319 passed, 1 skipped across tests/custom and tests/utils; `mypy src/` and `mypy tests/typecheck` clean. WireMock-backed tests under tests/wire require Docker and were not run locally. --- src/deepgram/transport.py | 4 ++- tests/custom/test_transport.py | 51 +++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/deepgram/transport.py b/src/deepgram/transport.py index a7897725..b92e3dff 100644 --- a/src/deepgram/transport.py +++ b/src/deepgram/transport.py @@ -27,7 +27,7 @@ # --------------------------------------------------------------------------- # Module paths that contain the websocket references we need to patch. -# All 8 are auto-generated by Fern — we never modify their source. +# All 10 are auto-generated by Fern — we never modify their source. # --------------------------------------------------------------------------- _TARGET_MODULES = [ "deepgram.listen.v1.raw_client", @@ -36,6 +36,8 @@ "deepgram.listen.v2.client", "deepgram.speak.v1.raw_client", "deepgram.speak.v1.client", + "deepgram.speak.v2.raw_client", + "deepgram.speak.v2.client", "deepgram.agent.v1.raw_client", "deepgram.agent.v1.client", ] diff --git a/tests/custom/test_transport.py b/tests/custom/test_transport.py index 84a262f7..80ceff4b 100644 --- a/tests/custom/test_transport.py +++ b/tests/custom/test_transport.py @@ -2,11 +2,13 @@ import json import sys -from typing import Any, Dict, Iterator, List +from pathlib import Path +from typing import Any, Dict, Iterator, List, Set from unittest.mock import MagicMock import pytest +import deepgram from deepgram.transport import ( AsyncTransport, SyncTransport, @@ -530,3 +532,50 @@ def test_async_transport_factory_auto_disables_reconnect(self): from deepgram.client import AsyncDeepgramClient client = AsyncDeepgramClient(api_key="test-key", transport_factory=factory) assert client.reconnect is False + + +# --------------------------------------------------------------------------- +# _TARGET_MODULES completeness +# --------------------------------------------------------------------------- + +_PATCHED_SYMBOLS = ("websockets_sync_client", "websockets_client_connect") + + +def _discover_websocket_modules() -> Set[str]: + """Return every `deepgram` module that references a patchable websocket symbol. + + Derived from the package source, deliberately not from `_TARGET_MODULES`. A + check that iterates `_TARGET_MODULES` can only confirm the list is internally + consistent; it cannot detect a websocket client missing from the list, + because the missing entry is never iterated. + """ + package_root = Path(deepgram.__file__).parent + discovered: Set[str] = set() + + for path in sorted(package_root.rglob("*.py")): + # transport.py names both symbols as its patch targets, so including it + # here would make the scan match itself. + if path.name == "transport.py": + continue + + source = path.read_text(encoding="utf-8") + if any(symbol in source for symbol in _PATCHED_SYMBOLS): + relative = path.relative_to(package_root).with_suffix("") + discovered.add(".".join(("deepgram",) + relative.parts)) + + return discovered + + +class TestTargetModuleCompleteness: + def test_every_websocket_module_is_registered_for_patching(self): + missing = sorted(_discover_websocket_modules() - set(_TARGET_MODULES)) + + assert not missing, ( + "websocket client module(s) absent from _TARGET_MODULES, so a custom " + "transport_factory is silently not applied to them: " + ", ".join(missing) + ) + + def test_discovery_locates_a_known_websocket_module(self): + # Guards the guard: if the scan silently found nothing, the completeness + # check above would pass vacuously. + assert "deepgram.listen.v1.raw_client" in _discover_websocket_modules()