Files
hermes-agent/tests/gateway/test_telegram_image_precompress.py
T
Teknium 333bf120a5 fix(telegram): harden image pre-compression for salvage of #74893
- Keep main's media_write_timeout=60s (PR's HERMES_* env var dropped per
  .env-is-secrets-only policy; main already fixed the timeout half).
- Replace stdlib imghdr (removed in Python 3.13) with a magic-byte sniff.
- Exclude GIFs: JPEG conversion flattens animations to one frame.
- Fix transparent-PNG handling: RGBA hit the len(getbands())==4 branch
  before the white-background composite, rendering transparency black.
- Clean up temp JPEGs after send (both single and media-group paths);
  the docstring promised caller cleanup that neither call site did.
- Write temp files via tempfile default dir instead of an undefined
  DEFAULT_OUTPUT_DIR (NameError at runtime in the original PR).
- Add real-Pillow regression tests incl. a sabotage-verified
  white-background test.
2026-09-13 20:57:40 -07:00

116 lines
4.2 KiB
Python

"""Tests for Telegram large-image pre-compression (salvage of PR #74893).
Behind slow HTTP proxies, raw PNGs > 1-2MB exceed PTB's media_write_timeout.
``_compress_image_to_jpeg`` converts large raster images to progressive JPEG
(resizing above 1600px) before upload. These tests exercise the real Pillow
path with real file I/O — no mocks on the compression itself.
"""
import os
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
PIL = pytest.importorskip("PIL")
from PIL import Image # noqa: E402
from gateway.platforms.base import PlatformConfig # noqa: E402
from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402
def _adapter():
return TelegramAdapter(PlatformConfig(enabled=True, token="***"))
def _make_png(path, size=(2400, 1800), noisy=True, mode="RGB"):
"""Write a PNG large enough to cross the 1MB compression threshold."""
if noisy:
# True random noise defeats PNG compression so the file exceeds 1MB.
nbytes = size[0] * size[1] * len(mode)
img = Image.frombytes(mode, size, os.urandom(nbytes))
else:
img = Image.new(mode, size)
img.save(path, "PNG")
return path
class TestSniffRasterFormat:
def test_png_magic(self, tmp_path):
p = tmp_path / "x.png"
Image.new("RGB", (4, 4)).save(p, "PNG")
assert TelegramAdapter._sniff_raster_format(str(p)) == "png"
def test_webp_magic(self, tmp_path):
p = tmp_path / "x.webp"
Image.new("RGB", (4, 4)).save(p, "WEBP")
assert TelegramAdapter._sniff_raster_format(str(p)) == "webp"
def test_gif_excluded(self, tmp_path):
p = tmp_path / "x.gif"
Image.new("P", (4, 4)).save(p, "GIF")
assert TelegramAdapter._sniff_raster_format(str(p)) is None
def test_non_image(self, tmp_path):
p = tmp_path / "x.bin"
p.write_bytes(b"not an image at all")
assert TelegramAdapter._sniff_raster_format(str(p)) is None
def test_missing_file(self, tmp_path):
assert TelegramAdapter._sniff_raster_format(str(tmp_path / "nope.png")) is None
class TestCompressImageToJpeg:
def test_large_png_is_compressed_and_resized(self, tmp_path):
adapter = _adapter()
src = _make_png(tmp_path / "big.png")
assert os.path.getsize(src) > adapter._IMG_COMPRESS_THRESHOLD_BYTES
out = adapter._compress_image_to_jpeg(str(src))
assert out is not None
try:
assert out.endswith(".jpg")
assert os.path.getsize(out) < os.path.getsize(src)
with Image.open(out) as jpg:
assert jpg.format == "JPEG"
assert max(jpg.size) <= adapter._IMG_MAX_DIMENSION
finally:
os.remove(out)
def test_small_png_is_left_alone(self, tmp_path):
adapter = _adapter()
src = tmp_path / "small.png"
Image.new("RGB", (32, 32)).save(src, "PNG")
assert adapter._compress_image_to_jpeg(str(src)) is None
def test_jpeg_is_left_alone(self, tmp_path):
adapter = _adapter()
src = tmp_path / "photo.jpg"
Image.new("RGB", (2000, 2000)).save(src, "JPEG")
assert adapter._compress_image_to_jpeg(str(src)) is None
def test_transparent_png_gets_white_background(self, tmp_path):
"""RGBA must composite onto white, not collapse to a black background."""
adapter = _adapter()
src = tmp_path / "transparent.png"
img = Image.new("RGBA", (2000, 2000), (0, 0, 0, 0)) # fully transparent
img.save(src, "PNG")
# Fully-transparent flat PNG compresses tiny; force it over threshold
# by lowering the adapter threshold for this test.
adapter._IMG_COMPRESS_THRESHOLD_BYTES = 0
out = adapter._compress_image_to_jpeg(str(src))
assert out is not None
try:
with Image.open(out) as jpg:
# Transparent areas must render white (255), not black (0).
assert jpg.getpixel((10, 10))[0] > 200
finally:
os.remove(out)
def test_missing_file_returns_none(self, tmp_path):
adapter = _adapter()
assert adapter._compress_image_to_jpeg(str(tmp_path / "gone.png")) is None