Files
Jan-Niclas LoosenandClaude Opus 5.5 b511ef6348 refactor(compat): Prüfbausteine nach src/Compatibility, Matrix und ManifestCheck generisch
Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:03:30 +01:00

255 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Tests\Support\DiviFeatureFixtures;
use LIT\MCP\Tests\Support\ReleaseMatrices;
use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException;
final class ReleaseGateTest extends TestCase
{
private function untested(): void
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested));
}
public function testTheTestCaseDefaultReleasesEveryFeature(): void
{
self::assertSame([], ReleaseGate::status()->matrix->unusable());
ReleaseGate::assertFeatures(DiviFeature::cases(), 7);
}
public function testLockOfIsNullForUsableFeaturesAndNamesTheUnusableOnesOtherwise(): void
{
self::assertNull(ReleaseGate::lockOf([]));
self::assertNull(ReleaseGate::lockOf([DiviFeature::PageWrite]));
DiviFeatureFixtures::lock(DiviFeature::Catalog);
$lock = ReleaseGate::lockOf([DiviFeature::PageWrite, DiviFeature::Catalog, DiviFeature::Catalog]);
self::assertNotNull($lock);
self::assertSame([DiviFeature::Catalog], $lock->features);
self::assertSame('5.3.0', $lock->status->version);
}
public function testALockWhoseLockedCapabilityIsNoDiviFeatureStillRefusesTheFeatureAsked(): void
{
$foreign = new class () implements Capability {
public function id(): string
{
return DiviFeature::PageWrite->id();
}
public function label(): string
{
return 'fremde Fähigkeit';
}
public function isWrite(): bool
{
return true;
}
};
ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseStatus::of(
'5.3.0',
CompatibilityMatrix::uniform([$foreign], ReleaseState::Incompatible, ['fremde Fähigkeit'])
));
try {
ReleaseGate::assertFeature(DiviFeature::PageWrite);
self::fail('a locked feature must refuse');
} catch (DiviLocked $locked) {
self::assertSame([DiviFeature::PageWrite], $locked->features);
}
}
public function testAssertFeatureRefusesOnlyALockedFeature(): void
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::status(
[DiviFeature::PresetWrite],
ReleaseState::Incompatible
));
ReleaseGate::assertFeature(DiviFeature::PageWrite);
try {
ReleaseGate::assertFeature(DiviFeature::PresetWrite, 7);
self::fail('a locked feature must refuse');
} catch (DiviLocked $locked) {
self::assertSame([DiviFeature::PresetWrite], $locked->features);
self::assertToolError($locked->toError(), 'divi_incompatible');
}
}
public function testAssertFeaturesNamesEveryLockedFeatureOnce(): void
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::status(
[DiviFeature::LibraryWrite, DiviFeature::ThemeBuilderWrite]
));
try {
ReleaseGate::assertFeatures([
DiviFeature::PageWrite,
DiviFeature::ThemeBuilderWrite,
DiviFeature::LibraryWrite,
DiviFeature::ThemeBuilderWrite,
]);
self::fail('locked features must refuse');
} catch (DiviLocked $locked) {
self::assertSame([DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite], $locked->features);
}
}
public function testAssertFeatureThrowsTheLockWithTheStatus(): void
{
$this->untested();
try {
ReleaseGate::assertFeature(DiviFeature::PageWrite, 7);
self::fail('an untested Divi must lock writes');
} catch (DiviLocked $locked) {
self::assertSame('5.3.0', $locked->status->version);
self::assertToolError($locked->toError(), 'divi_untested');
}
}
public function testProbingAllowsOnlyThatPostAndOnlyMeanwhile(): void
{
$this->untested();
$result = ReleaseGate::whileProbing(41, static function (): string {
ReleaseGate::assertFeatures(DiviFeature::cases(), 41);
return 'written';
});
self::assertSame('written', $result);
foreach ([static fn () => ReleaseGate::whileProbing(41, static fn () => ReleaseGate::assertFeatures(
DiviFeature::cases(),
42
)),
static fn () => ReleaseGate::whileProbing(41, static fn () => ReleaseGate::assertFeatures(
DiviFeature::cases()
)),
static fn () => ReleaseGate::whileProbing(41, static fn () => ReleaseGate::assertFeature(
DiviFeature::PageWrite,
42
)),
static fn () => ReleaseGate::assertFeature(DiviFeature::PageWrite, 41)] as $attempt) {
try {
$attempt();
self::fail('only post 41, only while probing');
} catch (DiviLocked) {
self::addToAssertionCount(1);
}
}
}
public function testProbingEndsEvenWhenTheWriteThrows(): void
{
$this->untested();
try {
ReleaseGate::whileProbing(41, static function (): never {
throw new RuntimeException('boom');
});
} catch (RuntimeException) {
}
$this->expectException(DiviLocked::class);
ReleaseGate::assertFeature(DiviFeature::PageWrite, 41);
}
public function testStatusIsComputedOncePerRequestUntilForgotten(): void
{
$calls = 0;
ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus {
++$calls;
return ReleaseMatrices::released('5.2.1');
});
ReleaseGate::status();
ReleaseGate::status();
self::assertSame(1, $calls);
ReleaseGate::forget();
ReleaseGate::status();
self::assertSame(2, $calls);
}
public function testInvalidateForgetsTheStatusAndClearsTheMirrorCacheOnlyWhenAsked(): void
{
$calls = 0;
$deleted = [];
ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus {
++$calls;
return ReleaseMatrices::released('5.2.1');
});
Functions\when('delete_transient')->alias(static function (string $key) use (&$deleted): bool {
$deleted[] = $key;
return true;
});
ReleaseGate::status();
ReleaseGate::invalidate();
ReleaseGate::status();
self::assertSame(2, $calls);
self::assertSame([], $deleted, 'a release or withdrawal leaves the mirror results alone');
ReleaseGate::invalidate(mirrors: true);
ReleaseGate::status();
self::assertSame(3, $calls);
self::assertSame([ReleaseResolver::CACHE], $deleted);
}
public function testAFailingResolverLocksWrites(): void
{
ReleaseGate::useResolver(static function (): never {
throw new RuntimeException('theme API gone');
});
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$status = ReleaseGate::status();
} finally {
ini_set('error_log', (string) $previous);
}
self::assertSame(ReleaseState::Incompatible, $status->state);
self::assertCount(count(DiviFeature::cases()), $status->matrix->unusable(), 'every feature is locked');
self::assertSame(
['Der Freigabe-Zustand ließ sich nicht ermitteln (Details im PHP-Error-Log).'],
$status->matrix->of(DiviFeature::PageRead)->problems
);
self::assertStringContainsString('theme API gone', (string) file_get_contents($log));
unlink($log);
$this->expectException(DiviLocked::class);
ReleaseGate::assertFeature(DiviFeature::PageRead);
}
public function testFindsTheLockInsideAWrappedFailure(): void
{
$locked = new DiviLocked(ReleaseMatrices::uniform(ReleaseState::Untested, '5.3.0'), [DiviFeature::PageWrite]);
$wrapped = WriteFailed::rolledBack('Nicht gespeichert.', new RuntimeException('draft', 0, $locked));
self::assertSame($locked, DiviLocked::in($wrapped));
self::assertNull(DiviLocked::in(new RuntimeException('other')));
}
}