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>
This commit is contained in:
232
tests/test_capability_tier_fit.py
Normal file
232
tests/test_capability_tier_fit.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""F.7e — ``evaluate_tier_fit`` + ``TIER_REQUIREMENTS`` coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from core.capability_probe import (
|
||||
TIER_REQUIREMENTS,
|
||||
_cap_matches,
|
||||
evaluate_tier_fit,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TIER_REQUIREMENTS sanity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTierRequirementsTable:
|
||||
@pytest.mark.unit
|
||||
def test_all_three_tiers_for_every_mapped_plugin(self):
|
||||
for plugin_type, tiers in TIER_REQUIREMENTS.items():
|
||||
assert {"read", "write", "admin"} <= set(tiers.keys()), f"{plugin_type} missing a tier"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_requirements_are_non_empty_sets(self):
|
||||
for plugin_type, tiers in TIER_REQUIREMENTS.items():
|
||||
for tier, reqs in tiers.items():
|
||||
assert isinstance(reqs, set), f"{plugin_type}/{tier} is not a set"
|
||||
assert reqs, f"{plugin_type}/{tier} has no required caps"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _cap_matches: canonical + alias resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCapMatches:
|
||||
@pytest.mark.unit
|
||||
def test_exact_match(self):
|
||||
assert _cap_matches("read", {"read"})
|
||||
assert _cap_matches("manage_options", {"manage_options"})
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_role_satisfies_capability_alias(self):
|
||||
# WP role -> implied capability
|
||||
assert _cap_matches("manage_options", {"administrator"})
|
||||
assert _cap_matches("edit_posts", {"editor"})
|
||||
assert _cap_matches("read", {"subscriber"})
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_write_satisfies_read_and_write(self):
|
||||
# WC "read_write" permission satisfies both read_* and write_*.
|
||||
assert _cap_matches("read_products", {"read_write"})
|
||||
assert _cap_matches("write_products", {"read_write"})
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_missing_capability(self):
|
||||
assert not _cap_matches("manage_options", {"edit_posts"})
|
||||
assert not _cap_matches("write_products", {"read"})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# evaluate_tier_fit: probe-unavailable path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEvaluateTierFit:
|
||||
@pytest.mark.unit
|
||||
def test_probe_unavailable_short_circuits(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"admin",
|
||||
{"probe_available": False, "reason": "companion_not_installed"},
|
||||
)
|
||||
assert fit["status"] == "probe_unavailable"
|
||||
assert fit["reason"] == "companion_not_installed"
|
||||
assert fit["missing"] == []
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_custom_tier_is_always_ok(self):
|
||||
# "custom" means the caller cherry-picked tools — no tier-level
|
||||
# contract to validate.
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"custom",
|
||||
{"probe_available": True, "granted": ["read"]},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
assert fit["required"] == []
|
||||
assert fit["missing"] == []
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_none_tier_is_always_ok(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
None,
|
||||
{"probe_available": True, "granted": ["read"]},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_tier_for_plugin_returns_unknown(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"deploy", # not in WP's table
|
||||
{"probe_available": True, "granted": ["read"]},
|
||||
)
|
||||
assert fit["status"] == "unknown_tier"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_plugin_returns_unknown(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"n8n", # no tier table
|
||||
"read",
|
||||
{"probe_available": True, "granted": ["read"]},
|
||||
)
|
||||
assert fit["status"] == "unknown_tier"
|
||||
|
||||
|
||||
class TestWordPressFit:
|
||||
@pytest.mark.unit
|
||||
def test_admin_tier_with_administrator_role_is_ok(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"admin",
|
||||
{
|
||||
"probe_available": True,
|
||||
"granted": ["administrator", "manage_options", "upload_files"],
|
||||
},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
assert fit["missing"] == []
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_admin_tier_with_editor_role_warns_missing_manage_options(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"admin",
|
||||
{"probe_available": True, "granted": ["editor", "edit_posts", "upload_files"]},
|
||||
)
|
||||
assert fit["status"] == "warning"
|
||||
assert "manage_options" in fit["missing"]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_write_tier_with_editor_role_is_ok(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"write",
|
||||
{"probe_available": True, "granted": ["editor"]},
|
||||
)
|
||||
# editor alias covers edit_posts + upload_files.
|
||||
assert fit["status"] == "ok"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_tier_with_subscriber_is_ok(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"wordpress",
|
||||
"read",
|
||||
{"probe_available": True, "granted": ["subscriber"]},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
|
||||
|
||||
class TestWooCommerceFit:
|
||||
@pytest.mark.unit
|
||||
def test_read_key_satisfies_read_tier(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"woocommerce",
|
||||
"read",
|
||||
{"probe_available": True, "granted": ["read_products", "read_orders"]},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_key_fails_write_tier(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"woocommerce",
|
||||
"write",
|
||||
{"probe_available": True, "granted": ["read_products", "read_orders"]},
|
||||
)
|
||||
assert fit["status"] == "warning"
|
||||
assert "write_products" in fit["missing"]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_write_key_satisfies_both(self):
|
||||
# Using the raw permission alias "read_write" directly.
|
||||
fit_read = evaluate_tier_fit(
|
||||
"woocommerce",
|
||||
"read",
|
||||
{"probe_available": True, "granted": ["read_write"]},
|
||||
)
|
||||
fit_write = evaluate_tier_fit(
|
||||
"woocommerce",
|
||||
"write",
|
||||
{"probe_available": True, "granted": ["read_write"]},
|
||||
)
|
||||
assert fit_read["status"] == "ok"
|
||||
assert fit_write["status"] == "ok"
|
||||
|
||||
|
||||
class TestGiteaFit:
|
||||
@pytest.mark.unit
|
||||
def test_admin_tier_needs_admin_repo_hook(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"gitea",
|
||||
"admin",
|
||||
{
|
||||
"probe_available": True,
|
||||
"granted": ["read:repository", "write:repository"],
|
||||
},
|
||||
)
|
||||
assert fit["status"] == "warning"
|
||||
assert "admin:repo_hook" in fit["missing"]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_write_tier_with_write_scope_ok(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"gitea",
|
||||
"write",
|
||||
{"probe_available": True, "granted": ["write:repository"]},
|
||||
)
|
||||
assert fit["status"] == "ok"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_tier_with_only_user_scope_warns(self):
|
||||
fit = evaluate_tier_fit(
|
||||
"gitea",
|
||||
"read",
|
||||
{"probe_available": True, "granted": ["read:user"]},
|
||||
)
|
||||
assert fit["status"] == "warning"
|
||||
assert "read:repository" in fit["missing"]
|
||||
Reference in New Issue
Block a user