Files
mcphub/tests/test_capability_tier_fit.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

233 lines
7.4 KiB
Python

"""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"]