From 8ec2aaed142a4971c491767ee0c2e301c8fbe653 Mon Sep 17 00:00:00 2001 From: Kyle Dunn Date: Sun, 19 Apr 2026 23:30:27 -0600 Subject: [PATCH 1/3] fix(gateway): add Signal message type classification for documents --- gateway/platforms/signal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gateway/platforms/signal.py b/gateway/platforms/signal.py index 4df4193bc0dc..7b8fd443d60b 100644 --- a/gateway/platforms/signal.py +++ b/gateway/platforms/signal.py @@ -492,6 +492,8 @@ async def _handle_envelope(self, envelope: dict) -> None: msg_type = MessageType.VOICE elif any(mt.startswith("image/") for mt in media_types): msg_type = MessageType.PHOTO + elif any(mt.startswith("application/") or mt.startswith("text/") for mt in media_types): + msg_type = MessageType.DOCUMENT # Parse timestamp from envelope data (milliseconds since epoch) ts_ms = envelope_data.get("timestamp", 0) From c5308203c69e50e42d83a4261abf063651728520 Mon Sep 17 00:00:00 2001 From: Kyle Dunn Date: Mon, 20 Apr 2026 00:14:05 -0600 Subject: [PATCH 2/3] test(gateway): verify Signal inbound PDF attachment sets MessageType.DOCUMENT --- tests/gateway/test_signal.py | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/gateway/test_signal.py b/tests/gateway/test_signal.py index eee3a0db8aab..1ab7f9430892 100644 --- a/tests/gateway/test_signal.py +++ b/tests/gateway/test_signal.py @@ -640,6 +640,79 @@ def test_signal_has_all_media_methods(self, monkeypatch): assert type(adapter).send_image is not BasePlatformAdapter.send_image +# --------------------------------------------------------------------------- +# Inbound attachment message type classification +# --------------------------------------------------------------------------- + +def _make_dm_envelope(sender: str, attachments: list, text: str = "") -> dict: + """Build a minimal signal-cli DM envelope with the given attachments.""" + return { + "envelope": { + "sourceNumber": sender, + "sourceName": "Test User", + "sourceUuid": "aaaaaaaa-0000-0000-0000-000000000001", + "timestamp": 1700000000000, + "dataMessage": { + "timestamp": 1700000000000, + "message": text, + "expiresInSeconds": 0, + "viewOnce": False, + "attachments": attachments, + }, + } + } + + +class TestSignalInboundMessageTypeClassification: + """_handle_envelope must set MessageType.DOCUMENT for application/* attachments. + + Before the fix, PDFs and other documents left msg_type as MessageType.TEXT, + so run.py's document-context injection (which gates on MessageType.DOCUMENT) + silently dropped the file and the agent never saw it. + """ + + @pytest.mark.asyncio + async def test_pdf_attachment_sets_document_type(self, monkeypatch): + """A PDF attachment (application/pdf) must produce MessageType.DOCUMENT, not TEXT.""" + from gateway.platforms.base import MessageType + + envelope = _make_dm_envelope( + sender="+15559876543", + attachments=[{ + "contentType": "application/pdf", + "id": "6zLO3b-6Yf3zVWeLDctA.pdf", + "size": 508237, + "filename": "report.pdf", + "width": None, + "height": None, + "caption": None, + "uploadTimestamp": 1700000000000, + }], + text="here's the doc", + ) + + adapter = _make_signal_adapter(monkeypatch) + adapter._rpc, _ = _stub_rpc(None) + + dispatched = [] + + async def _fake_handle_message(event): + dispatched.append(event) + + adapter.handle_message = _fake_handle_message + adapter._fetch_attachment = AsyncMock(return_value=("/tmp/report.pdf", ".pdf")) + + await adapter._handle_envelope(envelope) + + assert dispatched, "_handle_envelope did not dispatch any event" + event = dispatched[0] + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "PDFs must be classified as DOCUMENT so run.py injects file context." + ) + assert "/tmp/report.pdf" in event.media_urls + + # --------------------------------------------------------------------------- # send_document now routes through _send_attachment (#5105 bonus) # --------------------------------------------------------------------------- From d999f35eb5571972110e6fd49868965e6c70063b Mon Sep 17 00:00:00 2001 From: Kyle Dunn Date: Mon, 20 Apr 2026 09:40:57 -0600 Subject: [PATCH 3/3] test(gateway): verify Signal inbound text attachment sets MessageType.DOCUMENT --- tests/gateway/test_signal.py | 77 ++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/tests/gateway/test_signal.py b/tests/gateway/test_signal.py index 1ab7f9430892..5238403bf5b5 100644 --- a/tests/gateway/test_signal.py +++ b/tests/gateway/test_signal.py @@ -664,54 +664,97 @@ def _make_dm_envelope(sender: str, attachments: list, text: str = "") -> dict: class TestSignalInboundMessageTypeClassification: - """_handle_envelope must set MessageType.DOCUMENT for application/* attachments. + """_handle_envelope must set MessageType.DOCUMENT for application/* and text/* attachments. Before the fix, PDFs and other documents left msg_type as MessageType.TEXT, so run.py's document-context injection (which gates on MessageType.DOCUMENT) silently dropped the file and the agent never saw it. """ - @pytest.mark.asyncio - async def test_pdf_attachment_sets_document_type(self, monkeypatch): - """A PDF attachment (application/pdf) must produce MessageType.DOCUMENT, not TEXT.""" - from gateway.platforms.base import MessageType - + async def _dispatch_single_attachment(self, monkeypatch, content_type: str, + att_id: str, fetch_path: str, fetch_ext: str): + """Helper: run _handle_envelope with one attachment and return the dispatched event.""" envelope = _make_dm_envelope( sender="+15559876543", attachments=[{ - "contentType": "application/pdf", - "id": "6zLO3b-6Yf3zVWeLDctA.pdf", - "size": 508237, - "filename": "report.pdf", + "contentType": content_type, + "id": att_id, + "size": 1024, + "filename": None, "width": None, "height": None, "caption": None, "uploadTimestamp": 1700000000000, }], - text="here's the doc", ) - adapter = _make_signal_adapter(monkeypatch) adapter._rpc, _ = _stub_rpc(None) - dispatched = [] async def _fake_handle_message(event): dispatched.append(event) adapter.handle_message = _fake_handle_message - adapter._fetch_attachment = AsyncMock(return_value=("/tmp/report.pdf", ".pdf")) - + adapter._fetch_attachment = AsyncMock(return_value=(fetch_path, fetch_ext)) await adapter._handle_envelope(envelope) - assert dispatched, "_handle_envelope did not dispatch any event" - event = dispatched[0] + return dispatched[0] + + @pytest.mark.asyncio + async def test_pdf_attachment_sets_document_type(self, monkeypatch): + """A PDF attachment (application/pdf) must produce MessageType.DOCUMENT, not TEXT.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="application/pdf", + att_id="6zLO3b-6Yf3zVWeLDctA.pdf", + fetch_path="/tmp/report.pdf", + fetch_ext=".pdf", + ) + assert event.message_type == MessageType.DOCUMENT, ( f"Expected DOCUMENT, got {event.message_type}. " "PDFs must be classified as DOCUMENT so run.py injects file context." ) assert "/tmp/report.pdf" in event.media_urls + @pytest.mark.asyncio + async def test_text_plain_attachment_sets_document_type(self, monkeypatch): + """A text/plain attachment must produce MessageType.DOCUMENT, not TEXT.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="text/plain", + att_id="notes.txt", + fetch_path="/tmp/notes.txt", + fetch_ext=".txt", + ) + + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "text/plain must be classified as DOCUMENT so run.py injects file context." + ) + + @pytest.mark.asyncio + async def test_text_html_attachment_sets_document_type(self, monkeypatch): + """A text/html attachment must produce MessageType.DOCUMENT (covers the text/* wildcard).""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="text/html", + att_id="page.html", + fetch_path="/tmp/page.html", + fetch_ext=".html", + ) + + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "text/html must be classified as DOCUMENT so run.py injects file context." + ) + # --------------------------------------------------------------------------- # send_document now routes through _send_attachment (#5105 bonus)