Behebt die fünf Befunde der Abschlussprüfung von divi-absicherung: Divi-Reiter ohne aktives Divi zeigt nur noch einen Hinweis (kein Status, keine Prüfung, kein Aufräumen), Zustandstexte werden erst beim Rendern übersetzt, DiviPages::setFlags() und DiviThemeBuilder::liveId(true) prüfen jetzt selbst die Freigabesperre, WriteProbe::removeOrphans() löscht nur noch Entwürfe mit passendem Titel, und wp lit-mcp divi-check --update-reference verweigert sich ohne Entwicklungskopie, bevor irgendetwas geschrieben wird. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
173 lines
6.3 KiB
PHP
173 lines
6.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||
|
||
use Brain\Monkey\Functions;
|
||
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\Unit\TestCase;
|
||
use Mockery;
|
||
use RuntimeException;
|
||
use WP_Post;
|
||
|
||
final class WriteProbeTest extends TestCase
|
||
{
|
||
/** @var list<int> */
|
||
private array $deleted = [];
|
||
|
||
protected function setUp(): void
|
||
{
|
||
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'));
|
||
$this->deleted = [];
|
||
Functions\when('wp_insert_post')->justReturn(41);
|
||
Functions\when('get_post')->justReturn(
|
||
new WP_Post(['ID' => 41, 'post_content' => '<!-- wp:divi/section /-->'])
|
||
);
|
||
Functions\when('get_post_meta')->alias(
|
||
static fn (int $id, string $key): string => $key === '_et_pb_show_page_creation' ? 'off' : 'on'
|
||
);
|
||
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
|
||
self::assertTrue($force, 'never to the trash');
|
||
$this->deleted[] = $id;
|
||
|
||
return new WP_Post(['ID' => $id]);
|
||
});
|
||
}
|
||
|
||
private function composer(?callable $write = null): ContentComposer
|
||
{
|
||
$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);
|
||
|
||
return ['warnings' => [], 'outline' => ''];
|
||
};
|
||
$composer->allows('write')->andReturnUsing($write ?? $default);
|
||
|
||
return $composer;
|
||
}
|
||
|
||
private function renderer(string $html): PageRenderer
|
||
{
|
||
$renderer = Mockery::mock(PageRenderer::class);
|
||
$renderer->allows('render')->andReturn($html);
|
||
|
||
return $renderer;
|
||
}
|
||
|
||
public function testGreenWhenEveryModuleRendersAndThePageIsGone(): void
|
||
{
|
||
$probe = new WriteProbe(
|
||
$this->composer(),
|
||
$this->renderer(implode(' ', WriteProbe::TEXTS)),
|
||
'/nonexistent-divi'
|
||
);
|
||
|
||
self::assertSame([], $probe->run());
|
||
self::assertSame([41], $this->deleted);
|
||
}
|
||
|
||
public function testMissingTextsAndFlagsAreReported(): void
|
||
{
|
||
Functions\when('get_post_meta')->justReturn('');
|
||
|
||
$probe = new WriteProbe($this->composer(), $this->renderer('litcheck-heading'), '/nonexistent-divi');
|
||
$problems = $probe->run();
|
||
|
||
self::assertContains('Builder-Kennzeichen _et_pb_use_builder fehlt.', $problems);
|
||
self::assertContains('„litcheck-button“ fehlt in der gerenderten Seite.', $problems);
|
||
self::assertNotContains('„litcheck-heading“ fehlt in der gerenderten Seite.', $problems);
|
||
self::assertSame([41], $this->deleted);
|
||
}
|
||
|
||
public function testDeletesThePageEvenWhenWritingThrows(): void
|
||
{
|
||
$composer = $this->composer(static function (): never {
|
||
throw new RuntimeException('Composer kaputt');
|
||
});
|
||
|
||
$problems = (new WriteProbe($composer, $this->renderer(''), '/nonexistent-divi'))->run();
|
||
|
||
self::assertSame(['Schreibtest abgebrochen: Composer kaputt'], $problems);
|
||
self::assertSame([41], $this->deleted);
|
||
$this->expectException(\LIT\MCP\Integrations\Divi\Release\DiviLocked::class);
|
||
ReleaseGate::assertWritable(41);
|
||
}
|
||
|
||
public function testReportsAPageThatCouldNotBeDeleted(): void
|
||
{
|
||
Functions\when('wp_delete_post')->justReturn(false);
|
||
|
||
$probe = new WriteProbe(
|
||
$this->composer(),
|
||
$this->renderer(implode(' ', WriteProbe::TEXTS)),
|
||
'/nonexistent-divi'
|
||
);
|
||
$problems = $probe->run();
|
||
|
||
self::assertSame(
|
||
['Die Testseite 41 konnte nicht gelöscht werden; bitte unter Seiten endgültig löschen.'],
|
||
$problems
|
||
);
|
||
}
|
||
|
||
public function testCreatesAMarkedDraftPage(): void
|
||
{
|
||
// wp_insert_post is already stubbed in setUp(); Functions\expect() cannot rewire the same
|
||
// function again (Brain Monkey keeps the first redefinition), so the args are captured here.
|
||
$captured = null;
|
||
Functions\when('wp_insert_post')->alias(static function (array $post, bool $wpError) use (&$captured): int {
|
||
$captured = $post;
|
||
|
||
return 41;
|
||
});
|
||
|
||
$probe = new WriteProbe(
|
||
$this->composer(),
|
||
$this->renderer(implode(' ', WriteProbe::TEXTS)),
|
||
'/nonexistent-divi'
|
||
);
|
||
$probe->run();
|
||
|
||
self::assertIsArray($captured);
|
||
self::assertSame('page', $captured['post_type']);
|
||
self::assertSame('draft', $captured['post_status']);
|
||
self::assertTrue(str_starts_with($captured['post_title'], '[LIT MCP Prüfung] '));
|
||
self::assertSame(['_lit_mcp_check_page' => '1'], $captured['meta_input']);
|
||
}
|
||
|
||
public function testRemovesOrphanedCheckPages(): void
|
||
{
|
||
Functions\expect('get_posts')->once()->with(Mockery::on(static fn (array $query): bool =>
|
||
$query['meta_key'] === '_lit_mcp_check_page'
|
||
&& $query['post_status'] === 'draft'
|
||
&& $query['fields'] === 'all'
|
||
&& ($query['date_query'][0]['before'] ?? null) === '10 minutes ago'))->andReturn([
|
||
new WP_Post(['ID' => 51, 'post_title' => WriteProbe::TITLE . ' 2026-01-01 00:00']),
|
||
new WP_Post(['ID' => 52, 'post_title' => WriteProbe::TITLE . ' 2026-01-02 00:00']),
|
||
]);
|
||
|
||
self::assertSame([51, 52], WriteProbe::removeOrphans());
|
||
self::assertSame([51, 52], $this->deleted);
|
||
}
|
||
|
||
public function testKeepsAMarkedDraftWhoseTitleDoesNotMatch(): void
|
||
{
|
||
Functions\when('get_posts')->justReturn([
|
||
new WP_Post(['ID' => 60, 'post_title' => 'Ein anderer Entwurf']),
|
||
]);
|
||
|
||
self::assertSame([], WriteProbe::removeOrphans());
|
||
self::assertSame([], $this->deleted);
|
||
}
|
||
}
|