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
@@ -13,6 +13,7 @@ use LIT\MCP\Abilities\ToolAvailability;
use LIT\MCP\Admin\AdminTab;
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Core\EnvironmentComponent;
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
@@ -21,6 +22,7 @@ use LIT\MCP\Integrations\Divi\DiviLogoUsage;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\IntegrationRegistry;
@@ -121,6 +123,14 @@ final class DiviIntegrationTest extends TestCase
self::assertFalse(DiviIntegration::builderEnabledFor('post'));
}
public function testProvidesTheDiviCompatibilityProfile(): void
{
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
self::assertEquals([DiviProfile::profile()], $integration->compatibilities());
}
public function testProvidesTheLibraryGroup(): void
{
Functions\when('__')->returnArg();
@@ -265,18 +265,18 @@ final class DiviFeatureGateTest extends TestCase
);
}
public function testAnUnknownRequirementOfTheCoreLocksTheToolAndIsLogged(): void
/** The lock of IDs that are no Divi capability is CapabilityGate's (unknown ones fail closed there). */
public function testIdsThatAreNoDiviCapabilityAreIgnored(): void
{
$log = new FakeChangeLog();
$tool = $this->contentCreate($this->composerNeeding('page-write', 'something-else'));
DiviFeatureFixtures::useStates([
'page-write' => ReleaseState::Incompatible,
'catalog' => ReleaseState::Incompatible,
]);
$tool = $this->contentCreate($this->composerNeeding('pods-field-read', 'something-else'));
$error = $this->gate($log)->refusal($tool, ['post_type' => 'page', 'divi' => [[]]]);
$payload = self::assertToolError($error, 'divi_incompatible');
self::assertSame(['something-else'], $payload['unknown']);
self::assertSame(423, $error->get_error_data()['status'] ?? null);
self::assertSame('lit/content-create', $log->records[0]['ability']);
self::assertSame('refused', $log->records[0]['result']);
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($tool, ['post_type' => 'page', 'divi' => [[]]])
);
}
public function testAnnotateNeverChangesAResult(): void
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Tests\Support\ReleaseMatrices;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class DiviProfileTest extends TestCase
{
private function theme(string $template, string $version = '5.2.1'): void
{
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn($template);
$theme->allows('parent')->andReturn(false);
$theme->allows('get')->with('Version')->andReturn($version);
Functions\when('wp_get_theme')->justReturn($theme);
}
public function testDescribesDiviAsAReleasePerVersionPlugin(): void
{
$profile = DiviProfile::profile();
self::assertSame('divi', $profile->id);
self::assertSame('Divi', $profile->label);
self::assertSame(Tier::ReleasePerVersion, $profile->tier);
self::assertSame(DiviFeature::cases(), $profile->capabilities);
self::assertEquals(array_values(DiviAdapters::manifests()), $profile->manifests);
}
public function testTheMatrixIsTheReleaseGatesMatrix(): void
{
$status = ReleaseMatrices::status([DiviFeature::PageWrite], ReleaseState::Incompatible);
ReleaseGate::useResolver(static fn () => $status);
self::assertSame($status->matrix, (DiviProfile::profile()->matrix)());
}
public function testTheVersionComesFromTheDiviTemplateAndThereIsNoActiveCheck(): void
{
$this->theme('Divi', '5.3.0');
$profile = DiviProfile::profile();
self::assertNull($profile->active, 'Divi has its own component; DiviIntegration decides availability');
self::assertSame('5.3.0', ($profile->version)());
$this->theme('twentytwentyfive');
self::assertNull(($profile->version)());
}
}
@@ -119,20 +119,12 @@ final class FeatureSetsTest extends TestCase
);
}
public function testUnknownIdsNamesTheIdsOfACoreToolThatAreNoDiviFeature(): void
public function testDeclaredByIgnoresIdsThatAreNoDiviFeature(): void
{
$tool = Mockery::mock(Ability::class, DeclaresRequirements::class);
$tool->allows('requirements')->with(['divi' => []])->andReturn(['page-write', 'other-integration', 'catalog']);
$tool->allows('requirements')->andReturn(['pods-field-read', 'page-write']);
self::assertSame(['other-integration'], FeatureSets::unknownIds($tool, ['divi' => []]));
}
public function testUnknownIdsIsEmptyForDiviToolsAndAnyOtherTool(): void
{
$divi = Mockery::mock(Ability::class, RequiresDiviFeatures::class);
self::assertSame([], FeatureSets::unknownIds($divi, []));
self::assertSame([], FeatureSets::unknownIds(Mockery::mock(Ability::class), []));
self::assertSame([DiviFeature::PageWrite], FeatureSets::declaredBy($tool, ['divi' => []]));
}
public function testDeclaredByIsEmptyForAnyOtherTool(): void