Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions gateway/platforms/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,23 @@ async def send_document(
file_name or os.path.basename(file_path),
)

async def send_voice(
self,
chat_id: str,
audio_path: str,
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> SendResult:
"""Send an audio file natively via the WhatsApp bridge.

OGG/Opus files are treated by the Node bridge as PTT voice notes
(ptt=true); MP3/WAV/M4A are sent as normal audio attachments.
"""
del reply_to, metadata, kwargs
return await self._send_media_to_bridge(chat_id, audio_path, "audio", caption)

async def send_typing(self, chat_id: str, metadata=None) -> None:
"""Send typing indicator via bridge."""
if not self._running or not self._http_session:
Expand Down
18 changes: 18 additions & 0 deletions tests/gateway/test_whatsapp_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,24 @@ async def test_closed_when_bridge_dies_phase1(self):
assert adapter._bridge_log_fh is None


class TestSendVoice:
"""Regression tests for native WhatsApp audio sending."""

@pytest.mark.asyncio
async def test_send_voice_routes_to_bridge_audio_media(self):
adapter = _make_adapter()
adapter._send_media_to_bridge = AsyncMock(
return_value=MagicMock(success=True, message_id="wa-audio-1")
)

result = await adapter.send_voice("chat-123", "/tmp/voice.ogg", caption="hello")

assert result.success is True
adapter._send_media_to_bridge.assert_awaited_once_with(
"chat-123", "/tmp/voice.ogg", "audio", "hello"
)


class TestBridgeRuntimeFailure:
"""Verify runtime bridge death is surfaced as a fatal adapter error."""

Expand Down