Files
WP-LIT-MCP/tests/Unit/Compatibility/CompatibilityTest.php
T
Jan-Niclas LoosenandClaude Opus 5.5 c85131603b 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>
2026-11-06 12:26:54 +01:00

234 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Compatibility;
use Closure;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Unit\TestCase;
use LogicException;
use RuntimeException;
final class CompatibilityTest extends TestCase
{
private const CAPS = [FakeCapability::Read, FakeCapability::Write];
/** A manifest whose only call is missing and belongs to fake-write: a hard problem for that capability. */
private static function brokenManifest(): Manifest
{
return new Manifest('Fake', calls: [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])]);
}
private static function lockProfile(
bool $broken = false,
?Closure $active = null,
?Closure $version = null,
?string $minVersion = null,
Tier $tier = Tier::LockOnFailure
): Profile {
return new Profile(
'fake',
'Fake Plugin',
$tier,
self::CAPS,
$broken ? [self::brokenManifest()] : [],
$active,
$version,
$minVersion
);
}
public function testReportOnlyNeverLocksNotEvenWhenInactiveOrBroken(): void
{
$profile = new Profile(
'fake',
'Fake',
Tier::ReportOnly,
self::CAPS,
[self::brokenManifest()],
static fn (): bool => false,
minVersion: '9'
);
$compat = new Compatibility([$profile]);
self::assertSame(ReleaseState::Released, $compat->matrix($profile)->state());
self::assertNull($compat->lockOf(self::CAPS));
self::assertFalse($compat->active($profile));
self::assertSame(['Fake: lit_mcp_missing_function()'], $compat->problems($profile));
}
public function testLockOnFailureLocksOnlyWhatAHardProblemConcerns(): void
{
$compat = new Compatibility([self::lockProfile(broken: true)]);
self::assertTrue($compat->usable(FakeCapability::Read));
self::assertFalse($compat->usable(FakeCapability::Write));
self::assertSame([FakeCapability::Write], array_map(
static fn ($status) => $status->feature,
$compat->lockOf(self::CAPS)?->locked ?? []
));
}
public function testLockOnFailureWithoutProblemsIsFree(): void
{
$compat = new Compatibility([self::lockProfile()]);
self::assertNull($compat->lockOf(self::CAPS));
self::assertSame([], $compat->problems($compat->profile('fake')));
}
public function testAnInactivePluginLocksEverythingWithLockOnFailure(): void
{
$profile = self::lockProfile(active: static fn (): bool => false);
$compat = new Compatibility([$profile]);
self::assertFalse($compat->active($profile));
self::assertFalse($compat->usable(FakeCapability::Read));
self::assertSame(['Fake Plugin ist nicht installiert oder nicht aktiv.'], $compat->problems($profile));
}
public function testAVersionBelowTheMinimumLocksEverything(): void
{
$profile = self::lockProfile(version: static fn (): string => '2.7.9', minVersion: '2.8');
$compat = new Compatibility([$profile]);
self::assertSame('2.7.9', $compat->version($profile));
self::assertFalse($compat->usable(FakeCapability::Read));
self::assertFalse($compat->usable(FakeCapability::Write));
self::assertSame(
['Fake Plugin 2.7.9 ist älter als die Mindestversion 2.8.'],
$compat->problems($profile)
);
}
public function testTheMinimumVersionItselfIsFine(): void
{
$profile = self::lockProfile(version: static fn (): string => '2.8', minVersion: '2.8');
self::assertTrue((new Compatibility([$profile]))->usable(FakeCapability::Read));
}
public function testAnUnknownVersionWithAMinimumLocksFailClosed(): void
{
$profile = self::lockProfile(version: static fn (): ?string => null, minVersion: '2.8');
self::assertFalse((new Compatibility([$profile]))->usable(FakeCapability::Read));
}
public function testAThrowingActiveOrVersionClosureCountsAsInactiveAndUnknown(): void
{
$boom = static function (): never {
throw new RuntimeException('boom');
};
$profile = self::lockProfile(active: $boom, version: $boom);
$compat = new Compatibility([$profile]);
self::assertFalse($compat->active($profile));
self::assertNull($compat->version($profile));
self::assertFalse($compat->usable(FakeCapability::Read));
}
public function testAThrowingCheckLocksEverythingFailClosed(): void
{
$profile = self::lockProfile();
$compat = new Compatibility([$profile], static function (): never {
throw new RuntimeException('boom');
});
self::assertFalse($compat->usable(FakeCapability::Read));
self::assertFalse($compat->usable(FakeCapability::Write));
self::assertSame(
['Die Kompatibilitätsprüfung von Fake Plugin ist fehlgeschlagen.'],
$compat->problems($profile)
);
}
public function testReleasePerVersionTakesItsMatrixFromTheClosure(): void
{
$profile = new Profile(
'fake',
'Fake',
Tier::ReleasePerVersion,
self::CAPS,
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allIncompatible(self::CAPS, 'gesperrt')
);
$compat = new Compatibility([$profile]);
self::assertFalse($compat->usable(FakeCapability::Read));
self::assertSame(['gesperrt'], $compat->problems($profile));
}
public function testMatrixAndCheckAreComputedOncePerProfile(): void
{
$checks = 0;
$matrices = 0;
$lock = self::lockProfile();
$release = new Profile(
'release',
'Release',
Tier::ReleasePerVersion,
[FakeCapability::Other],
matrix: static function () use (&$matrices): CompatibilityMatrix {
$matrices++;
return CompatibilityMatrix::allReleased([FakeCapability::Other]);
}
);
$count = static function (Profile $profile) use (&$checks): ManifestCheck {
$checks++;
return new ManifestCheck($profile->manifests, signatures: false);
};
$compat = new Compatibility([$lock, $release], $count);
foreach ([1, 2, 3] as $ignored) {
$compat->usable(FakeCapability::Read);
$compat->usable(FakeCapability::Other);
$compat->problems($lock);
}
self::assertSame(1, $checks);
self::assertSame(1, $matrices);
}
public function testLookups(): void
{
$lock = self::lockProfile();
$other = new Profile('other', 'Other', Tier::ReportOnly, [FakeCapability::Other]);
$compat = new Compatibility([$lock, $other]);
self::assertSame([$lock, $other], $compat->profiles());
self::assertSame($other, $compat->profile('other'));
self::assertNull($compat->profile('nope'));
self::assertSame(FakeCapability::Other, $compat->capability('fake-other'));
self::assertNull($compat->capability('fake-nope'));
self::assertSame($other, $compat->owner(FakeCapability::Other));
self::assertSame($lock, $compat->owner(FakeCapability::Read));
}
public function testAnIdTwoProfilesClaimIsAProgrammingError(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('fake-read');
new Compatibility([
self::lockProfile(),
new Profile('other', 'Other', Tier::ReportOnly, [FakeCapability::Read]),
]);
}
public function testACapabilityNoProfileOwnsIsNotUsable(): void
{
self::assertFalse((new Compatibility([]))->usable(FakeCapability::Read));
}
}