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:
co-authored by
Claude Opus 5.5
parent
b511ef6348
commit
c85131603b
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Compatibility;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Compatibility\CapabilityGate;
|
||||
use LIT\MCP\Compatibility\Compatibility;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Tests\Support\FakeCapability;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class CapabilityGateTest extends TestCase
|
||||
{
|
||||
/** @param list<string>|null $ids null = the ability does not implement DeclaresRequirements. */
|
||||
private function tool(bool $readonly, ?array $ids): Ability
|
||||
{
|
||||
$ability = $ids === null
|
||||
? Mockery::mock(Ability::class)
|
||||
: Mockery::mock(Ability::class, DeclaresRequirements::class);
|
||||
$ability->allows('name')->andReturn('lit/fake');
|
||||
$ability->allows('annotations')->andReturn(['readonly' => $readonly]);
|
||||
if ($ids !== null) {
|
||||
$ability->allows('requirements')->andReturn($ids);
|
||||
}
|
||||
|
||||
return $ability;
|
||||
}
|
||||
|
||||
/** fake-write is locked (missing call), fake-read free, fake-other belongs to a release-per-version profile. */
|
||||
/** @param list<string> $delegated */
|
||||
private function gate(?FakeChangeLog $log = null, array $delegated = []): CapabilityGate
|
||||
{
|
||||
$lock = new Profile(
|
||||
'fake',
|
||||
'Fake Plugin',
|
||||
Tier::LockOnFailure,
|
||||
[FakeCapability::Read, FakeCapability::Write],
|
||||
[new Manifest('Fake', calls: [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])])]
|
||||
);
|
||||
$release = new Profile(
|
||||
'release',
|
||||
'Release',
|
||||
Tier::ReleasePerVersion,
|
||||
[FakeCapability::Other],
|
||||
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allIncompatible(
|
||||
[FakeCapability::Other],
|
||||
'would lock'
|
||||
)
|
||||
);
|
||||
|
||||
return new CapabilityGate(new Compatibility([$lock, $release]), $log, $delegated);
|
||||
}
|
||||
|
||||
public function testAFreeCapabilityPassesAndAWriteIsNotLogged(): void
|
||||
{
|
||||
self::assertNull($this->gate(FakeChangeLog::forbidden())->refusal($this->tool(false, ['fake-read']), []));
|
||||
}
|
||||
|
||||
public function testALockedCapabilityIsRefusedWith423(): void
|
||||
{
|
||||
$error = $this->gate(FakeChangeLog::forbidden())->refusal($this->tool(true, ['fake-read', 'fake-write']), []);
|
||||
|
||||
$payload = self::assertToolError($error, 'compat_incompatible');
|
||||
self::assertSame(['fake-write'], array_column($payload['features'], 'id'));
|
||||
}
|
||||
|
||||
public function testTheIdOfADelegatedProfileIsLeftToItsOwnGate(): void
|
||||
{
|
||||
$gate = $this->gate(FakeChangeLog::forbidden(), ['release']);
|
||||
|
||||
self::assertNull($gate->refusal($this->tool(false, ['fake-other']), []));
|
||||
}
|
||||
|
||||
public function testAReleasePerVersionProfileWithoutDelegationIsLockedByItsMatrix(): void
|
||||
{
|
||||
$payload = self::assertToolError(
|
||||
$this->gate(FakeChangeLog::forbidden())->refusal($this->tool(true, ['fake-other']), []),
|
||||
'compat_incompatible'
|
||||
);
|
||||
|
||||
self::assertSame(['fake-other'], array_column($payload['features'], 'id'));
|
||||
}
|
||||
|
||||
public function testAnUnknownIdLocksTheCall(): void
|
||||
{
|
||||
$payload = self::assertToolError(
|
||||
$this->gate(FakeChangeLog::forbidden())->refusal($this->tool(true, ['fake-read', 'fake-nope']), []),
|
||||
'compat_incompatible'
|
||||
);
|
||||
|
||||
self::assertSame(['fake-nope'], $payload['unknown']);
|
||||
}
|
||||
|
||||
public function testAnUnknownIdOfAWriteIsRefusedWith423AndLogged(): void
|
||||
{
|
||||
$log = new FakeChangeLog();
|
||||
|
||||
$error = $this->gate($log)->refusal($this->tool(false, ['fake-read', 'something-else']), []);
|
||||
|
||||
self::assertSame(423, $error->get_error_data()['status'] ?? null);
|
||||
$log->assertRecorded(
|
||||
['ability' => 'lit/fake', 'object_type' => 'compat', 'object_id' => 'something-else', 'result' => 'refused']
|
||||
);
|
||||
self::assertStringContainsString('unbekannte Fähigkeit something-else', $log->summaries()[0]);
|
||||
}
|
||||
|
||||
public function testARefusedWriteIsLoggedWithTheProfileId(): void
|
||||
{
|
||||
$log = new FakeChangeLog();
|
||||
|
||||
$this->gate($log)->refusal($this->tool(false, ['fake-write']), []);
|
||||
|
||||
$log->assertRecorded(
|
||||
['ability' => 'lit/fake', 'object_type' => 'compat', 'object_id' => 'fake', 'result' => 'refused']
|
||||
);
|
||||
self::assertStringContainsString('Fake Plugin', $log->summaries()[0]);
|
||||
}
|
||||
|
||||
public function testARefusedWriteWithAnUnknownIdIsLogged(): void
|
||||
{
|
||||
$log = new FakeChangeLog();
|
||||
|
||||
$this->gate($log)->refusal($this->tool(false, ['fake-nope']), []);
|
||||
|
||||
$log->assertRecorded(['object_type' => 'compat', 'object_id' => 'fake-nope', 'result' => 'refused']);
|
||||
}
|
||||
|
||||
public function testAnAbilityWithoutRequirementsIsUntouched(): void
|
||||
{
|
||||
$gate = $this->gate(FakeChangeLog::forbidden());
|
||||
$tool = $this->tool(false, null);
|
||||
|
||||
self::assertNull($gate->refusal($tool, []));
|
||||
self::assertSame(['ok' => true], $gate->annotate($tool, ['ok' => true]));
|
||||
self::assertSame(['type' => 'object'], $gate->outputSchema($tool, ['type' => 'object']));
|
||||
self::assertNull($gate->outputSchema($tool, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Compatibility;
|
||||
|
||||
use LIT\MCP\Abilities\ToolRefusal;
|
||||
use LIT\MCP\Compatibility\CapabilityLocked;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
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;
|
||||
|
||||
final class CapabilityLockedTest extends TestCase
|
||||
{
|
||||
public function testTheErrorNamesPluginCapabilitiesAndProblems(): void
|
||||
{
|
||||
$profile = new Profile('fake', 'Fake Plugin', Tier::LockOnFailure, [FakeCapability::Write]);
|
||||
$locked = CapabilityLocked::of(
|
||||
[new FeatureStatus(FakeCapability::Write, ReleaseState::Incompatible, ['Fake: x() fehlt'])],
|
||||
[$profile]
|
||||
);
|
||||
|
||||
self::assertInstanceOf(ToolRefusal::class, $locked);
|
||||
self::assertSame('Fake Plugin locked – fake-write refused', $locked->getMessage());
|
||||
$error = $locked->toError();
|
||||
$payload = self::assertToolError($error, 'compat_incompatible');
|
||||
self::assertSame(423, $error->get_error_data()['status']);
|
||||
self::assertStringContainsString('Fake Plugin', $payload['message']);
|
||||
self::assertStringContainsString('Fake fake-write', $payload['message']);
|
||||
self::assertNotSame('', $payload['hint']);
|
||||
self::assertSame(['fake-write'], array_column($payload['features'], 'id'));
|
||||
self::assertSame(['Fake: x() fehlt'], $payload['problems']);
|
||||
}
|
||||
|
||||
public function testALockWithoutLockedCapabilitiesIsAProgrammingError(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
CapabilityLocked::of([], []);
|
||||
}
|
||||
|
||||
public function testUnknownIdsLockTheCall(): void
|
||||
{
|
||||
$locked = CapabilityLocked::unknown(['fake-x', 'fake-y']);
|
||||
|
||||
$error = $locked->toError();
|
||||
$payload = self::assertToolError($error, 'compat_incompatible');
|
||||
self::assertSame(423, $error->get_error_data()['status']);
|
||||
self::assertSame(['fake-x', 'fake-y'], $payload['unknown']);
|
||||
self::assertStringContainsString('fake-x, fake-y', $payload['message']);
|
||||
self::assertSame(['fake-x', 'fake-y'], $locked->unknown);
|
||||
}
|
||||
|
||||
public function testUnknownNeedsAtLeastOneId(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
CapabilityLocked::unknown([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?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\CompatibilityComponent;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Tests\Support\FakeCapability;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use LogicException;
|
||||
|
||||
final class CompatibilityComponentTest extends TestCase
|
||||
{
|
||||
private function profile(bool $active, bool $broken = false): Profile
|
||||
{
|
||||
return new Profile(
|
||||
'fake',
|
||||
'Fake Plugin',
|
||||
Tier::LockOnFailure,
|
||||
[FakeCapability::Read, FakeCapability::Write],
|
||||
[new Manifest(
|
||||
'Fake',
|
||||
calls: $broken ? [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])] : []
|
||||
)],
|
||||
static fn (): bool => $active,
|
||||
static fn (): string => '4.2.0'
|
||||
);
|
||||
}
|
||||
|
||||
private function component(Profile $profile): EnvironmentComponent
|
||||
{
|
||||
return new CompatibilityComponent($profile, static fn (): Compatibility => new Compatibility([$profile]));
|
||||
}
|
||||
|
||||
public function testAnInactivePluginIsReportedAsNotInstalledWithoutProblems(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['id' => 'fake', 'active' => false, 'version' => null, 'problems' => [], 'release' => null],
|
||||
$this->component($this->profile(false))->describe()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnActiveWorkingPluginHasItsVersionAndNoProblems(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['id' => 'fake', 'active' => true, 'version' => '4.2.0', 'problems' => [], 'release' => null],
|
||||
$this->component($this->profile(true))->describe()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnActiveButBrokenPluginListsItsProblems(): void
|
||||
{
|
||||
$described = $this->component($this->profile(true, broken: true))->describe();
|
||||
|
||||
self::assertTrue($described['active']);
|
||||
self::assertSame('4.2.0', $described['version']);
|
||||
self::assertNotSame([], $described['problems']);
|
||||
self::assertContainsOnly('string', $described['problems']);
|
||||
self::assertNull($described['release']);
|
||||
}
|
||||
|
||||
public function testTheCompatibilityIsAskedOnlyWhenDescribing(): void
|
||||
{
|
||||
$asked = 0;
|
||||
$profile = $this->profile(true);
|
||||
$component = new CompatibilityComponent($profile, static function () use (&$asked, $profile): Compatibility {
|
||||
$asked++;
|
||||
|
||||
return new Compatibility([$profile]);
|
||||
});
|
||||
|
||||
self::assertSame(0, $asked);
|
||||
$component->describe();
|
||||
self::assertSame(1, $asked);
|
||||
}
|
||||
|
||||
public function testAReleasePerVersionProfileHasItsOwnComponent(): void
|
||||
{
|
||||
$profile = new Profile(
|
||||
'rel',
|
||||
'Rel',
|
||||
Tier::ReleasePerVersion,
|
||||
[FakeCapability::Other],
|
||||
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Other])
|
||||
);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
new CompatibilityComponent($profile, static fn (): Compatibility => new Compatibility([$profile]));
|
||||
}
|
||||
|
||||
public function testForProfilesBuildsAComponentPerProfileWithoutReleasePerVersion(): void
|
||||
{
|
||||
$plain = $this->profile(true);
|
||||
$release = new Profile(
|
||||
'rel',
|
||||
'Rel',
|
||||
Tier::ReleasePerVersion,
|
||||
[FakeCapability::Other],
|
||||
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Other])
|
||||
);
|
||||
|
||||
$components = CompatibilityComponent::forProfiles(new Compatibility([$release, $plain]));
|
||||
|
||||
self::assertSame(['fake'], array_map(static fn (CompatibilityComponent $c): string => $c->describe()['id'], $components));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Compatibility;
|
||||
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Tests\Support\FakeCapability;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use LogicException;
|
||||
|
||||
final class ProfileTest extends TestCase
|
||||
{
|
||||
public function testAProfileKeepsWhatItWasGiven(): void
|
||||
{
|
||||
$matrix = static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Read]);
|
||||
$profile = new Profile(
|
||||
'fake',
|
||||
'Fake Plugin',
|
||||
Tier::ReleasePerVersion,
|
||||
[FakeCapability::Read],
|
||||
matrix: $matrix,
|
||||
minVersion: '2.0'
|
||||
);
|
||||
|
||||
self::assertSame('fake', $profile->id);
|
||||
self::assertSame('Fake Plugin', $profile->label);
|
||||
self::assertSame(Tier::ReleasePerVersion, $profile->tier);
|
||||
self::assertSame([FakeCapability::Read], $profile->capabilities);
|
||||
self::assertSame($matrix, $profile->matrix);
|
||||
self::assertSame('2.0', $profile->minVersion);
|
||||
self::assertSame([], $profile->manifests);
|
||||
}
|
||||
|
||||
public function testAReleasePerVersionProfileWithoutMatrixIsAProgrammingError(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage('needs a matrix closure');
|
||||
|
||||
new Profile('fake', 'Fake', Tier::ReleasePerVersion, [FakeCapability::Read]);
|
||||
}
|
||||
|
||||
public function testCapabilityIdsMustBeUniqueWithinAProfile(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage('fake-read');
|
||||
|
||||
new Profile('fake', 'Fake', Tier::ReportOnly, [FakeCapability::Read, FakeCapability::Read]);
|
||||
}
|
||||
|
||||
public function testTheTiersHaveStableValues(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['report', 'lock', 'release'],
|
||||
array_map(static fn (Tier $t): string => $t->value, Tier::cases())
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user