fix: atomic Divi check lock, failed-check notice, test fixes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 02:45:50 +02:00
co-authored by Claude Opus 5.5
parent 6113e5ad5a
commit b441febd81
5 changed files with 132 additions and 21 deletions
+1
View File
@@ -73,6 +73,7 @@ final class DiviTabTest extends TestCase
{
$reports = [null, self::report(false), self::report(true, '5.2.9'), self::report(true, '5.3.0', 'old')];
foreach ($reports as $report) {
$this->redirect = null;
$releases = Mockery::mock(ReleaseStore::class);
$releases->expects('add')->never();
+1
View File
@@ -179,6 +179,7 @@ final class SettingsPageTest extends TestCase
return [
['divi_checked', 'success', 'Divi-Prüfung abgeschlossen'],
['divi_check_running', 'warning', 'bereits eine Divi-Prüfung'],
['divi_check_failed', 'error', 'PHP-Error-Log'],
['divi_released', 'success', 'für LIT MCP freigegeben'],
['divi_release_refused', 'error', 'Prüfung erneut ausführen'],
['divi_withdrawn', 'success', 'Freigabe zurückgezogen'],
@@ -11,6 +11,7 @@ use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
final class DiviCheckRouteTest extends TestCase
{
@@ -24,6 +25,28 @@ final class DiviCheckRouteTest extends TestCase
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(
@@ -43,15 +66,12 @@ final class DiviCheckRouteTest extends TestCase
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('add_option')
->once()
->with(DiviCheckRoute::LOCK, Mockery::type('int'), '', false)
->andReturn(true);
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
@@ -63,8 +83,8 @@ final class DiviCheckRouteTest extends TestCase
public function testASecondCheckWaitsForTheFirst(): void
{
Functions\when('add_option')->justReturn(false);
Functions\when('get_option')->alias(static fn (): int => time());
$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();
@@ -76,18 +96,66 @@ final class DiviCheckRouteTest extends TestCase
public function testAStaleLockIsTakenOver(): void
{
Functions\when('add_option')->alias(static function (): bool {
static $calls = 0;
return ++$calls > 1;
});
Functions\when('get_option')->justReturn(1);
Functions\expect('delete_option')->twice()->with(DiviCheckRoute::LOCK);
$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');
(new DiviCheckRoute(static fn () => $check, $reports))->handle();
$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);
}
}