diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 125bc1fb6adf..6344520da3c6 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -2267,8 +2267,11 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]: # Extract MEDIA: tags, allowing optional whitespace after the colon # and quoted/backticked paths for LLM-formatted outputs. + # NOTE: Keep this extension list in sync with `_LOCAL_MEDIA_EXTS` in + # `extract_local_files` below. Mismatches cause MEDIA: directives to be + # silently dropped while bare-path detection (or vice versa) still works. media_pattern = re.compile( - r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' + r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|bmp|tiff|svg|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|tar|gz|tgz|bz2|xz|rar|7z|docx?|odt|rtf|xlsx?|ods|pptx?|odp|key|txt|md|csv|tsv|json|xml|yaml|yml|html|htm|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' ) for match in media_pattern.finditer(content): path = match.group("path").strip() diff --git a/tests/gateway/test_platform_base.py b/tests/gateway/test_platform_base.py index 3f303d0377c1..8b5cbcb5b1c3 100644 --- a/tests/gateway/test_platform_base.py +++ b/tests/gateway/test_platform_base.py @@ -794,3 +794,39 @@ def test_http_proxy_falls_back_without_aiohttp_socks(self): sess_kw, req_kw = proxy_kwargs_for_aiohttp("http://proxy:8080") assert sess_kw == {} assert req_kw == {"proxy": "http://proxy:8080"} + + +class TestExtractMediaExtensionCoverage: + """Regression: MEDIA: directive extension list must cover every extension + that ``extract_local_files`` accepts. A mismatch causes MEDIA tags for + common artifact types (e.g. ``.html`` exports) to be silently dropped — + ``send_message`` reports success while the document never ships. + """ + + @pytest.mark.parametrize("ext", [ + # Originally-missing extensions that triggered the regression + "html", "htm", "md", "svg", "bmp", "tiff", + "json", "xml", "yaml", "yml", "tsv", + "ods", "odp", "odt", "rtf", "key", + "tar", "gz", "tgz", "bz2", "xz", + # Extensions that were already covered — keep matching + "png", "jpg", "jpeg", "gif", "webp", + "mp4", "mov", "mkv", "webm", + "mp3", "wav", "ogg", "m4a", "flac", + "pdf", "docx", "xlsx", "pptx", "txt", "csv", "zip", "7z", + ]) + def test_media_tag_extracted_for_extension(self, ext): + text = f"Here you go MEDIA:/tmp/report.{ext} done" + media, cleaned = BasePlatformAdapter.extract_media(text) + assert media == [(f"/tmp/report.{ext}", False)], ( + f"MEDIA: directive for .{ext} was silently dropped — extension" + f" missing from extract_media regex" + ) + assert f"/tmp/report.{ext}" not in cleaned + + def test_media_tag_html_real_world_path(self): + """Specific case that produced the user-visible bug.""" + text = "MEDIA:/root/.hermes/media_cache/00-Visual-Report.html" + media, cleaned = BasePlatformAdapter.extract_media(text) + assert media == [("/root/.hermes/media_cache/00-Visual-Report.html", False)] + assert cleaned == ""