Files
mcphub/tests/plugins/wordpress/test_media_optimize.py
airano-ir f203ca88de
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
feat(v3.12.0): media pipeline, AI image generation, capability probe, companion v2.9.0
Three-month batch sync from internal repo (~80 commits) covering Tracks F.5a, F.7e, F.8, F.17, F.18, F.X.

WordPress media pipeline
- Pillow-based optimization, AI image generation (OpenAI / Stability / Replicate / Google Nano Banana / OpenRouter), chunked + resumable uploads, bulk delete/reassign, idempotent retries.

Capability discovery (F.7e)
- Per-site credential probe + adapters for WordPress / WooCommerce / Gitea, tier-fit unions granted ∪ roles, capability badge UI with HTMX partial re-check, install hint in every companion-unreachable error.

Companion plugin overhaul
- Renamed wordpress-plugin/airano-mcp-seo-bridge → wordpress-plugin/airano-mcp-bridge.
- Eight new endpoints: /capabilities, /bulk-meta, /export, /cache-purge, /transient-flush, /site-health, /audit-hook, /upload-and-attach.
- wp.org Plugin Check pass: i18n, WP_Filesystem, scheme allowlist on audit-hook URL.

Other
- Gitea ergonomics (F.17): batch files, tree, search, compare, releases, fork.
- Opportunistic bcrypt upgrade for legacy SHA-256 admin keys (F.8).
- n8n refactor: structured errors, capability probe, missing tools backfilled.
- Idempotency-Key dedup for AI media upload retries; WP client fast-fails on unreachable sites.

Docs
- README + CLAUDE.md drop the fixed "633 tools" claim. The total grows with each release; per-plugin approximations + dashboard-surfaced counts replace it.
- Tools/Tests badges removed in favour of "Plugins: 10".

Deployment
- PyPI mirror chain, optional BUILD_HTTP_PROXY, Alpine→Yandex apk mirror, Debian-slim Plan-B Dockerfile, mirror.gcr.io variant.

CI
- Black + Ruff clean on Python 3.12; pytest tests/ green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:25:58 +02:00

