diff --git a/src/Integrations/Divi/Release/CompatibilityMatrix.php b/src/Integrations/Divi/Release/CompatibilityMatrix.php index 424d658..893a4a7 100644 --- a/src/Integrations/Divi/Release/CompatibilityMatrix.php +++ b/src/Integrations/Divi/Release/CompatibilityMatrix.php @@ -59,15 +59,6 @@ final class CompatibilityMatrix return new self($features); } - /** @return list The features that write (ReleaseGate::assertWritable(), ReleaseStatus::writable()). */ - public static function writeFeatures(): array - { - return array_values(array_filter( - DiviFeature::cases(), - static fn (DiviFeature $feature): bool => $feature->isWrite() - )); - } - public function of(DiviFeature $feature): FeatureStatus { return $this->features[$feature->value]; diff --git a/src/Integrations/Divi/Release/DiviLocked.php b/src/Integrations/Divi/Release/DiviLocked.php index 08540d5..1dead6e 100644 --- a/src/Integrations/Divi/Release/DiviLocked.php +++ b/src/Integrations/Divi/Release/DiviLocked.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace LIT\MCP\Integrations\Divi\Release; use LIT\MCP\Abilities\ToolRefusal; +use LogicException; use RuntimeException; use Throwable; use WP_Error; @@ -13,26 +14,31 @@ defined('ABSPATH') || exit; /** * A Divi call refused because a feature it needs is not released (spec §5.5, D6); AbilityRegistrar returns - * toError(). Without features it is the general write lock of the overall status (texts of spec §5.6). + * toError(). */ final class DiviLocked extends RuntimeException implements ToolRefusal { - /** @param list $features The locked features the refused call needs; [] = writing in general. */ - public function __construct(public readonly ReleaseStatus $status, public readonly array $features = []) + /** + * @param non-empty-list $features The locked features the refused call needs. + * @throws LogicException When $features is empty – a lock always names what it locks. + */ + public function __construct(public readonly ReleaseStatus $status, public readonly array $features) { + if ($features === []) { + throw new LogicException('a Divi lock needs the locked features'); + } + parent::__construct(sprintf( '%s is %s for LIT MCP – %s refused', $status->versionLabel(), $status->state->value, - $features === [] - ? 'write' - : implode(', ', array_map(static fn (DiviFeature $feature): string => $feature->value, $features)) + implode(', ', array_map(static fn (DiviFeature $feature): string => $feature->value, $features)) )); } public function toError(): WP_Error { - return $this->features === [] ? $this->status->refusal() : $this->status->refusalFor($this->features); + return $this->status->refusalFor($this->features); } /** The lock anywhere in $e's chain of previous exceptions (callers wrap failures in WriteFailed). */ diff --git a/src/Integrations/Divi/Release/ReleaseGate.php b/src/Integrations/Divi/Release/ReleaseGate.php index 3fe16fa..715ec62 100644 --- a/src/Integrations/Divi/Release/ReleaseGate.php +++ b/src/Integrations/Divi/Release/ReleaseGate.php @@ -92,16 +92,6 @@ final class ReleaseGate } } - /** - * Every writing feature (CompatibilityMatrix::writeFeatures()) – for callers that know no feature. - * - * @throws DiviLocked - */ - public static function assertWritable(?int $postId = null): void - { - self::assertFeatures(CompatibilityMatrix::writeFeatures(), $postId); - } - public static function whileProbing(int $postId, callable $write): mixed { $previous = self::$probe; diff --git a/src/Integrations/Divi/Release/ReleaseStatus.php b/src/Integrations/Divi/Release/ReleaseStatus.php index c44a5aa..5186db1 100644 --- a/src/Integrations/Divi/Release/ReleaseStatus.php +++ b/src/Integrations/Divi/Release/ReleaseStatus.php @@ -11,8 +11,8 @@ use WP_Error; defined('ABSPATH') || exit; /** - * The release of the installed Divi (spec §5.2): the matrix of its features (E1) and, for callers that know no - * features, the overall state (the worst feature state) with every problem, and the texts of spec §5.6. + * The release of the installed Divi (spec §5.2): the matrix of its features (E1), the overall state (the worst + * feature state) with every problem, and the refusal texts of a call whose features are locked (D6). */ final class ReleaseStatus { @@ -20,19 +20,6 @@ final class ReleaseStatus public const SOURCE_DATABASE = 'database'; public const SOURCE_AUTO = 'auto'; - private const UNTESTED = 'Achtung: %s ist auf dieser Website noch nicht für LIT MCP freigegeben. Dieses ' - . 'Werkzeug braucht Divi-Fähigkeiten, die noch nicht geprüft sind; andere Divi-Werkzeuge laufen weiter. ' - . 'Ein Administrator kann unter Einstellungen → LIT MCP → Divi die Prüfung ausführen und die Version ' - . 'freigeben. Es wurde nichts geändert.'; - private const UNTESTED_HINT = 'Den Nutzer bitten, die Divi-Prüfung auszuführen und die Version freizugeben; ' - . 'bis dahin ein Werkzeug ohne die betroffene Fähigkeit nutzen. lit-site-status zeigt unter components → ' - . 'divi → features, welche Fähigkeiten und Werkzeuge betroffen sind.'; - private const INCOMPATIBLE = '%s hat Stellen geändert, die LIT MCP für dieses Werkzeug nutzt; andere ' - . 'Divi-Werkzeuge laufen weiter. Die betroffenen Werkzeuge bleiben gesperrt, bis LIT MCP aktualisiert ' - . 'wurde. Es wurde nichts geändert.'; - private const INCOMPATIBLE_HINT = 'Den Nutzer informieren, dass LIT MCP aktualisiert werden muss; bis dahin ' - . 'ein Werkzeug ohne die betroffene Fähigkeit nutzen. lit-site-status zeigt unter components → divi → ' - . 'features, welche Fähigkeiten und Werkzeuge betroffen sind.'; private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP ' . 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung ' . 'ausführt und die Version freigibt. Es wurde nichts geändert.'; @@ -74,43 +61,11 @@ final class ReleaseStatus return new self($matrix->state(), $version, $matrix->source(), $matrix->problems(), $release, $matrix); } - public static function released(string $version, string $source = self::SOURCE_CODE): self - { - return new self(ReleaseState::Released, $version, $source); - } - - /** Every writing feature is usable – for callers that know no feature (general write lock of DiviLocked). */ - public function writable(): bool - { - return $this->matrix->usable(CompatibilityMatrix::writeFeatures()); - } - public function versionLabel(): string { return $this->version === '' ? 'Divi (Version unbekannt)' : 'Divi ' . $this->version; } - /** The error of a refused write. @throws LogicException For a released status. */ - public function refusal(): WP_Error - { - return match ($this->state) { - ReleaseState::Untested => Errors::make( - 'divi_untested', - sprintf(self::UNTESTED, $this->versionLabel()), - self::UNTESTED_HINT, - 423 - ), - ReleaseState::Incompatible => Errors::make( - 'divi_incompatible', - sprintf(self::INCOMPATIBLE, $this->versionLabel()), - self::INCOMPATIBLE_HINT, - 423, - ['problems' => $this->problems] - ), - ReleaseState::Released => throw new LogicException('a released Divi refuses nothing'), - }; - } - /** * The error of a call refused because some of $features are locked (D6): divi_incompatible when one of them is * incompatible, else divi_untested; HTTP 423. The message names the features in that state, the hint every diff --git a/tests/Support/ReleaseMatrices.php b/tests/Support/ReleaseMatrices.php index 9225040..98b0cbc 100644 --- a/tests/Support/ReleaseMatrices.php +++ b/tests/Support/ReleaseMatrices.php @@ -10,7 +10,10 @@ use LIT\MCP\Integrations\Divi\Release\FeatureStatus; use LIT\MCP\Integrations\Divi\Release\ReleaseState; use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; -/** Release statuses for tests: every feature released except the named ones. */ +/** + * Release statuses for tests, built the way production builds them (ReleaseStatus::of() with a matrix): every feature + * released except the named ones, all released, or all in one state. + */ final class ReleaseMatrices { /** @param list $locked */ @@ -38,4 +41,27 @@ final class ReleaseMatrices ): ReleaseStatus { return ReleaseStatus::of($version, self::matrix($locked, $state)); } + + /** + * Every feature released from $source (DiviReleases::TESTED = code, ReleaseStore = database, auto). + * + * @param array|null $release The database entry of a SOURCE_DATABASE release. + */ + public static function released( + string $version = '5.2.1', + string $source = ReleaseStatus::SOURCE_CODE, + ?array $release = null + ): ReleaseStatus { + return ReleaseStatus::of($version, CompatibilityMatrix::allReleased($source), $release); + } + + /** + * Every feature in $state with $problems (an unreleased or broken Divi as a whole). + * + * @param list $problems + */ + public static function uniform(ReleaseState $state, string $version = '5.3.0', array $problems = []): ReleaseStatus + { + return ReleaseStatus::of($version, CompatibilityMatrix::uniform($state, $problems)); + } } diff --git a/tests/Unit/Abilities/AbilityRegistrarTest.php b/tests/Unit/Abilities/AbilityRegistrarTest.php index f2128a3..e819c44 100644 --- a/tests/Unit/Abilities/AbilityRegistrarTest.php +++ b/tests/Unit/Abilities/AbilityRegistrarTest.php @@ -17,12 +17,12 @@ use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate; use LIT\MCP\Integrations\Divi\Release\DiviLocked; use LIT\MCP\Integrations\Divi\Release\ReleaseGate; use LIT\MCP\Integrations\Divi\Release\ReleaseState; -use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures; use LIT\MCP\Integrations\Integration; use LIT\MCP\Integrations\IntegrationRegistry; use LIT\MCP\Server\RequestContext; use LIT\MCP\Tests\Support\DiviFeatureFixtures; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -281,7 +281,10 @@ final class AbilityRegistrarTest extends TestCase public function testALockThrownInsideAWriteBecomesTheLockError(): void { - $locked = new DiviLocked(new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['x'])); + $locked = new DiviLocked( + ReleaseMatrices::uniform(ReleaseState::Incompatible, '5.3.0', ['x']), + [DiviFeature::PageWrite] + ); $ability = $this->ability(); $ability->allows('execute')->andThrow(WriteFailed::rolledBack('Nicht gespeichert.', $locked)); @@ -302,7 +305,7 @@ final class AbilityRegistrarTest extends TestCase public function testAPartialWriteStaysPartialEvenWithALockInside(): void { - $locked = new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.3.0')); + $locked = new DiviLocked(ReleaseMatrices::uniform(ReleaseState::Untested, '5.3.0'), [DiviFeature::PageWrite]); $ability = $this->ability(); $ability->allows('execute')->andThrow(WriteFailed::partial('Teilweise gespeichert.', $locked)); diff --git a/tests/Unit/Admin/DiviTabTest.php b/tests/Unit/Admin/DiviTabTest.php index 2f8ff94..3a77424 100644 --- a/tests/Unit/Admin/DiviTabTest.php +++ b/tests/Unit/Admin/DiviTabTest.php @@ -23,6 +23,7 @@ use LIT\MCP\Plugin; use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Support\FeatureFakeTool; use LIT\MCP\Tests\Support\MatrixFixtures; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -197,7 +198,7 @@ final class DiviTabTest extends TestCase public function testUntestedWithAFreshGreenReportOffersTheRelease(): void { - $html = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report()); + $html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report()); self::assertStringContainsString('ungetestet', $html); self::assertStringContainsString('Prüfung starten', $html); @@ -207,9 +208,9 @@ final class DiviTabTest extends TestCase public function testCheckButtonSaysWhatItDoesAndLeadsWhileNothingElseCanBeDone(): void { - $stale = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report(true, '5.2.9')); - $fresh = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report()); - $released = $this->rendered(ReleaseStatus::released('5.3.0'), self::report()); + $stale = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9')); + $fresh = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report()); + $released = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report()); foreach ([$stale, $fresh, $released] as $html) { self::assertStringContainsString('legt eine private Testseite an', $html); @@ -222,7 +223,7 @@ final class DiviTabTest extends TestCase public function testUntestedWithAStaleReportAsksForANewCheck(): void { - $html = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report(true, '5.2.9')); + $html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9')); self::assertStringNotContainsString('Divi 5.3.0 freigeben', $html); self::assertStringContainsString('Prüfung erneut ausführen', $html); @@ -230,7 +231,7 @@ final class DiviTabTest extends TestCase public function testReleasedFromCodeOffersNeitherReleaseNorWithdrawal(): void { - $html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report()); + $html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report()); self::assertStringContainsString('freigegeben', $html); self::assertStringContainsString('im Plugin als getestet hinterlegt', $html); @@ -240,7 +241,7 @@ final class DiviTabTest extends TestCase public function testAnAutomaticReleaseSaysSoAndOffersNeitherReleaseNorWithdrawal(): void { - $html = $this->rendered(ReleaseStatus::released('5.99.0', ReleaseStatus::SOURCE_AUTO), self::report()); + $html = $this->rendered(ReleaseMatrices::released('5.99.0', ReleaseStatus::SOURCE_AUTO), self::report()); self::assertStringContainsString('Automatisch freigegeben', $html); self::assertStringNotContainsString('freigeben', $html); @@ -249,7 +250,7 @@ final class DiviTabTest extends TestCase public function testADatabaseReleaseCanBeWithdrawn(): void { - $status = new ReleaseStatus(ReleaseState::Released, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [ + $status = ReleaseMatrices::released('5.3.0', ReleaseStatus::SOURCE_DATABASE, [ 'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1, ]); @@ -287,7 +288,7 @@ final class DiviTabTest extends TestCase public function testIncompatibleListsTheProblems(): void { $html = $this->rendered( - new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['Theme Builder: Nachbau von x() – y']), + ReleaseMatrices::uniform(ReleaseState::Incompatible, '5.3.0', ['Theme Builder: Nachbau von x() – y']), null ); @@ -316,7 +317,7 @@ final class DiviTabTest extends TestCase $tab = $this->tab(self::report(), Mockery::mock(ReleaseStore::class)); $tab->removeOrphans(); - $html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report(), $tab); + $html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report(), $tab); self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html); } @@ -326,7 +327,7 @@ final class DiviTabTest extends TestCase Functions\expect('get_posts')->never(); Functions\expect('wp_delete_post')->never(); - $html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report()); + $html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report()); self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html); } @@ -367,7 +368,7 @@ final class DiviTabTest extends TestCase new FeatureStatus(DiviFeature::Catalog, ReleaseState::Untested, [], null), ]); - return new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, [], null, matrix: $matrix); + return ReleaseStatus::of('5.3.0', $matrix); } public function testTheFeatureTableShowsStateSourceProblemsAndTools(): void @@ -403,11 +404,11 @@ final class DiviTabTest extends TestCase public function testTheTableNamesEverySourceInWords(): void { - $code = $this->rendered(ReleaseStatus::released('5.3.0'), null); + $code = $this->rendered(ReleaseMatrices::released('5.3.0'), null); $database = $this->rendered( - new ReleaseStatus(ReleaseState::Released, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [ + ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), [ 'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1, - ], matrix: MatrixFixtures::released('database')), + ]), self::report() ); @@ -417,7 +418,7 @@ final class DiviTabTest extends TestCase public function testTheTabExplainsTheAutomationAndTheFilter(): void { - $html = $this->rendered(ReleaseStatus::released('5.3.0'), null); + $html = $this->rendered(ReleaseMatrices::released('5.3.0'), null); self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html); self::assertStringContainsString('lit_mcp_divi_auto_release', $html); @@ -429,11 +430,7 @@ final class DiviTabTest extends TestCase public function testTheTabNoLongerSaysWritingIsLockedAsAWhole(): void { $untested = $this->rendered( - new ReleaseStatus( - ReleaseState::Untested, - '5.3.0', - matrix: MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested) - ), + ReleaseStatus::of('5.3.0', MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested)), null ); $incompatible = $this->rendered(self::mixedStatus(), null); @@ -450,9 +447,9 @@ final class DiviTabTest extends TestCase $matrix = MatrixFixtures::released('database', [ new FeatureStatus(DiviFeature::PageWrite, ReleaseState::Incompatible, ['Seiten: x() fehlt'], null), ]); - $status = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [ + $status = ReleaseStatus::of('5.3.0', $matrix, [ 'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1, - ], matrix: $matrix); + ]); self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report())); } @@ -495,7 +492,7 @@ final class DiviTabTest extends TestCase public function testNoNoticeWhileEveryFeatureIsUsable(): void { - self::assertSame('', $this->notice(ReleaseStatus::released('5.3.0'))); + self::assertSame('', $this->notice(ReleaseMatrices::released('5.3.0'))); } public function testNoNoticeForUsersWithoutManageOptions(): void diff --git a/tests/Unit/Cli/DiviCheckCommandTest.php b/tests/Unit/Cli/DiviCheckCommandTest.php index 2accc5f..27fb17c 100644 --- a/tests/Unit/Cli/DiviCheckCommandTest.php +++ b/tests/Unit/Cli/DiviCheckCommandTest.php @@ -18,6 +18,7 @@ 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\MatrixFixtures; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -354,7 +355,7 @@ final class DiviCheckCommandTest extends TestCase ReleaseGate::useResolver(static function () use (&$order): ReleaseStatus { $order[] = 'resolve'; - return ReleaseStatus::released('5.3.0'); + return ReleaseMatrices::released('5.3.0'); }); $this->command()->run([]); diff --git a/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php b/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php index a19a40c..c0ea7ff 100644 --- a/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php +++ b/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php @@ -374,7 +374,9 @@ final class DesignChangeTest extends TestCase public function testLockedDiviWritesNoSnapshot(): void { - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.16.0')); + ReleaseGate::useResolver( + static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0') + ); $stored = ['v' => 1]; $area = self::area($stored); $snapshots = Mockery::mock(SnapshotStore::class); diff --git a/tests/Unit/Integrations/Divi/Design/DesignSystemWriterTest.php b/tests/Unit/Integrations/Divi/Design/DesignSystemWriterTest.php index 326efcd..259c88d 100644 --- a/tests/Unit/Integrations/Divi/Design/DesignSystemWriterTest.php +++ b/tests/Unit/Integrations/Divi/Design/DesignSystemWriterTest.php @@ -12,6 +12,7 @@ use LIT\MCP\Integrations\Divi\Release\Manifest; use LIT\MCP\Integrations\Divi\Release\ReleaseGate; use LIT\MCP\Integrations\Divi\Release\ReleaseState; use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; final class DesignSystemWriterTest extends TestCase @@ -38,7 +39,9 @@ final class DesignSystemWriterTest extends TestCase public function testWritesNeedAReleasedDivi(): void { - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.16.0')); + ReleaseGate::useResolver( + static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0') + ); $this->expectException(DiviLocked::class); diff --git a/tests/Unit/Integrations/Divi/Design/DiviDesignDetachAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviDesignDetachAbilityTest.php index 196d07e..34fdc4a 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviDesignDetachAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviDesignDetachAbilityTest.php @@ -18,10 +18,11 @@ use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery; use LIT\MCP\Integrations\Divi\Design\SnapshotStore; use LIT\MCP\Integrations\Divi\Library\GlobalUsages; use LIT\MCP\Integrations\Divi\Library\UsageReport; +use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\DiviLocked; use LIT\MCP\Integrations\Divi\Release\ReleaseState; -use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use Mockery\MockInterface; @@ -378,11 +379,14 @@ final class DiviDesignDetachAbilityTest extends TestCase { return [ 'rolled back' => [WriteFailed::rolledBack('Seite 12 nicht gespeichert.'), 'write_failed'], - 'Divi locked' => [new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.16.0')), 'divi_untested'], + 'Divi locked' => [new DiviLocked( + ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0'), + [DiviFeature::PageWrite] + ), 'divi_untested'], 'Divi locked inside a rollback' => [ WriteFailed::rolledBack( 'Seite 12 nicht gespeichert.', - new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.16.0')) + new DiviLocked(ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0'), [DiviFeature::PageWrite]) ), 'divi_untested', ], diff --git a/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php b/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php index f247ce8..1326eba 100644 --- a/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php +++ b/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php @@ -17,11 +17,12 @@ use LIT\MCP\Integrations\Divi\Library\UsageReport; use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\SaveScope; +use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\DiviLocked; use LIT\MCP\Integrations\Divi\Release\ReleaseState; -use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Support\LibraryFixtures; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use Throwable; @@ -456,7 +457,9 @@ final class PageRewriterTest extends TestCase public function testLockedDiviBeforeTheFirstPageIsThrown(): void { $rewriter = $this->rewriter([self::page(101, self::layout(self::BUTTON))]); - $this->failures = [new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.16.0'))]; + $this->failures = [ + new DiviLocked(ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0'), [DiviFeature::PageWrite]), + ]; $this->expectException(DiviLocked::class); diff --git a/tests/Unit/Integrations/Divi/DiviEnvironmentComponentTest.php b/tests/Unit/Integrations/Divi/DiviEnvironmentComponentTest.php index 7be4965..623c1b2 100644 --- a/tests/Unit/Integrations/Divi/DiviEnvironmentComponentTest.php +++ b/tests/Unit/Integrations/Divi/DiviEnvironmentComponentTest.php @@ -15,6 +15,7 @@ use LIT\MCP\Integrations\Divi\Release\ReleaseState; use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Tests\Support\FeatureFakeTool; use LIT\MCP\Tests\Support\MatrixFixtures; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -83,14 +84,7 @@ final class DiviEnvironmentComponentTest extends TestCase 'auto', 'Seiten: Signatur von et_pb_x() geändert' ); - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus( - ReleaseState::Incompatible, - '5.3.0', - null, - [], - null, - matrix: $matrix - )); + ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseStatus::of('5.3.0', $matrix)); $overview = new FeatureOverview(static fn (): array => [ new FeatureFakeTool('lit/divi-outline', true, [DiviFeature::PageRead]), new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]), @@ -119,7 +113,7 @@ final class DiviEnvironmentComponentTest extends TestCase ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus { ++$calls; - return ReleaseStatus::released('5.3.0'); + return ReleaseMatrices::released('5.3.0'); }); $component = new DiviEnvironmentComponent(new DiviCompatibility([])); diff --git a/tests/Unit/Integrations/Divi/Release/CompatibilityMatrixTest.php b/tests/Unit/Integrations/Divi/Release/CompatibilityMatrixTest.php index 75703a2..43508be 100644 --- a/tests/Unit/Integrations/Divi/Release/CompatibilityMatrixTest.php +++ b/tests/Unit/Integrations/Divi/Release/CompatibilityMatrixTest.php @@ -84,16 +84,6 @@ final class CompatibilityMatrixTest extends TestCase ); } - public function testTheWriteFeaturesAreTheWritingCases(): void - { - self::assertSame( - array_values(array_filter(DiviFeature::cases(), static fn (DiviFeature $f): bool => $f->isWrite())), - CompatibilityMatrix::writeFeatures() - ); - self::assertContains(DiviFeature::PageWrite, CompatibilityMatrix::writeFeatures()); - self::assertNotContains(DiviFeature::PageRead, CompatibilityMatrix::writeFeatures()); - } - public function testToArrayListsEveryFeatureInCaseOrder(): void { $rows = self::matrix(['tb-write' => [ReleaseState::Incompatible, ['b']]])->toArray(); diff --git a/tests/Unit/Integrations/Divi/Release/DiviLockedTest.php b/tests/Unit/Integrations/Divi/Release/DiviLockedTest.php index 34887c2..6bbfde0 100644 --- a/tests/Unit/Integrations/Divi/Release/DiviLockedTest.php +++ b/tests/Unit/Integrations/Divi/Release/DiviLockedTest.php @@ -8,9 +8,9 @@ use LIT\MCP\Abilities\ToolRefusal; use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\DiviLocked; use LIT\MCP\Integrations\Divi\Release\ReleaseState; -use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; +use LogicException; final class DiviLockedTest extends TestCase { @@ -26,12 +26,11 @@ final class DiviLockedTest extends TestCase self::assertSame(['preset-write'], array_column($payload['features'], 'id')); } - public function testWithoutFeaturesTheErrorIsTheGeneralWriteLock(): void + public function testALockWithoutFeaturesIsAProgrammingError(): void { - $status = new ReleaseStatus(ReleaseState::Untested, '5.3.0'); - $locked = new DiviLocked($status); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('a Divi lock needs the locked features'); - self::assertSame('Divi 5.3.0 is untested for LIT MCP – write refused', $locked->getMessage()); - self::assertEquals($status->refusal(), $locked->toError()); + new DiviLocked(ReleaseMatrices::uniform(ReleaseState::Untested), []); } } diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseGateTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseGateTest.php index 51739b2..71e7b8c 100644 --- a/tests/Unit/Integrations/Divi/Release/ReleaseGateTest.php +++ b/tests/Unit/Integrations/Divi/Release/ReleaseGateTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release; use LIT\MCP\Abilities\WriteFailed; -use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix; use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\DiviLocked; use LIT\MCP\Integrations\Divi\Release\ReleaseGate; @@ -19,15 +18,13 @@ final class ReleaseGateTest extends TestCase { private function untested(): void { - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0')); + ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested)); } public function testTheTestCaseDefaultReleasesEveryFeature(): void { - self::assertTrue(ReleaseGate::status()->writable()); self::assertSame([], ReleaseGate::status()->matrix->unusable()); - ReleaseGate::assertWritable(7); - ReleaseGate::assertFeatures(DiviFeature::cases()); + ReleaseGate::assertFeatures(DiviFeature::cases(), 7); } public function testFeatureReadsTheMatrixOfThisRequest(): void @@ -74,34 +71,12 @@ final class ReleaseGateTest extends TestCase } } - public function testAssertWritableNeedsEveryWritingFeature(): void - { - ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::status([DiviFeature::Catalog])); - ReleaseGate::assertWritable(); - - ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::status([DiviFeature::LibraryWrite])); - try { - ReleaseGate::assertWritable(); - self::fail('a locked writing feature must refuse'); - } catch (DiviLocked $locked) { - self::assertSame([DiviFeature::LibraryWrite], $locked->features); - } - - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0')); - try { - ReleaseGate::assertWritable(); - self::fail('an untested Divi must lock writes'); - } catch (DiviLocked $locked) { - self::assertSame(CompatibilityMatrix::writeFeatures(), $locked->features); - } - } - - public function testAssertWritableThrowsTheLockWithTheStatus(): void + public function testAssertFeatureThrowsTheLockWithTheStatus(): void { $this->untested(); try { - ReleaseGate::assertWritable(7); + ReleaseGate::assertFeature(DiviFeature::PageWrite, 7); self::fail('an untested Divi must lock writes'); } catch (DiviLocked $locked) { self::assertSame('5.3.0', $locked->status->version); @@ -114,20 +89,24 @@ final class ReleaseGateTest extends TestCase $this->untested(); $result = ReleaseGate::whileProbing(41, static function (): string { - ReleaseGate::assertWritable(41); ReleaseGate::assertFeatures(DiviFeature::cases(), 41); return 'written'; }); self::assertSame('written', $result); - foreach ([static fn () => ReleaseGate::whileProbing(41, static fn () => ReleaseGate::assertWritable(42)), - static fn () => ReleaseGate::whileProbing(41, static fn () => ReleaseGate::assertWritable()), + 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::assertWritable(41)] as $attempt) { + static fn () => ReleaseGate::assertFeature(DiviFeature::PageWrite, 41)] as $attempt) { try { $attempt(); self::fail('only post 41, only while probing'); @@ -149,7 +128,7 @@ final class ReleaseGateTest extends TestCase } $this->expectException(DiviLocked::class); - ReleaseGate::assertWritable(41); + ReleaseGate::assertFeature(DiviFeature::PageWrite, 41); } public function testStatusIsComputedOncePerRequestUntilForgotten(): void @@ -158,7 +137,7 @@ final class ReleaseGateTest extends TestCase ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus { ++$calls; - return ReleaseStatus::released('5.2.1'); + return ReleaseMatrices::released('5.2.1'); }); ReleaseGate::status(); @@ -198,7 +177,7 @@ final class ReleaseGateTest extends TestCase public function testFindsTheLockInsideAWrappedFailure(): void { - $locked = new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.3.0')); + $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)); diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseGuardsTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseGuardsTest.php index 3840c7e..9a3203f 100644 --- a/tests/Unit/Integrations/Divi/Release/ReleaseGuardsTest.php +++ b/tests/Unit/Integrations/Divi/Release/ReleaseGuardsTest.php @@ -145,7 +145,9 @@ final class ReleaseGuardsTest extends TestCase public function testTheCheckPageMayBeWrittenWhileProbing(): void { - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0')); + ReleaseGate::useResolver( + static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.3.0') + ); Functions\when('wp_update_post')->justReturn(41); Functions\when('update_post_meta')->justReturn(true); Functions\when('get_post_meta')->alias( diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php index 59ee1f3..77bd61d 100644 --- a/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php +++ b/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php @@ -124,7 +124,7 @@ final class ReleaseResolverTest extends TestCase ])->resolve(); self::assertSame(self::allBut('released'), self::states($status)); - self::assertTrue($status->writable()); + self::assertSame([], $status->matrix->unusable()); self::assertSame([], $status->problems); } @@ -142,7 +142,7 @@ final class ReleaseResolverTest extends TestCase self::assertSame(['Seiten: et_x()'], $status->matrix->of(DiviFeature::PageWrite)->problems); self::assertSame(ReleaseState::Incompatible, $status->state); self::assertSame(['Seiten: et_x()', 'Katalog: Konstante X'], $status->problems); - self::assertFalse($status->writable()); + self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable()); } public function testAProblemWithoutFeaturesLocksEveryFeature(): void @@ -181,7 +181,7 @@ final class ReleaseResolverTest extends TestCase self::assertSame(self::allBut('released'), self::states($status)); self::assertSame('auto', $status->source); self::assertSame('auto', $status->matrix->of(DiviFeature::PresetWrite)->source); - self::assertTrue($status->writable()); + self::assertSame([], $status->matrix->unusable()); } /** Review focus 3: the automatic release needs every signature unchanged. */ @@ -196,7 +196,7 @@ final class ReleaseResolverTest extends TestCase ['Designsystem: x() – Signatur geändert'], $status->matrix->of(DiviFeature::PresetWrite)->problems ); - self::assertFalse($status->writable()); + self::assertFalse($status->matrix->of(DiviFeature::PresetWrite)->usable()); } public function testAMissingReferenceMakesItsFeatureUntestedUnderAutomaticRelease(): void diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php index 080a7cc..cb04320 100644 --- a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php +++ b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php @@ -15,52 +15,6 @@ use LogicException; final class ReleaseStatusTest extends TestCase { - public function testOnlyAReleasedVersionIsWritable(): void - { - self::assertTrue(ReleaseStatus::released('5.2.1')->writable()); - self::assertFalse((new ReleaseStatus(ReleaseState::Untested, '5.3.0'))->writable()); - self::assertFalse((new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['x']))->writable()); - } - - public function testUntestedRefusalNamesTheVersionAndTheWayOut(): void - { - $payload = self::assertToolError( - (new ReleaseStatus(ReleaseState::Untested, '5.3.0'))->refusal(), - 'divi_untested' - ); - - self::assertSame( - 'Achtung: Divi 5.3.0 ist auf dieser Website noch nicht für LIT MCP freigegeben. Dieses Werkzeug braucht ' - . 'Divi-Fähigkeiten, die noch nicht geprüft sind; andere Divi-Werkzeuge laufen weiter. Ein ' - . 'Administrator kann unter Einstellungen → LIT MCP → Divi die Prüfung ausführen und die Version ' - . 'freigeben. Es wurde nichts geändert.', - $payload['message'] - ); - self::assertStringContainsString('ein Werkzeug ohne die betroffene Fähigkeit nutzen', $payload['hint']); - self::assertStringContainsString('components → divi → features', $payload['hint']); - } - - public function testIncompatibleRefusalListsTheProblems(): void - { - $status = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['Theme Builder: Nachbau von x()']); - $payload = self::assertToolError($status->refusal(), 'divi_incompatible'); - - self::assertSame( - 'Divi 5.3.0 hat Stellen geändert, die LIT MCP für dieses Werkzeug nutzt; andere Divi-Werkzeuge laufen ' - . 'weiter. Die betroffenen Werkzeuge bleiben gesperrt, bis LIT MCP aktualisiert wurde. Es wurde ' - . 'nichts geändert.', - $payload['message'] - ); - self::assertSame(['Theme Builder: Nachbau von x()'], $payload['problems']); - } - - public function testAReleasedStatusHasNoRefusal(): void - { - $this->expectException(LogicException::class); - - ReleaseStatus::released('5.2.1')->refusal(); - } - public function testToArrayForSiteStatus(): void { $status = new ReleaseStatus( @@ -82,7 +36,7 @@ final class ReleaseStatusTest extends TestCase { $untested = new ReleaseStatus(ReleaseState::Untested, '5.3.0'); $broken = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['x']); - $released = ReleaseStatus::released('5.2.1', ReleaseStatus::SOURCE_DATABASE); + $released = ReleaseMatrices::released('5.2.1', ReleaseStatus::SOURCE_DATABASE); foreach (DiviFeature::cases() as $feature) { self::assertSame(ReleaseState::Untested, $untested->matrix->of($feature)->state); @@ -106,17 +60,6 @@ final class ReleaseStatusTest extends TestCase self::assertSame('auto', ReleaseStatus::of('5.99.0', CompatibilityMatrix::allReleased('auto'))->source); } - public function testWritableAsksOnlyTheWritingFeatures(): void - { - $readLocked = ReleaseMatrices::status([DiviFeature::Catalog, DiviFeature::PageRead]); - self::assertSame(ReleaseState::Untested, $readLocked->state); - self::assertTrue($readLocked->writable()); - - foreach (CompatibilityMatrix::writeFeatures() as $feature) { - self::assertFalse(ReleaseMatrices::status([$feature])->writable(), $feature->value); - } - } - public function testAMatrixInAnotherStateThanTheStatusIsRefused(): void { $this->expectException(LogicException::class); diff --git a/tests/Unit/Integrations/Divi/Release/WriteProbeTest.php b/tests/Unit/Integrations/Divi/Release/WriteProbeTest.php index 4f92374..e364b6a 100644 --- a/tests/Unit/Integrations/Divi/Release/WriteProbeTest.php +++ b/tests/Unit/Integrations/Divi/Release/WriteProbeTest.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release; use Brain\Monkey\Functions; +use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\ReleaseGate; use LIT\MCP\Integrations\Divi\Release\ReleaseState; use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; use LIT\MCP\Integrations\Divi\Release\WriteProbe; use LIT\MCP\Integrations\WordPress\Content\ContentComposer; use LIT\MCP\Integrations\WordPress\Render\PageRenderer; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -25,7 +27,7 @@ final class WriteProbeTest extends TestCase { parent::setUp(); // The probe must work while Divi is untested – that is when it runs. - ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0')); + ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested)); $this->deleted = []; Functions\when('wp_insert_post')->justReturn(41); Functions\when('get_post')->justReturn( @@ -47,7 +49,7 @@ final class WriteProbeTest extends TestCase $composer = Mockery::mock(ContentComposer::class); $default = static function (int $id, string $type, array $sections): array { // The composer writes through the saver, which asks the gate: the probe must have opened it for 41. - ReleaseGate::assertWritable($id); + ReleaseGate::assertFeatures(DiviFeature::cases(), $id); return ['warnings' => [], 'outline' => '']; }; @@ -100,7 +102,7 @@ final class WriteProbeTest extends TestCase self::assertSame(['Schreibtest abgebrochen: Composer kaputt'], $problems); self::assertSame([41], $this->deleted); $this->expectException(\LIT\MCP\Integrations\Divi\Release\DiviLocked::class); - ReleaseGate::assertWritable(41); + ReleaseGate::assertFeature(DiviFeature::PageWrite, 41); } public function testReportsAPageThatCouldNotBeDeleted(): void diff --git a/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php b/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php index 57297c4..3088982 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php @@ -11,8 +11,7 @@ use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\Divi\Pages\DiviSaver; -use LIT\MCP\Integrations\Divi\Release\ReleaseState; -use LIT\MCP\Integrations\Divi\Release\ReleaseStatus; +use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\WordPress\Content\ContentComposer; use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility; use LIT\MCP\Integrations\WordPress\Content\NewDraft; @@ -20,6 +19,7 @@ use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Tests\Support\DiviWriterFixture; use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\ReleaseMatrices; use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -228,7 +228,7 @@ final class ContentCreateDiviTest extends TestCase */ public function testLockedDiviRefusesBeforeTheDraftExists(): void { - $refusal = (new ReleaseStatus(ReleaseState::Untested, '5.3.0'))->refusal(); + $refusal = ReleaseMatrices::status([DiviFeature::PageWrite])->refusalFor([DiviFeature::PageWrite]); Functions\expect('wp_insert_post')->never(); $result = $this->ability($this->composer(true, true, $refusal))->execute(self::INPUT);