From a4faeda326d85b736f778bf241f886006761f33b Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sun, 18 Oct 2026 22:29:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20divi-check=20gibt=20die=20Matrix=20je?= =?UTF-8?q?=20F=C3=A4higkeit=20aus=20(Exit=202=20bei=20nicht=20nutzbarer?= =?UTF-8?q?=20F=C3=A4higkeit);=20Texte=20nennen=20F=C3=A4higkeiten=20statt?= =?UTF-8?q?=20pauschal=20gesperrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- src/Cli/DiviCheckCommand.php | 81 ++++++++++++- .../Divi/Release/ReleaseStatus.php | 31 ++--- tests/Unit/Cli/DiviCheckCommandTest.php | 109 +++++++++++++++++- .../Divi/Release/ReleaseStatusTest.php | 28 ++--- 4 files changed, 204 insertions(+), 45 deletions(-) diff --git a/src/Cli/DiviCheckCommand.php b/src/Cli/DiviCheckCommand.php index 3fecd35..f0fba57 100644 --- a/src/Cli/DiviCheckCommand.php +++ b/src/Cli/DiviCheckCommand.php @@ -9,6 +9,7 @@ use LIT\MCP\Integrations\Divi\Release\CheckReport; use LIT\MCP\Integrations\Divi\Release\CheckReportStore; use LIT\MCP\Integrations\Divi\Release\CheckStep; use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck; +use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix; use LIT\MCP\Integrations\Divi\Release\DiviAdapters; use LIT\MCP\Integrations\Divi\Release\DiviEnvironment; use LIT\MCP\Integrations\Divi\Release\DiviReference; @@ -22,7 +23,14 @@ defined('ABSPATH') || exit; /** * wp lit-mcp divi-check (spec §7): the admin check from the command line, for a developer checking a new Divi - * version. Never releases anything – a green version goes into DiviReleases::TESTED by hand. + * version. Never releases anything – a green version goes into DiviReleases::TESTED by hand. After the report it + * prints the compatibility matrix (E3). + * + * Exit codes: 0 = report green and every feature usable; 1 = report red (or the check could not run, or + * --update-reference refused); 2 = report green but a feature is not usable. The 2 is for a version that is + * checked but not yet released (not in DiviReleases::TESTED, automatic release off or a reference missing): the + * checks passed, yet LIT MCP would still refuse the affected tools. A script that only cares about "usable" tests + * for ≠ 0. --update-reference keeps its own code: it runs before the new reference is part of any release. */ final class DiviCheckCommand { @@ -37,6 +45,7 @@ final class DiviCheckCommand * @param (Closure(): ?string)|null $sourceDir null = DiviReference::sourceDir(). * @param (Closure(): array)|null $signatures "sig:" => sha256 of every Divi call in the * running process; null = DiviReference::currentSignatures(). + * @param (Closure(): CompatibilityMatrix)|null $matrix null = the release gate's matrix, computed again. */ public function __construct( private Closure $check, @@ -45,7 +54,8 @@ final class DiviCheckCommand private ?Closure $themeDir = null, private ?string $expectedFile = null, private ?Closure $sourceDir = null, - private ?Closure $signatures = null + private ?Closure $signatures = null, + private ?Closure $matrix = null ) { $this->out = $out ?? static function (string $line): void { \WP_CLI::line($line); @@ -62,7 +72,9 @@ final class DiviCheckCommand /** * Prüft die installierte Divi-Version wie der Reiter „Divi“: Manifeste, Fingerabdrücke, Modulkatalog, Format - * und Schreibtest auf einer Wegwerfseite. Gibt nichts frei. + * und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei. + * Exit-Code 0: Prüfung grün und alle Fähigkeiten nutzbar; 1: Prüfung rot; 2: Prüfung grün, aber mindestens + * eine Fähigkeit nicht nutzbar (z. B. Version noch nicht in DiviReleases::TESTED). * * ## OPTIONS * @@ -111,10 +123,69 @@ final class DiviCheckCommand $this->diff(); } if (isset($assoc['update-reference'])) { - return $this->updateReference($report); + $code = $this->updateReference($report); + $this->printMatrix(); + + return $code; } - return $report->green() ? 0 : 1; + $unusable = $this->printMatrix(); + if (! $report->green()) { + return 1; + } + + return $unusable > 0 ? 2 : 0; + } + + /** + * Prints one line per feature (✓ released, ✗ incompatible, – untested) with label and state, the problems + * indented below. + * + * @return int The number of features that are not usable. + */ + private function printMatrix(): int + { + $matrix = $this->currentMatrix(); + ($this->out)('Fähigkeiten:'); + foreach ($matrix->toArray() as $row) { + ($this->out)(sprintf( + '%s %s – %s', + match ($row['state']) { + 'released' => '✓', + 'incompatible' => '✗', + default => '–', + }, + $row['label'], + self::stateLabel($row['state'], $row['source']) + )); + foreach ($row['problems'] as $problem) { + ($this->out)(' ' . $problem); + } + } + + return count($matrix->unusable()); + } + + /** The check just ran, so the state is computed again instead of read from a cache older than the report. */ + private function currentMatrix(): CompatibilityMatrix + { + if ($this->matrix !== null) { + return ($this->matrix)(); + } + + delete_transient(ReleaseResolver::CACHE); + ReleaseGate::forget(); + + return ReleaseGate::status()->matrix; + } + + private static function stateLabel(string $state, ?string $source): string + { + return match ($state) { + 'released' => 'freigegeben (' . ($source ?? '?') . ')', + 'incompatible' => 'inkompatibel', + default => 'ungetestet', + }; } private function diff(): void diff --git a/src/Integrations/Divi/Release/ReleaseStatus.php b/src/Integrations/Divi/Release/ReleaseStatus.php index 15903f5..c44a5aa 100644 --- a/src/Integrations/Divi/Release/ReleaseStatus.php +++ b/src/Integrations/Divi/Release/ReleaseStatus.php @@ -20,16 +20,19 @@ 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. Schreibende ' - . 'Divi-Werkzeuge sind gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung ' - . 'ausführt und die Version freigibt. Es wurde nichts geändert.'; + 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; ' - . 'lesende Divi-Werkzeuge funktionieren weiter.'; - private const INCOMPATIBLE = '%s hat Stellen geändert, die LIT MCP nutzt. Schreibende Divi-Werkzeuge sind ' - . 'gesperrt, bis LIT MCP aktualisiert wurde. Es wurde nichts geändert.'; - private const INCOMPATIBLE_HINT = 'Den Nutzer informieren, dass LIT MCP aktualisiert werden muss; lesende ' - . 'Divi-Werkzeuge funktionieren weiter.'; - private const NOTICE = 'Achtung: %s ist für LIT MCP %s; Lesen funktioniert, Schreiben ist gesperrt.'; + . '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.'; @@ -158,16 +161,6 @@ final class ReleaseStatus ); } - /** The warning a reading Divi tool carries while writes are locked. */ - public function notice(): string - { - return sprintf( - self::NOTICE, - $this->versionLabel(), - $this->state === ReleaseState::Incompatible ? 'inkompatibel' : 'ungetestet' - ); - } - /** * "„Seiten schreiben“", "„A“ und „B“", "„A“, „B“ und „C“". * diff --git a/tests/Unit/Cli/DiviCheckCommandTest.php b/tests/Unit/Cli/DiviCheckCommandTest.php index d88a56d..2accc5f 100644 --- a/tests/Unit/Cli/DiviCheckCommandTest.php +++ b/tests/Unit/Cli/DiviCheckCommandTest.php @@ -10,7 +10,14 @@ use LIT\MCP\Integrations\Divi\Release\CheckReport; use LIT\MCP\Integrations\Divi\Release\CheckReportStore; use LIT\MCP\Integrations\Divi\Release\CheckStep; use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck; +use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix; +use LIT\MCP\Integrations\Divi\Release\DiviFeature; use LIT\MCP\Integrations\Divi\Release\DiviReference; +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\MatrixFixtures; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -61,7 +68,8 @@ final class DiviCheckCommandTest extends TestCase private function command( array $problems = [], bool $expectSave = false, - bool $noSourceDir = false + bool $noSourceDir = false, + ?CompatibilityMatrix $matrix = null ): DiviCheckCommand { $steps = array_map( static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []), @@ -91,7 +99,8 @@ final class DiviCheckCommandTest extends TestCase } return $this->signatures; - } + }, + $matrix === null ? null : static fn (): CompatibilityMatrix => $matrix ); } @@ -256,4 +265,100 @@ final class DiviCheckCommandTest extends TestCase file_put_contents($file, "command()->run([])); + + $schreibtest = array_search('✓ Schreibtest auf einer Wegwerfseite', $this->lines, true); + $header = array_search('Fähigkeiten:', $this->lines, true); + self::assertIsInt($schreibtest); + self::assertIsInt($header); + self::assertGreaterThan($schreibtest, $header); + self::assertContains('✓ Seiten schreiben – freigegeben (code)', $this->lines); + self::assertContains('✓ Theme Builder lesen – freigegeben (code)', $this->lines); + } + + public function testEveryFeatureGetsOneLine(): void + { + $this->command()->run([]); + + $matrixLines = array_filter( + $this->lines, + static fn (string $line): bool => str_contains($line, ' – freigegeben') + ); + self::assertCount(count(DiviFeature::cases()), $matrixLines); + } + + public function testAnIncompatibleFeatureFailsWithItsProblemsIndented(): void + { + $matrix = MatrixFixtures::with( + DiviFeature::PageWrite, + ReleaseState::Incompatible, + 'auto', + 'Seiten: et_pb_x() fehlt' + ); + + $code = $this->command(matrix: $matrix)->run([]); + + self::assertSame(2, $code); + self::assertContains('✗ Seiten schreiben – inkompatibel', $this->lines); + self::assertContains(' Seiten: et_pb_x() fehlt', $this->lines); + self::assertContains('✓ Seiten lesen – freigegeben (auto)', $this->lines); + } + + public function testAnUntestedFeatureFailsWithExitTwoAlthoughTheReportIsGreen(): void + { + $matrix = MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested, 'auto'); + + $code = $this->command(expectSave: true, matrix: $matrix)->run([]); + + self::assertSame(2, $code); + self::assertContains('– Modulkatalog – ungetestet', $this->lines); + } + + public function testARedReportKeepsExitOneWhateverTheMatrixSays(): void + { + $red = [CheckStep::CATALOG => ['divi/text: unbekanntes Modul']]; + $incompatible = MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Incompatible, 'auto', 'x'); + + self::assertSame(1, $this->command($red)->run([])); + self::assertSame(1, $this->command($red, matrix: $incompatible)->run([])); + } + + public function testUpdateReferenceKeepsItsOwnExitCodeAndStillPrintsTheMatrix(): void + { + $this->writeDivi(); + $matrix = MatrixFixtures::with( + DiviFeature::PageWrite, + ReleaseState::Incompatible, + 'auto', + 'Seiten: Nachbau geändert' + ); + + $code = $this->command([CheckStep::MIRRORS => ['geändert']], matrix: $matrix) + ->run(['update-reference' => true]); + + self::assertSame(0, $code); + self::assertContains('✗ Seiten schreiben – inkompatibel', $this->lines); + } + + public function testWithoutAnInjectedMatrixTheStateIsComputedAfterTheCheck(): void + { + $order = []; + Functions\when('delete_transient')->alias(static function (string $key) use (&$order): bool { + $order[] = 'delete:' . $key; + + return true; + }); + ReleaseGate::useResolver(static function () use (&$order): ReleaseStatus { + $order[] = 'resolve'; + + return ReleaseStatus::released('5.3.0'); + }); + + $this->command()->run([]); + + self::assertSame(['delete:' . ReleaseResolver::CACHE, 'resolve'], $order); + } } diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php index 3314b0f..080a7cc 100644 --- a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php +++ b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php @@ -30,12 +30,14 @@ final class ReleaseStatusTest extends TestCase ); self::assertSame( - 'Achtung: Divi 5.3.0 ist auf dieser Website noch nicht für LIT MCP freigegeben. Schreibende ' - . 'Divi-Werkzeuge sind gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die ' - . 'Prüfung ausführt und die Version freigibt. Es wurde nichts geändert.', + '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('lesende Divi-Werkzeuge funktionieren weiter', $payload['hint']); + self::assertStringContainsString('ein Werkzeug ohne die betroffene Fähigkeit nutzen', $payload['hint']); + self::assertStringContainsString('components → divi → features', $payload['hint']); } public function testIncompatibleRefusalListsTheProblems(): void @@ -44,26 +46,14 @@ final class ReleaseStatusTest extends TestCase $payload = self::assertToolError($status->refusal(), 'divi_incompatible'); self::assertSame( - 'Divi 5.3.0 hat Stellen geändert, die LIT MCP nutzt. Schreibende Divi-Werkzeuge sind gesperrt, bis ' - . 'LIT MCP aktualisiert wurde. Es wurde nichts geändert.', + '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 testNoticesForReadingTools(): void - { - self::assertSame( - 'Achtung: Divi 5.3.0 ist für LIT MCP ungetestet; Lesen funktioniert, Schreiben ist gesperrt.', - (new ReleaseStatus(ReleaseState::Untested, '5.3.0'))->notice() - ); - self::assertSame( - 'Achtung: Divi (Version unbekannt) ist für LIT MCP inkompatibel; Lesen funktioniert, Schreiben ist ' - . 'gesperrt.', - (new ReleaseStatus(ReleaseState::Incompatible, ''))->notice() - ); - } - public function testAReleasedStatusHasNoRefusal(): void { $this->expectException(LogicException::class);