diff --git a/gateway/platforms/signal.py b/gateway/platforms/signal.py index c72a0844d2eb..843f808c64fe 100644 --- a/gateway/platforms/signal.py +++ b/gateway/platforms/signal.py @@ -97,6 +97,12 @@ def _guess_extension(data: bytes) -> str: return ".gif" if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP": return ".webp" + # RIFF/WAVE shares the ``RIFF`` chunk header with WebP; the form-type at + # bytes 8-11 disambiguates. Without this, an inbound WAV voice note fell + # through to ``.bin`` (octet-stream) and was cached as a document, so STT + # never saw it — even though ``.wav`` is in the audio allowlist + MIME map. + if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WAVE": + return ".wav" if data[:4] == b"%PDF": return ".pdf" if len(data) >= 8 and data[4:8] == b"ftyp": diff --git a/tests/gateway/test_signal.py b/tests/gateway/test_signal.py index 6677cd4b4581..45259d01a12e 100644 --- a/tests/gateway/test_signal.py +++ b/tests/gateway/test_signal.py @@ -164,6 +164,43 @@ class TestSignalHelpers: from gateway.platforms.signal import _guess_extension assert _guess_extension(b"\x00\x00\x00\x18ftypisom" + b"\x00" * 100) == ".mp4" + def test_guess_extension_wav(self): + """RIFF/WAVE must be detected as ``.wav`` (regression for the WAV gap). + + WAV shares the ``RIFF`` chunk header with WebP; only the form-type at + bytes 8-11 (``WAVE`` vs ``WEBP``) distinguishes them. Before the fix the + sniffer only handled ``WEBP`` and a WAV file fell through to ``.bin``. + """ + from gateway.platforms.signal import _guess_extension + # Canonical WAV header: 'RIFF' 'WAVE' 'fmt '... + wav = b"RIFF\x24\x08\x00\x00WAVEfmt " + b"\x00" * 100 + assert _guess_extension(wav) == ".wav" + + def test_guess_extension_wav_not_misread_as_webp(self): + """A RIFF container that is NOT WebP must not be returned as ``.webp``.""" + from gateway.platforms.signal import _guess_extension + wav = b"RIFF\x24\x08\x00\x00WAVEfmt " + b"\x00" * 100 + assert _guess_extension(wav) != ".webp" + + def test_guess_extension_wav_routes_to_audio_cache(self): + """A detected WAV must route to the audio cache, not the document cache. + + ``.wav`` is already in ``_is_audio_ext``; the bug was purely that + ``_guess_extension`` never produced ``.wav`` for raw bytes, so the + attachment was treated as a document and STT never received it. + """ + from gateway.platforms.signal import _is_audio_ext, _guess_extension + wav = b"RIFF\x24\x08\x00\x00WAVEfmt " + b"\x00" * 100 + ext = _guess_extension(wav) + assert ext == ".wav" + assert _is_audio_ext(ext) is True + + def test_guess_extension_webp_still_detected(self): + """Guard that adding WAVE detection didn't break the WebP branch.""" + from gateway.platforms.signal import _guess_extension + webp = b"RIFF\x24\x08\x00\x00WEBPVP8 " + b"\x00" * 100 + assert _guess_extension(webp) == ".webp" + def test_guess_extension_aac_adts_unprotected(self): """ADTS AAC, MPEG-4, no CRC (the canonical Android Signal voice note).