feat(admin): Prüfroute je Profil und Reiter „Compatibility“
Die generische CheckRoute (POST lit-mcp/v1/<id>-check, für Divi weiter /divi-check) ersetzt DiviCheckRoute: Lock je Profil mit Besitzer-Token, Bericht je Profil, Rücksprung auf den neuen Reiter und Hook lit_mcp_compat_changed (Divi lädt darauf seinen Freigabe-Status neu). Der Reiter „Compatibility“ zeigt je Plugin Zustand (Wort und Icon), Hinweis zur Stufe, Fähigkeiten, getestete und freigegebene Versionen und den letzten Bericht, mit Prüfen, Freigeben und Zurückziehen (Recht manage_options, Allowlist der Profil-ID, Nonce je Profil). Freigeben serverseitig nur mit frischem grünem Bericht. Der Load-Hook lit_mcp_compat_profile_load bereitet den Abbau des DiviTab vor, der bis dahin parallel läuft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7d6a97a54e
commit
85f4138b36
@@ -20,7 +20,6 @@ use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
@@ -325,9 +324,12 @@ final class DiviIntegrationTest extends TestCase
|
||||
$upgraded = $callback;
|
||||
}
|
||||
);
|
||||
Actions\expectAdded('rest_api_init')->once()->with(Mockery::on(
|
||||
static fn (array $callback): bool => $callback[0] instanceof DiviCheckRoute
|
||||
));
|
||||
$changed = null;
|
||||
Actions\expectAdded('lit_mcp_compat_changed')->once()->whenHappen(
|
||||
static function (callable $callback) use (&$changed): void {
|
||||
$changed = $callback;
|
||||
}
|
||||
);
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once()->whenHappen(
|
||||
static function (callable $callback) use (&$tabs): void {
|
||||
$tabs = $callback;
|
||||
@@ -339,6 +341,10 @@ final class DiviIntegrationTest extends TestCase
|
||||
(new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->boot();
|
||||
self::assertIsCallable($upgraded);
|
||||
$upgraded();
|
||||
// The route announces a finished check; only Divi's own profile resets the release gate (once, no mirrors).
|
||||
self::assertIsCallable($changed);
|
||||
$changed('pods');
|
||||
$changed('divi');
|
||||
self::assertIsCallable($tabs);
|
||||
$all = $tabs([$base]);
|
||||
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
|
||||
final class DiviCheckRouteTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.de/wp-admin/' . $path);
|
||||
Functions\when('add_query_arg')->alias(
|
||||
static fn (array $args, string $url): string => $url . '?' . http_build_query($args)
|
||||
);
|
||||
Functions\when('get_current_user_id')->justReturn(1);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($GLOBALS['wpdb']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/** Mock of the global $wpdb, its INSERT IGNORE returning $inserted rows affected. */
|
||||
private static function wpdb(int $inserted): \wpdb
|
||||
{
|
||||
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||||
$db->expects('query')->once()->with(Mockery::on(static function (string $sql): bool {
|
||||
return str_starts_with(
|
||||
$sql,
|
||||
"INSERT IGNORE INTO wp_options (option_name, option_value, autoload) "
|
||||
. "VALUES ('lit_mcp_divi_check_lock', '"
|
||||
) && str_ends_with($sql, "', 'no')");
|
||||
}))->andReturn($inserted);
|
||||
$GLOBALS['wpdb'] = $db;
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
public function testRegistersAPostRouteForAdministratorsOnly(): void
|
||||
{
|
||||
Functions\expect('register_rest_route')->once()->with(
|
||||
'lit-mcp/v1',
|
||||
'/divi-check',
|
||||
Mockery::on(static function (array $args): bool {
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'manage_options');
|
||||
$allowed = ($args['permission_callback'])();
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
return $args['methods'] === 'POST' && $allowed === true && ($args['permission_callback'])() === false;
|
||||
})
|
||||
);
|
||||
|
||||
(new DiviCheckRoute(static fn () => null))->registerRoute();
|
||||
}
|
||||
|
||||
public function testRunsTheCheckStoresTheReportAndRedirectsToTheTab(): void
|
||||
{
|
||||
self::wpdb(1);
|
||||
$report = new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', []);
|
||||
$check = Mockery::mock(CompatibilityCheck::class);
|
||||
$check->expects('run')->with(1)->andReturn($report);
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('save')->with($report);
|
||||
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
||||
|
||||
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
||||
|
||||
self::assertSame(303, $response->get_status());
|
||||
self::assertStringContainsString('tab=divi', $response->get_headers()['Location']);
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_checked', $response->get_headers()['Location']);
|
||||
}
|
||||
|
||||
public function testASecondCheckWaitsForTheFirst(): void
|
||||
{
|
||||
$db = self::wpdb(0);
|
||||
$db->expects('get_var')->once()->andReturn((string) time());
|
||||
Functions\expect('delete_option')->never();
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('save')->never();
|
||||
|
||||
$response = (new DiviCheckRoute(static fn () => self::fail('no second check'), $reports))->handle();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_check_running', $response->get_headers()['Location']);
|
||||
}
|
||||
|
||||
public function testAStaleLockIsTakenOver(): void
|
||||
{
|
||||
$db = self::wpdb(0);
|
||||
$db->expects('get_var')->once()->andReturn('1');
|
||||
$db->expects('query')->once()->with(Mockery::on(static function (string $sql): bool {
|
||||
return str_starts_with($sql, "UPDATE wp_options SET option_value = '")
|
||||
&& str_ends_with(
|
||||
$sql,
|
||||
"' WHERE option_name = 'lit_mcp_divi_check_lock' AND option_value = '1'"
|
||||
);
|
||||
}))->andReturn(1);
|
||||
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
||||
$check = Mockery::mock(CompatibilityCheck::class);
|
||||
$check->expects('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', []));
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('save');
|
||||
|
||||
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_checked', $response->get_headers()['Location']);
|
||||
}
|
||||
|
||||
public function testATakeoverLostToAnotherRequestWaitsForIt(): void
|
||||
{
|
||||
$db = self::wpdb(0);
|
||||
$db->expects('get_var')->once()->andReturn('1');
|
||||
$db->expects('query')->once()->with(Mockery::on(static fn (string $sql): bool => str_starts_with(
|
||||
$sql,
|
||||
"UPDATE wp_options SET option_value = '"
|
||||
)))->andReturn(0);
|
||||
Functions\expect('delete_option')->never();
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('save')->never();
|
||||
|
||||
$response = (new DiviCheckRoute(static fn () => self::fail('no second check'), $reports))->handle();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_check_running', $response->get_headers()['Location']);
|
||||
}
|
||||
|
||||
public function testTheLockIsReleasedAndTheCheckMarkedFailedWhenItThrows(): void
|
||||
{
|
||||
self::wpdb(1);
|
||||
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
||||
$check = Mockery::mock(CompatibilityCheck::class);
|
||||
$check->expects('run')->andThrow(new RuntimeException('divi kaputt'));
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('save')->never();
|
||||
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
||||
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||
$previous = ini_set('error_log', $log);
|
||||
|
||||
try {
|
||||
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
||||
} finally {
|
||||
ini_set('error_log', (string) $previous);
|
||||
}
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_check_failed', $response->get_headers()['Location']);
|
||||
self::assertStringContainsString(
|
||||
'divi check failed: RuntimeException: divi kaputt',
|
||||
(string) file_get_contents($log)
|
||||
);
|
||||
unlink($log);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviReleases;
|
||||
@@ -71,5 +73,9 @@ final class DiviProfileTest extends TestCase
|
||||
self::assertSame(DiviReleases::TESTED, $profile->testedVersions);
|
||||
self::assertSame(ReleaseResolver::stateKey('5.3.0'), ($profile->stateKey)('5.3.0'));
|
||||
self::assertInstanceOf(ManifestCheck::class, ($profile->manifestCheck)());
|
||||
// Reports saved before step labels were stored still get their Divi labels.
|
||||
$old = new CheckStep(DiviCheckSteps::WRITE, []);
|
||||
self::assertSame(DiviCheckSteps::label($old), ($profile->stepLabel)($old));
|
||||
self::assertNotSame(DiviCheckSteps::WRITE, ($profile->stepLabel)($old));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user