feat(compat): Profile, Gates und Pods über den Kompatibilitätskern

Neu: Tier, Profile, Compatibility, CapabilityLocked, CapabilityGate, CompatibilityComponent, ProvidesCompatibility, DiviProfile sowie PodsFeature, PodsCompatibility und PodsIntegration. Das CapabilityGate sperrt gesperrte und unbekannte Fähigkeits-IDs (übernimmt die Sperre unbekannter IDs vom DiviFeatureGate) und delegiert nur Divi ausdrücklich. Pods-Schema, -Lesen, -Schreiben und -Feldrechte laufen über usable(PodsFeature::…); Schreiben verlangt FieldRead und FieldWrite, damit ein Rollback keine Werte verliert. lit-site-status meldet Pods über die CompatibilityComponent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 12:26:54 +01:00
co-authored by Claude Opus 5.5
parent b511ef6348
commit c85131603b
38 changed files with 2259 additions and 135 deletions
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Abilities;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\AbilityRegistrar;
use LIT\MCP\Abilities\DeclaresRequirements;
use LIT\MCP\Compatibility\CapabilityGate;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Server\RequestContext;
use LIT\MCP\Tests\Support\DiviFeatureFixtures;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error;
/**
* The two guards Plugin hands to AbilityRegistrar, [DiviFeatureGate, CapabilityGate], through the real
* permission callback: each ID is judged by exactly one of them and none passes both unjudged.
*/
final class CompatibilityGatesTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
RequestContext::reset(true);
Functions\when('current_user_can')->justReturn(true);
}
protected function tearDown(): void
{
RequestContext::reset();
parent::tearDown();
}
/** @param list<string> $ids */
private function permission(array $ids): bool|WP_Error
{
$ability = Mockery::mock(Ability::class, DeclaresRequirements::class);
$ability->allows('name')->andReturn('lit/test');
$ability->allows('label')->andReturn('Test');
$ability->allows('description')->andReturn('Test');
$ability->allows('inputSchema')->andReturn(['type' => 'object', 'properties' => []]);
$ability->allows('outputSchema')->andReturn(null);
$ability->allows('annotations')->andReturn(['readonly' => true]);
$ability->allows('isDirect')->andReturn(true);
$ability->allows('requirements')->andReturn($ids);
$ability->allows('permission')->andReturn(true);
$free = new Profile('fake', 'Fake Plugin', Tier::LockOnFailure, [FakeCapability::Read]);
$compat = new Compatibility([DiviProfile::profile(), PodsCompatibility::profile(), $free]);
$integration = Mockery::mock(Integration::class);
$integration->allows('id')->andReturn('core');
$integration->allows('isAvailable')->andReturn(true);
$integration->allows('abilities')->andReturn([$ability]);
$registrar = new AbilityRegistrar(
new IntegrationRegistry($integration),
null,
[new DiviFeatureGate(FakeChangeLog::forbidden()), new CapabilityGate($compat, FakeChangeLog::forbidden())]
);
return ($registrar->argsFor($ability)['permission_callback'])([]);
}
public function testAFreeNonDiviIdPassesTheDiviGateAndTheCapabilityGate(): void
{
self::assertTrue($this->permission(['fake-read']));
}
public function testALockedPodsIdPassesTheDiviGateAndIsRefusedByTheCapabilityGate(): void
{
DiviFeatureFixtures::useStates([]);
$error = $this->permission(['pods-field-read']);
self::assertToolError($error, 'compat_incompatible');
}
public function testAnUnknownIdIsRefusedByTheCapabilityGate(): void
{
DiviFeatureFixtures::useStates([]);
$payload = self::assertToolError($this->permission(['page-write', 'nobody-knows-me']), 'compat_incompatible');
self::assertSame(['nobody-knows-me'], $payload['unknown']);
}
public function testAReleasedDiviIdPassesBothGates(): void
{
DiviFeatureFixtures::useStates([]);
self::assertTrue($this->permission(['page-write']));
}
public function testALockedDiviIdIsRefusedByTheDiviGateAloneNotTwice(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite);
self::assertToolError($this->permission(['page-write']), 'divi_untested');
}
}