Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
7.7 KiB
PHP
222 lines
7.7 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\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\ReleaseState;
|
|
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 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')));
|
|
}
|
|
}
|