<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="21" skipped="0" tests="21" time="0.560" timestamp="2026-08-16T22:48:01.738248+09:00" hostname="cube-B850M-AORUS-ELITE-WIFI6E-ICE"><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_pdf_document_cached" time="0.049"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 865278, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d328140&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00d4fc380&gt;

    @pytest.mark.asyncio
    async def test_pdf_document_cached(self, adapter):
        """A PDF attachment should be downloaded, cached, typed as DOCUMENT."""
        pdf_bytes = b"%PDF-1.4 fake content"
    
        with _mock_aiohttp_download(pdf_bytes):
            msg = make_message([make_attachment(filename="report.pdf", content_type="application/pdf")])
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
        assert event.message_type == MessageType.DOCUMENT
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 865278, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:173: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_txt_content_injected" time="0.006"><failure message="AssertionError: assert '[Content of notes.txt]:' in 'summarize this'&#10; +  where 'summarize this' = MessageEvent(text='summarize this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platform=&lt;Pl...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 895649, tzinfo=datetime.timezone.utc)).text">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d41a870&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c8afc80&gt;

    @pytest.mark.asyncio
    async def test_txt_content_injected(self, adapter):
        """.txt file under 100KB should have its content injected into event.text."""
        file_content = b"Hello from a text file"
    
        with _mock_aiohttp_download(file_content):
            msg = make_message(
                attachments=[make_attachment(filename="notes.txt", content_type="text/plain")],
                content="summarize this",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert "[Content of notes.txt]:" in event.text
E       AssertionError: assert '[Content of notes.txt]:' in 'summarize this'
E        +  where 'summarize this' = MessageEvent(text='summarize this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platform=&lt;Pl...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 895649, tzinfo=datetime.timezone.utc)).text

tests/gateway/test_discord_document_handling.py:191: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_md_content_injected" time="0.005"><failure message="AssertionError: assert '[Content of readme.md]:' in '(The user sent a message with no text content)'&#10; +  where '(The user sent a message with no text content)' = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 903637, tzinfo=datetime.timezone.utc)).text">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d3286e0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c99b6e0&gt;

    @pytest.mark.asyncio
    async def test_md_content_injected(self, adapter):
        """.md file under 100KB should have its content injected."""
        file_content = b"# Title\nSome markdown content"
    
        with _mock_aiohttp_download(file_content):
            msg = make_message(
                attachments=[make_attachment(filename="readme.md", content_type="text/markdown")],
                content="",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert "[Content of readme.md]:" in event.text
E       AssertionError: assert '[Content of readme.md]:' in '(The user sent a message with no text content)'
E        +  where '(The user sent a message with no text content)' = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 903637, tzinfo=datetime.timezone.utc)).text

tests/gateway/test_discord_document_handling.py:210: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_log_content_injected" time="0.005"><failure message="AssertionError: assert '[Content of btsnoop_hci.log]:' in 'please inspect this'&#10; +  where 'please inspect this' = MessageEvent(text='please inspect this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platfor...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 910938, tzinfo=datetime.timezone.utc)).text">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d328bf0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c9e43b0&gt;

    @pytest.mark.asyncio
    async def test_log_content_injected(self, adapter):
        """.log file under 100KB should be treated as text/plain and injected."""
        file_content = b"BLE trace line 1\nBLE trace line 2"
    
        with _mock_aiohttp_download(file_content):
            msg = make_message(
                attachments=[make_attachment(filename="btsnoop_hci.log", content_type="text/plain")],
                content="please inspect this",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert "[Content of btsnoop_hci.log]:" in event.text
E       AssertionError: assert '[Content of btsnoop_hci.log]:' in 'please inspect this'
E        +  where 'please inspect this' = MessageEvent(text='please inspect this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platfor...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 910938, tzinfo=datetime.timezone.utc)).text

tests/gateway/test_discord_document_handling.py:226: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_mid_sized_zip_under_32mb_is_cached" time="0.005"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 917777, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d3297c0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c9eb2f0&gt;

    @pytest.mark.asyncio
    async def test_mid_sized_zip_under_32mb_is_cached(self, adapter):
        """A 25MB .zip should be accepted now that Discord documents allow up to 32MB."""
        msg = make_message([
            make_attachment(
                filename="bugreport.zip",
                content_type="application/zip",
                size=25 * 1024 * 1024,
            )
        ])
    
        with _mock_aiohttp_download(b"PK\x03\x04test"):
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 917777, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:262: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_zip_document_cached" time="0.005"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 925020, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d329d60&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c9d35f0&gt;

    @pytest.mark.asyncio
    async def test_zip_document_cached(self, adapter):
        """A .zip file should be cached as a supported document."""
        msg = make_message([
            make_attachment(filename="archive.zip", content_type="application/zip")
        ])
    
        with _mock_aiohttp_download(b"PK\x03\x04test"):
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 925020, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:276: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_large_txt_cached_not_injected" time="0.004"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 932610, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d447440&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00ca4b8c0&gt;

    @pytest.mark.asyncio
    async def test_large_txt_cached_not_injected(self, adapter):
        """.txt over 100KB should be cached but NOT injected into event.text."""
        large_content = b"x" * (200 * 1024)
    
        with _mock_aiohttp_download(large_content):
            msg = make_message(
                attachments=[make_attachment(filename="big.txt", content_type="text/plain", size=len(large_content))],
                content="",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 932610, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:316: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling" name="test_multiple_text_files_both_injected" time="0.004"><failure message="AssertionError: assert '[Content of file1.txt]:' in '(The user sent a message with no text content)'&#10; +  where '(The user sent a message with no text content)' = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 939862, tzinfo=datetime.timezone.utc)).text">self = &lt;tests.gateway.test_discord_document_handling.TestIncomingDocumentHandling object at 0x78b00d446db0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00ca49970&gt;

    @pytest.mark.asyncio
    async def test_multiple_text_files_both_injected(self, adapter):
        """Two text file attachments should both be injected into event.text in order."""
        content1 = b"First file content"
        content2 = b"Second file content"
    
        call_count = 0
        responses = [content1, content2]
    
        def make_session(_responses):
            idx = 0
    
            class FakeSession:
                async def __aenter__(self):
                    return self
    
                async def __aexit__(self, *_):
                    pass
    
                def get(self, url, **kwargs):
                    nonlocal idx
                    data = _responses[idx % len(_responses)]
                    idx += 1
    
                    resp = AsyncMock()
                    resp.status = 200
                    resp.read = AsyncMock(return_value=data)
                    resp.__aenter__ = AsyncMock(return_value=resp)
                    resp.__aexit__ = AsyncMock(return_value=False)
                    return resp
    
            return FakeSession()
    
        with patch("aiohttp.ClientSession", return_value=make_session([content1, content2])):
            msg = make_message(
                attachments=[
                    make_attachment(filename="file1.txt", content_type="text/plain"),
                    make_attachment(filename="file2.txt", content_type="text/plain"),
                ],
                content="",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert "[Content of file1.txt]:" in event.text
E       AssertionError: assert '[Content of file1.txt]:' in '(The user sent a message with no text content)'
E        +  where '(The user sent a message with no text content)' = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 939862, tzinfo=datetime.timezone.utc)).text

tests/gateway/test_discord_document_handling.py:364: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestAllowAnyAttachment" name="test_unknown_type_cached_when_flag_on" time="0.006"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 964913, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestAllowAnyAttachment object at 0x78b00d4461e0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00ca4aa50&gt;

    @pytest.mark.asyncio
    async def test_unknown_type_cached_when_flag_on(self, adapter):
        """Flag on: unknown extension is cached as application/octet-stream."""
        adapter.config.extra["allow_any_attachment"] = True
    
        with _mock_aiohttp_download(b"\x00\x01\x02 binary payload"):
            msg = make_message([
                make_attachment(filename="weird.xyz", content_type="application/x-custom")
            ])
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 964913, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:428: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestAllowAnyAttachment" name="test_unknown_type_no_content_type_becomes_octet_stream" time="0.005"><failure message="AssertionError: assert [] == ['application/octet-stream']&#10;  &#10;  Right contains one more item: 'application/octet-stream'&#10;  Use -v to get more diff">self = &lt;tests.gateway.test_discord_document_handling.TestAllowAnyAttachment object at 0x78b00d445ac0&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00d4ddaf0&gt;

    @pytest.mark.asyncio
    async def test_unknown_type_no_content_type_becomes_octet_stream(self, adapter):
        """Flag on + no content_type from discord: MIME falls back to octet-stream."""
        adapter.config.extra["allow_any_attachment"] = True
    
        with _mock_aiohttp_download(b"raw bytes"):
            msg = make_message([
                make_attachment(filename="mystery.bin", content_type=None)
            ])
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
        assert event.message_type == MessageType.DOCUMENT
&gt;       assert event.media_types == ["application/octet-stream"]
E       AssertionError: assert [] == ['application/octet-stream']
E         
E         Right contains one more item: 'application/octet-stream'
E         Use -v to get more diff

tests/gateway/test_discord_document_handling.py:450: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestAllowAnyAttachment" name="test_max_attachment_bytes_zero_means_unlimited" time="0.005"><failure message="AssertionError: assert 0 == 1&#10; +  where 0 = len([])&#10; +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 980381, tzinfo=datetime.timezone.utc)).media_urls">self = &lt;tests.gateway.test_discord_document_handling.TestAllowAnyAttachment object at 0x78b00d4eec60&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00d43f920&gt;

    @pytest.mark.asyncio
    async def test_max_attachment_bytes_zero_means_unlimited(self, adapter):
        """max_attachment_bytes=0 disables the size cap entirely."""
        adapter.config.extra["allow_any_attachment"] = True
        adapter.config.extra["max_attachment_bytes"] = 0
    
        # 64 MiB — would normally exceed the historical 32 MiB hardcoded cap.
        with _mock_aiohttp_download(b"x" * 16):
            msg = make_message([
                make_attachment(
                    filename="huge.xyz",
                    content_type="application/x-custom",
                    size=64 * 1024 * 1024,
                )
            ])
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert len(event.media_urls) == 1
E       AssertionError: assert 0 == 1
E        +  where 0 = len([])
E        +    where [] = MessageEvent(text='(The user sent a message with no text content)', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, s...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 980381, tzinfo=datetime.timezone.utc)).media_urls

tests/gateway/test_discord_document_handling.py:488: AssertionError</failure></testcase><testcase classname="tests.gateway.test_discord_document_handling.TestAllowAnyAttachment" name="test_allowlisted_doc_unchanged_when_flag_on" time="0.005"><failure message="AssertionError: assert '[Content of notes.txt]:' in 'check this'&#10; +  where 'check this' = MessageEvent(text='check this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platform=&lt;Platfo...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 987775, tzinfo=datetime.timezone.utc)).text">self = &lt;tests.gateway.test_discord_document_handling.TestAllowAnyAttachment object at 0x78b00d4ee660&gt;
adapter = &lt;plugins.platforms.discord.adapter.DiscordAdapter object at 0x78b00c9d19a0&gt;

    @pytest.mark.asyncio
    async def test_allowlisted_doc_unchanged_when_flag_on(self, adapter):
        """Flag on must not change handling of types already in SUPPORTED_DOCUMENT_TYPES.
    
        A .txt should still get its content inlined (the historical behavior),
        and the MIME should still be the canonical text/plain — not whatever
        discord guessed.
        """
        adapter.config.extra["allow_any_attachment"] = True
        file_content = b"still a text file"
    
        with _mock_aiohttp_download(file_content):
            msg = make_message(
                attachments=[make_attachment(filename="notes.txt", content_type="text/plain")],
                content="check this",
            )
            await adapter._handle_message(msg)
    
        event = adapter.handle_message.call_args[0][0]
&gt;       assert "[Content of notes.txt]:" in event.text
E       AssertionError: assert '[Content of notes.txt]:' in 'check this'
E        +  where 'check this' = MessageEvent(text='check this', message_type=&lt;MessageType.DOCUMENT: 'document'&gt;, source=SessionSource(platform=&lt;Platfo...ontext=None, internal=False, timestamp=datetime.datetime(2026, 8, 16, 13, 48, 1, 987775, tzinfo=datetime.timezone.utc)).text

tests/gateway/test_discord_document_handling.py:509: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_external_media_download_rejects_oversized_content_length" time="0.007"><failure message="AssertionError: Regex pattern did not match.&#10;  Expected regex: 'exceeds Matrix limit'&#10;  Actual message: 'blocked unsafe media URL'">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d340fb0&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d35f0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_oversized_content_length(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"x"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {"Content-Length": "11"}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        self.adapter._max_media_bytes = 10
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
        with pytest.raises(ValueError, match="exceeds Matrix limit"):
&gt;           await self.adapter._download_external_media_with_cap(
                "https://example.com/image.png"
            )

tests/gateway/test_matrix.py:3276: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;gateway.platforms.matrix.MatrixAdapter object at 0x78b00c9d0e00&gt;
url = 'https://example.com/image.png'

    async def _download_external_media_with_cap(self, url: str) -&gt; tuple[bytes, str, str]:
        """Download external media while enforcing redirect safety and size caps."""
        from tools.url_safety import is_safe_url
    
        if not is_safe_url(url):
&gt;           raise ValueError("blocked unsafe media URL")
E           ValueError: blocked unsafe media URL

gateway/platforms/matrix.py:1723: ValueError

During handling of the above exception, another exception occurred:

self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d340fb0&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d35f0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_oversized_content_length(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"x"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {"Content-Length": "11"}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        self.adapter._max_media_bytes = 10
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
&gt;       with pytest.raises(ValueError, match="exceeds Matrix limit"):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: Regex pattern did not match.
E         Expected regex: 'exceeds Matrix limit'
E         Actual message: 'blocked unsafe media URL'

tests/gateway/test_matrix.py:3275: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_external_media_download_rejects_oversized_stream" time="0.006"><failure message="AssertionError: Regex pattern did not match.&#10;  Expected regex: 'exceeds Matrix limit'&#10;  Actual message: 'blocked unsafe media URL'">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d341520&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d0ef0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_oversized_stream(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"12345"
                yield b"67890"
                yield b"!"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        self.adapter._max_media_bytes = 10
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
        with pytest.raises(ValueError, match="exceeds Matrix limit"):
&gt;           await self.adapter._download_external_media_with_cap(
                "https://example.com/image.png"
            )

tests/gateway/test_matrix.py:3319: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;gateway.platforms.matrix.MatrixAdapter object at 0x78b00d4d7e30&gt;
url = 'https://example.com/image.png'

    async def _download_external_media_with_cap(self, url: str) -&gt; tuple[bytes, str, str]:
        """Download external media while enforcing redirect safety and size caps."""
        from tools.url_safety import is_safe_url
    
        if not is_safe_url(url):
&gt;           raise ValueError("blocked unsafe media URL")
E           ValueError: blocked unsafe media URL

gateway/platforms/matrix.py:1723: ValueError

During handling of the above exception, another exception occurred:

self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d341520&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d0ef0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_oversized_stream(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"12345"
                yield b"67890"
                yield b"!"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        self.adapter._max_media_bytes = 10
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
&gt;       with pytest.raises(ValueError, match="exceeds Matrix limit"):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: Regex pattern did not match.
E         Expected regex: 'exceeds Matrix limit'
E         Actual message: 'blocked unsafe media URL'

tests/gateway/test_matrix.py:3318: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_external_media_download_rejects_unsafe_redirect" time="0.006"><failure message="AssertionError: Regex pattern did not match.&#10;  Expected regex: 'unsafe redirect'&#10;  Actual message: 'blocked unsafe media URL'">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d341c40&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d0cb0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_unsafe_redirect(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"ok"
    
        class _Response:
            url = "http://127.0.0.1/private.png"
            headers = {}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
        with pytest.raises(ValueError, match="unsafe redirect"):
&gt;           await self.adapter._download_external_media_with_cap(
                "https://example.com/image.png"
            )

tests/gateway/test_matrix.py:3359: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;gateway.platforms.matrix.MatrixAdapter object at 0x78b00d599d60&gt;
url = 'https://example.com/image.png'

    async def _download_external_media_with_cap(self, url: str) -&gt; tuple[bytes, str, str]:
        """Download external media while enforcing redirect safety and size caps."""
        from tools.url_safety import is_safe_url
    
        if not is_safe_url(url):
&gt;           raise ValueError("blocked unsafe media URL")
E           ValueError: blocked unsafe media URL

gateway/platforms/matrix.py:1723: ValueError

During handling of the above exception, another exception occurred:

self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d341c40&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00c9d0cb0&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_unsafe_redirect(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"ok"
    
        class _Response:
            url = "http://127.0.0.1/private.png"
            headers = {}
            content_type = "image/png"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
&gt;       with pytest.raises(ValueError, match="unsafe redirect"):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: Regex pattern did not match.
E         Expected regex: 'unsafe redirect'
E         Actual message: 'blocked unsafe media URL'

tests/gateway/test_matrix.py:3358: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_external_media_download_rejects_non_image_content_type" time="0.006"><failure message="AssertionError: Regex pattern did not match.&#10;  Expected regex: 'not an image'&#10;  Actual message: 'blocked unsafe media URL'">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d3428d0&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00d59be90&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_non_image_content_type(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"&lt;html&gt;&lt;/html&gt;"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {}
            content_type = "text/html"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
        with pytest.raises(ValueError, match="not an image"):
&gt;           await self.adapter._download_external_media_with_cap(
                "https://example.com/image.png"
            )

tests/gateway/test_matrix.py:3406: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;gateway.platforms.matrix.MatrixAdapter object at 0x78b00c9b7f80&gt;
url = 'https://example.com/image.png'

    async def _download_external_media_with_cap(self, url: str) -&gt; tuple[bytes, str, str]:
        """Download external media while enforcing redirect safety and size caps."""
        from tools.url_safety import is_safe_url
    
        if not is_safe_url(url):
&gt;           raise ValueError("blocked unsafe media URL")
E           ValueError: blocked unsafe media URL

gateway/platforms/matrix.py:1723: ValueError

During handling of the above exception, another exception occurred:

self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d3428d0&gt;
monkeypatch = &lt;_pytest.monkeypatch.MonkeyPatch object at 0x78b00d59be90&gt;

    @pytest.mark.asyncio
    async def test_external_media_download_rejects_non_image_content_type(self, monkeypatch):
        import aiohttp
    
        class _Content:
            async def iter_chunked(self, _size):
                yield b"&lt;html&gt;&lt;/html&gt;"
    
        class _Response:
            url = "https://example.com/image.png"
            headers = {}
            content_type = "text/html"
            content = _Content()
    
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def raise_for_status(self):
                return None
    
        class _Session:
            async def __aenter__(self):
                return self
    
            async def __aexit__(self, *_args):
                return None
    
            def get(self, *_args, **_kwargs):
                return _Response()
    
        monkeypatch.setattr(aiohttp, "ClientSession", lambda **_kwargs: _Session())
    
&gt;       with pytest.raises(ValueError, match="not an image"):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: Regex pattern did not match.
E         Expected regex: 'not an image'
E         Actual message: 'blocked unsafe media URL'

tests/gateway/test_matrix.py:3405: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_send_image_failure_log_redacts_signed_url" time="0.006"><failure message="AssertionError: assert 'https://example.com/image.png' in 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n'&#10; +  where 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n' = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00c999460&gt;.text">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d342f30&gt;
caplog = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00c999460&gt;

    @pytest.mark.asyncio
    async def test_send_image_failure_log_redacts_signed_url(self, caplog):
        from gateway.platforms.base import SendResult
    
        signed_url = "https://example.com/image.png?signature=secret-token#frag"
        self.adapter._download_external_media_with_cap = AsyncMock(
            side_effect=ValueError("download failed")
        )
        self.adapter.send = AsyncMock(return_value=SendResult(success=True))
    
        await self.adapter.send_image("!room:example.org", signed_url)
    
&gt;       assert "https://example.com/image.png" in caplog.text
E       AssertionError: assert 'https://example.com/image.png' in 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n'
E        +  where 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n' = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00c999460&gt;.text

tests/gateway/test_matrix.py:3422: AssertionError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_send_image_failure_response_does_not_expose_signed_url_query" time="0.006"><failure message="IndexError: tuple index out of range">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d343500&gt;

    @pytest.mark.asyncio
    async def test_send_image_failure_response_does_not_expose_signed_url_query(self):
        from gateway.platforms.base import SendResult
    
        signed_url = "https://example.com/image.png?signature=secret-token"
        self.adapter._download_external_media_with_cap = AsyncMock(
            side_effect=ValueError("download failed")
        )
        self.adapter.send = AsyncMock(return_value=SendResult(success=True))
    
        await self.adapter.send_image("!room:example.org", signed_url)
    
&gt;       sent_text = self.adapter.send.await_args.args[1]
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       IndexError: tuple index out of range

tests/gateway/test_matrix.py:3438: IndexError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_send_image_failure_response_does_not_expose_signed_url_fragment" time="0.006"><failure message="IndexError: tuple index out of range">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d343d40&gt;

    @pytest.mark.asyncio
    async def test_send_image_failure_response_does_not_expose_signed_url_fragment(self):
        from gateway.platforms.base import SendResult
    
        signed_url = "https://example.com/image.png#fragment-secret"
        self.adapter._download_external_media_with_cap = AsyncMock(
            side_effect=ValueError("download failed")
        )
        self.adapter.send = AsyncMock(return_value=SendResult(success=True))
    
        await self.adapter.send_image("!room:example.org", signed_url)
    
&gt;       sent_text = self.adapter.send.await_args.args[1]
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       IndexError: tuple index out of range

tests/gateway/test_matrix.py:3456: IndexError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_send_image_failure_response_preserves_caption" time="0.006"><failure message="IndexError: tuple index out of range">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d33d1f0&gt;

    @pytest.mark.asyncio
    async def test_send_image_failure_response_preserves_caption(self):
        from gateway.platforms.base import SendResult
    
        signed_url = "https://example.com/image.png?signature=secret-token#fragment"
        self.adapter._download_external_media_with_cap = AsyncMock(
            side_effect=ValueError("download failed")
        )
        self.adapter.send = AsyncMock(return_value=SendResult(success=True))
    
        await self.adapter.send_image(
            "!room:example.org",
            signed_url,
            caption="Here is the image",
        )
    
&gt;       sent_text = self.adapter.send.await_args.args[1]
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       IndexError: tuple index out of range

tests/gateway/test_matrix.py:3478: IndexError</failure></testcase><testcase classname="tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization" name="test_send_image_failure_log_still_redacts_signed_url" time="0.006"><failure message="AssertionError: assert 'https://example.com/image.png' in 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n'&#10; +  where 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n' = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00cae5490&gt;.text">self = &lt;tests.gateway.test_matrix.TestMatrixImageOnlyMediaNormalization object at 0x78b00d33e390&gt;
caplog = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00cae5490&gt;

    @pytest.mark.asyncio
    async def test_send_image_failure_log_still_redacts_signed_url(self, caplog):
        from gateway.platforms.base import SendResult
    
        signed_url = "https://example.com/image.png?signature=secret-token#fragment"
        self.adapter._download_external_media_with_cap = AsyncMock(
            side_effect=ValueError("download failed")
        )
        self.adapter.send = AsyncMock(return_value=SendResult(success=True))
    
        await self.adapter.send_image("!room:example.org", signed_url)
    
&gt;       assert "https://example.com/image.png" in caplog.text
E       AssertionError: assert 'https://example.com/image.png' in 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n'
E        +  where 'WARNING  tools.url_safety:url_safety.py:351 Blocked request — DNS resolution failed for: example.com\nWARNING  gateway.platforms.matrix:matrix.py:1689 Matrix: blocked unsafe image URL (SSRF protection)\n' = &lt;_pytest.logging.LogCaptureFixture object at 0x78b00cae5490&gt;.text

tests/gateway/test_matrix.py:3497: AssertionError</failure></testcase></testsuite></testsuites>