feat: admin check steps with throwaway write test and reports
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CheckReportTest extends TestCase
|
||||
{
|
||||
private static function report(array $steps): CheckReport
|
||||
{
|
||||
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '5.3.0', '0.6.0', 'key', $steps);
|
||||
}
|
||||
|
||||
private static function green(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (string $key): CheckStep => new CheckStep($key, []),
|
||||
[CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE]
|
||||
);
|
||||
}
|
||||
|
||||
public function testGreenOnlyWithEveryStepGreenAndNoneSkipped(): void
|
||||
{
|
||||
self::assertTrue(self::report(self::green())->green());
|
||||
self::assertFalse(self::report([])->green());
|
||||
|
||||
$red = self::green();
|
||||
$red[2] = new CheckStep(CheckStep::CATALOG, ['divi/text: unbekanntes Modul']);
|
||||
self::assertFalse(self::report($red)->green());
|
||||
|
||||
$skipped = self::green();
|
||||
$skipped[4] = new CheckStep(CheckStep::WRITE, [], true);
|
||||
self::assertFalse(self::report($skipped)->green());
|
||||
}
|
||||
|
||||
public function testFreshOnlyForTheSameVersionsAndStateKey(): void
|
||||
{
|
||||
$report = self::report(self::green());
|
||||
|
||||
self::assertTrue($report->freshFor('5.3.0', '0.6.0', 'key'));
|
||||
self::assertFalse($report->freshFor('5.3.1', '0.6.0', 'key'), 'Divi updated');
|
||||
self::assertFalse($report->freshFor('5.3.0', '0.7.0', 'key'), 'plugin updated');
|
||||
self::assertFalse($report->freshFor('5.3.0', '0.6.0', 'other'), 'a Divi file changed');
|
||||
}
|
||||
|
||||
public function testRoundTripsThroughAnArray(): void
|
||||
{
|
||||
$steps = self::green();
|
||||
$steps[1] = new CheckStep(CheckStep::MIRRORS, ['Theme Builder: Nachbau von x() – y']);
|
||||
$report = self::report($steps);
|
||||
|
||||
self::assertEquals($report, CheckReport::fromArray($report->toArray()));
|
||||
self::assertNull(CheckReport::fromArray('broken'));
|
||||
self::assertNull(CheckReport::fromArray(['id' => 'r1']));
|
||||
self::assertSame('Fingerabdrücke nachgebauter Divi-Funktionen', $report->step(CheckStep::MIRRORS)?->label());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\FormatCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
use LIT\MCP\Plugin;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
|
||||
final class CompatibilityCheckTest extends TestCase
|
||||
{
|
||||
private function check(
|
||||
array $code,
|
||||
array $mirrors,
|
||||
?WriteProbe $probe = null,
|
||||
?\Closure $catalog = null
|
||||
): CompatibilityCheck {
|
||||
Functions\when('wp_generate_uuid4')->justReturn('uuid-1');
|
||||
$manifests = Mockery::mock(ManifestCheck::class);
|
||||
$manifests->allows('codeChecks')->andReturn($code);
|
||||
$manifests->allows('registrationChecks')->andReturn([]);
|
||||
$manifests->allows('mirrorChecks')->andReturn($mirrors);
|
||||
$format = Mockery::mock(FormatCheck::class);
|
||||
$format->allows('problems')->andReturn([]);
|
||||
if ($probe === null) {
|
||||
$probe = Mockery::mock(WriteProbe::class);
|
||||
$probe->allows('run')->andReturn([]);
|
||||
}
|
||||
|
||||
return new CompatibilityCheck(
|
||||
$manifests,
|
||||
$catalog ?? static fn (): array => [],
|
||||
$format,
|
||||
$probe,
|
||||
static fn (): string => '5.3.0',
|
||||
static fn (string $version): string => 'key-' . $version
|
||||
);
|
||||
}
|
||||
|
||||
public function testAGreenRunNamesVersionsAndStateKey(): void
|
||||
{
|
||||
$report = $this->check([], [])->run(7);
|
||||
|
||||
self::assertTrue($report->green());
|
||||
self::assertSame(['uuid-1', 7, '5.3.0', Plugin::VERSION, 'key-5.3.0'], [
|
||||
$report->id, $report->userId, $report->diviVersion, $report->pluginVersion, $report->stateKey,
|
||||
]);
|
||||
self::assertSame(
|
||||
['manifest', 'mirrors', 'catalog', 'format', 'write'],
|
||||
array_map(static fn (CheckStep $step): string => $step->key, $report->steps)
|
||||
);
|
||||
}
|
||||
|
||||
public function testNoWriteTestWhenDiviFunctionsAreMissing(): void
|
||||
{
|
||||
$probe = Mockery::mock(WriteProbe::class);
|
||||
$probe->expects('run')->never();
|
||||
|
||||
$report = $this->check(['Seiten: et_x()' => static fn (): bool => false], [], $probe)->run(7);
|
||||
|
||||
self::assertSame(['Seiten: et_x()'], $report->step(CheckStep::MANIFEST)?->problems);
|
||||
self::assertTrue($report->step(CheckStep::WRITE)?->skipped);
|
||||
}
|
||||
|
||||
public function testChangedFingerprintsStillRunTheWriteTest(): void
|
||||
{
|
||||
$probe = Mockery::mock(WriteProbe::class);
|
||||
$probe->expects('run')->once()->andReturn([]);
|
||||
|
||||
$report = $this->check([], ['TB: Nachbau von y()' => static fn (): bool => false], $probe)->run(7);
|
||||
|
||||
self::assertFalse($report->green());
|
||||
self::assertTrue($report->step(CheckStep::WRITE)?->green());
|
||||
}
|
||||
|
||||
public function testAThrowingStepIsARedStep(): void
|
||||
{
|
||||
$report = $this->check([], [], null, static function (): never {
|
||||
throw new RuntimeException('Katalog weg');
|
||||
})->run(7);
|
||||
|
||||
self::assertSame(['Prüfschritt abgebrochen: Katalog weg'], $report->step(CheckStep::CATALOG)?->problems);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Release\FormatCheck;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FormatCheckTest extends TestCase
|
||||
{
|
||||
/** parse_blocks() as WordPress would answer for well-formed Divi markup: the same names and nesting. */
|
||||
private static function wordPress(): \Closure
|
||||
{
|
||||
$convert = static function (array $blocks) use (&$convert): array {
|
||||
return array_map(static fn (Block $block): array => [
|
||||
'blockName' => $block->name(),
|
||||
'innerBlocks' => $convert($block->children()),
|
||||
], $blocks);
|
||||
};
|
||||
|
||||
return static fn (string $content): array => $convert((new Parser())->parse($content)->children());
|
||||
}
|
||||
|
||||
public function testTheShippedReferenceRoundTripsAndMatchesWordPress(): void
|
||||
{
|
||||
self::assertSame([], (new FormatCheck(null, self::wordPress()))->problems());
|
||||
self::assertFileEquals(
|
||||
dirname(__DIR__, 5) . '/tests/Fixtures/divi/converted-landing.html',
|
||||
FormatCheck::REFERENCE
|
||||
);
|
||||
}
|
||||
|
||||
public function testADifferentStructureInWordPressIsReported(): void
|
||||
{
|
||||
$check = new FormatCheck(null, static fn (): array => [['blockName' => 'divi/section', 'innerBlocks' => []]]);
|
||||
|
||||
self::assertSame(['WordPress liest eine andere Blockstruktur als LIT MCP.'], $check->problems());
|
||||
}
|
||||
|
||||
public function testAMissingReferenceIsReported(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Der Referenzinhalt der Prüfung fehlt im Plugin.'],
|
||||
(new FormatCheck('/nonexistent/reference.html', self::wordPress()))->problems()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?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'] === 'any'
|
||||
&& $query['fields'] === 'ids'
|
||||
&& ($query['date_query'][0]['before'] ?? null) === '10 minutes ago'))->andReturn([51, 52]);
|
||||
|
||||
self::assertSame([51, 52], WriteProbe::removeOrphans());
|
||||
self::assertSame([51, 52], $this->deleted);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user