feat(v3.12.0): media pipeline, AI image generation, capability probe, companion v2.9.0
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

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>
This commit is contained in:
2026-04-25 16:25:58 +02:00
parent 788439e377
commit f203ca88de
140 changed files with 23802 additions and 2253 deletions

View File

@@ -0,0 +1,122 @@
"""F.X.fix #2 — Rank Math SEO roundtrip: write → read → assert equality.
Protects against the regression where ``update_post_seo`` wrote
``rank_math_seo_title`` (wrong key) while ``get_post_seo`` read back
from the same wrong key, so a full write→read cycle looked successful
but the WordPress frontend showed no SEO title because Rank Math never
saw the value.
"""
from __future__ import annotations
import json
from unittest.mock import AsyncMock
import pytest
from plugins.wordpress.handlers.seo import SEOHandler
@pytest.fixture
def fake_wp_state():
"""In-memory meta store shared across fake client get/post calls.
Rank Math is considered active when at least one canonical
rank_math_* key exists in the store, which mirrors the real
_check_seo_plugins detection heuristic.
"""
return {
"meta": {
# Seeded so _check_seo_plugins detects Rank Math as active.
"rank_math_focus_keyword": "",
"rank_math_title": "",
"rank_math_description": "",
}
}
@pytest.fixture
def fake_client(fake_wp_state):
client = AsyncMock()
async def _get(endpoint, params=None, use_custom_namespace=False, use_woocommerce=False):
if endpoint.startswith("posts/"):
return {
"id": 42,
"title": {"rendered": "Test Post"},
"meta": dict(fake_wp_state["meta"]),
}
if endpoint == "posts":
return [
{
"id": 42,
"title": {"rendered": "Test Post"},
"meta": dict(fake_wp_state["meta"]),
}
]
raise AssertionError(f"unexpected GET {endpoint}")
async def _post(endpoint, json_data=None, **_):
if endpoint.startswith("posts/") and json_data and "meta" in json_data:
fake_wp_state["meta"].update(json_data["meta"])
return {"id": 42, "meta": dict(fake_wp_state["meta"])}
raise AssertionError(f"unexpected POST {endpoint}")
client.get = _get
client.post = _post
return client
class TestRankMathRoundtrip:
@pytest.mark.asyncio
async def test_update_writes_canonical_rank_math_title(self, fake_client, fake_wp_state):
handler = SEOHandler(fake_client)
# Force the bridge-status fallback path to detect Rank Math via
# meta heuristic (seeded in fixture).
raw = await handler.update_post_seo(
post_id=42,
focus_keyword="mcp hub",
seo_title="MCP Hub — unified MCP server",
meta_description="Self-hosted MCP platform",
)
resp = json.loads(raw)
assert "rank_math_title" in resp["updated_fields"]
# The wrong key must NOT be written — guards against silent
# regression of the rank_math_seo_title bug.
assert "rank_math_seo_title" not in resp["updated_fields"]
assert fake_wp_state["meta"]["rank_math_title"] == "MCP Hub — unified MCP server"
assert fake_wp_state["meta"]["rank_math_focus_keyword"] == "mcp hub"
assert fake_wp_state["meta"]["rank_math_description"] == "Self-hosted MCP platform"
@pytest.mark.asyncio
async def test_get_reads_canonical_rank_math_title(self, fake_client, fake_wp_state):
fake_wp_state["meta"].update(
{
"rank_math_focus_keyword": "mcp hub",
"rank_math_title": "Readback Title",
"rank_math_description": "Readback Desc",
}
)
handler = SEOHandler(fake_client)
raw = await handler.get_post_seo(post_id=42)
data = json.loads(raw)
assert data["plugin_detected"] == "rank_math"
assert data["seo_title"] == "Readback Title"
assert data["focus_keyword"] == "mcp hub"
assert data["meta_description"] == "Readback Desc"
@pytest.mark.asyncio
async def test_roundtrip_write_then_read_returns_non_empty(self, fake_client, fake_wp_state):
handler = SEOHandler(fake_client)
await handler.update_post_seo(
post_id=42,
focus_keyword="kw",
seo_title="Title A",
meta_description="Desc A",
)
raw = await handler.get_post_seo(post_id=42)
data = json.loads(raw)
# Every field we just wrote must come back non-empty.
assert data["focus_keyword"] == "kw"
assert data["seo_title"] == "Title A"
assert data["meta_description"] == "Desc A"