181 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for F.5a.2 image optimization pipeline."""
from __future__ import annotations
import io
import pytest
PIL = pytest.importorskip("PIL")
from PIL import Image # noqa: E402
from plugins.wordpress.handlers._media_optimize import optimize # noqa: E402
def _make_png(w: int, h: int, mode: str = "RGB") -> bytes:
import os as _os
img = Image.new(mode, (w, h))
# Fill with noise so solid-color PNG compression doesn't undercut JPEG
img.frombytes(_os.urandom(w * h * len(mode)))
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
def _make_jpeg(w: int, h: int, quality: int = 95) -> bytes:
img = Image.new("RGB", (w, h), color=(200, 100, 50))
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality)
return buf.getvalue()
def test_large_jpeg_is_resized():
data = _make_jpeg(4000, 3000)
new_data, new_mime = optimize(data, "image/jpeg", max_edge=1024)
assert new_mime == "image/jpeg"
img = Image.open(io.BytesIO(new_data))
assert max(img.size) == 1024
assert len(new_data) < len(data)
def test_opaque_png_converted_to_jpeg():
data = _make_png(800, 600, mode="RGB")
new_data, new_mime = optimize(data, "image/png", max_edge=2560)
assert new_mime == "image/jpeg"
assert len(new_data) < len(data)
def test_transparent_png_stays_png():
data = _make_png(800, 600, mode="RGBA")
new_data, new_mime = optimize(data, "image/png", max_edge=2560)
assert new_mime == "image/png"
def test_small_image_returns_original_bytes():
# Tiny image that cannot be made smaller
data = _make_jpeg(8, 8, quality=50)
new_data, new_mime = optimize(data, "image/jpeg", max_edge=2560)
# Either unchanged or still JPEG
assert new_mime in (None, "image/jpeg")
assert len(new_data) <= len(data) + 1024
def test_pdf_passthrough():
data = b"%PDF-1.4\n%garbage\n%%EOF"
new_data, new_mime = optimize(data, "application/pdf")
assert new_data == data
assert new_mime == "application/pdf"
def test_video_passthrough():
data = b"\x00" * 32
new_data, new_mime = optimize(data, "video/mp4")
assert new_data == data
assert new_mime == "video/mp4"
def test_unknown_mime_tries_anyway_but_fails_gracefully():
# Garbage bytes with no PIL-recognizable format → passthrough
data = b"not an image at all"
new_data, new_mime = optimize(data, None)
assert new_data == data
# ---------------------------------------------------------------------------
# F.5a.8.1 — convert_to override (WebP / AVIF)
# ---------------------------------------------------------------------------
def _peek_format(data: bytes) -> str:
return (Image.open(io.BytesIO(data)).format or "").upper()
def test_convert_png_to_webp():
data = _make_png(64, 64, mode="RGB")
new_data, new_mime = optimize(data, "image/png", convert_to="webp")
assert new_mime == "image/webp"
assert _peek_format(new_data) == "WEBP"
def test_convert_jpeg_to_webp():
data = _make_jpeg(64, 64)
new_data, new_mime = optimize(data, "image/jpeg", convert_to="webp")
assert new_mime == "image/webp"
assert _peek_format(new_data) == "WEBP"
def test_convert_preserves_alpha_on_webp():
data = _make_png(64, 64, mode="RGBA")
new_data, new_mime = optimize(data, "image/png", convert_to="webp")
assert new_mime == "image/webp"
img = Image.open(io.BytesIO(new_data))
# Either RGBA or palette-with-alpha is acceptable
assert "A" in img.mode or "A" in "".join(img.getbands())
def test_convert_avif_falls_back_to_webp_when_unsupported():
from plugins.wordpress.handlers._media_optimize import _avif_supported
data = _make_png(32, 32, mode="RGB")
new_data, new_mime = optimize(data, "image/png", convert_to="avif")
if _avif_supported():
assert new_mime == "image/avif"
assert _peek_format(new_data) == "AVIF"
else:
assert new_mime == "image/webp"
assert _peek_format(new_data) == "WEBP"
def test_convert_to_env_default(monkeypatch):
import plugins.wordpress.handlers._media_optimize as opt_mod
monkeypatch.setattr(opt_mod, "_DEFAULT_CONVERT_TO", "webp")
data = _make_png(48, 48, mode="RGB")
new_data, new_mime = optimize(data, "image/png")
assert new_mime == "image/webp"
assert _peek_format(new_data) == "WEBP"
def test_convert_wins_over_size_guard():
# 1×1 images re-encode to formats that carry larger container overhead.
# The explicit convert_to request MUST still win — the size guard only
# applies to the implicit recompression path.
tiny = _make_png(1, 1, mode="RGB")
new_data, new_mime = optimize(tiny, "image/png", convert_to="webp")
assert new_mime == "image/webp"
assert _peek_format(new_data) == "WEBP"
def test_convert_unknown_value_falls_through_to_heuristic():
# "jpg" isn't in the convert_to map → optimizer uses source-format heuristic.
data = _make_jpeg(64, 64)
_, new_mime = optimize(data, "image/jpeg", convert_to="jpg")
assert new_mime == "image/jpeg"
def test_convert_to_does_not_touch_pdf():
data = b"%PDF-1.4\n%fake\n"
new_data, new_mime = optimize(data, "application/pdf", convert_to="webp")
assert new_data == data
assert new_mime == "application/pdf"
def test_maybe_optimize_forwards_convert_to():
from plugins.wordpress.handlers.media import _maybe_optimize
out, mime = _maybe_optimize(
_make_png(48, 48, mode="RGB"), "image/png", skip=False, convert_to="webp"
)
assert mime == "image/webp"
assert _peek_format(out) == "WEBP"
def test_maybe_optimize_skip_overrides_convert_to():
from plugins.wordpress.handlers.media import _maybe_optimize
src = _make_png(48, 48, mode="RGB")
out, mime = _maybe_optimize(src, "image/png", skip=True, convert_to="webp")
assert out == src
assert mime == "image/png"