fix(compat): Bereinigung liegengebliebener Prüf-Einträge löschte laufende Prüfungen
Entwürfe haben in WordPress kein gültiges GMT-Datum (0000-00-00), deshalb hielt die Orphan-Bereinigung jeden markierten Prüf-Eintrag für alt und konnte eine laufende Divi-Prüfung abbrechen. Der neue Kern-Baustein ThrowawayPost setzt beim Anlegen ein echtes GMT-Datum, prüft das Alter über post_modified_gmt und kapselt Anlegen, Prüfen und Löschen (Anlegen jetzt innerhalb von try). WriteProbe nutzt ihn; Fehlermeldungen von WordPress gehen nur noch ins Error-Log. Live in DDEV belegt: frischer Eintrag bleibt, 11 Minuten alter wird gelöscht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
239d35c198
commit
42aa4f8dc9
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Compatibility;
|
||||||
|
|
||||||
|
use LIT\MCP\Log;
|
||||||
|
use Throwable;
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The life cycle of a check probe's throwaway entry: a marked draft is created, exercised and always deleted for
|
||||||
|
* good (finally, not to the trash, with its revisions and meta). Entries a dead check left behind carry the marker;
|
||||||
|
* removeOrphans() deletes them. Nothing variable (exception or WP_Error text, read values) enters a report – it
|
||||||
|
* reaches MCP clients through the lock reason – the details go to the PHP error log.
|
||||||
|
*
|
||||||
|
* Creating and deleting fire the hooks of every other plugin on the site (save_post, delete_post …); that cannot be
|
||||||
|
* avoided with a real wp_insert_post()/wp_delete_post().
|
||||||
|
*/
|
||||||
|
final class ThrowawayPost
|
||||||
|
{
|
||||||
|
/** Only entries not modified for this long are orphans – a running check's entry is younger. */
|
||||||
|
private const ORPHAN_AGE_SECONDS = 600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $marker Meta key of the entries (only a query hint, the title prefix decides).
|
||||||
|
* @param string $titlePrefix Title of the entries starts with it.
|
||||||
|
* @param string $abortedText Report text when the exercise (or the creation) threw.
|
||||||
|
* @param string $createFailedText Report text when the entry could not be created.
|
||||||
|
* @param string $deleteFailedFormat sprintf format with %d for the ID of an entry that could not be deleted.
|
||||||
|
* @param string $logContext Context of the log lines.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $marker,
|
||||||
|
private string $titlePrefix,
|
||||||
|
private string $abortedText,
|
||||||
|
private string $createFailedText,
|
||||||
|
private string $deleteFailedFormat,
|
||||||
|
private string $logContext
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(int): list<string> $exercise Gets the ID of the draft, returns the problems found.
|
||||||
|
* @return list<string> Fixed German problem texts; [] = green.
|
||||||
|
*/
|
||||||
|
public function run(string $postType, callable $exercise): array
|
||||||
|
{
|
||||||
|
$id = 0;
|
||||||
|
$problems = [];
|
||||||
|
try {
|
||||||
|
$id = $this->create($postType);
|
||||||
|
$problems = $id > 0 ? $exercise($id) : [$this->createFailedText];
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::failure($this->logContext, $e);
|
||||||
|
$problems = [$this->abortedText];
|
||||||
|
} finally {
|
||||||
|
if ($id > 0 && ! self::delete($id)) {
|
||||||
|
$problems[] = sprintf($this->deleteFailedFormat, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $problems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string>|string $postTypes
|
||||||
|
* @return list<int> IDs of entries a dead check left behind, deleted now. Only drafts whose title starts with
|
||||||
|
* the prefix are considered.
|
||||||
|
*/
|
||||||
|
public function removeOrphans(array|string $postTypes): array
|
||||||
|
{
|
||||||
|
$posts = get_posts([
|
||||||
|
'post_type' => $postTypes,
|
||||||
|
'post_status' => 'draft',
|
||||||
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a handful of marked entries at most.
|
||||||
|
'meta_key' => $this->marker,
|
||||||
|
// post_modified_gmt, not post_date_gmt: an edit by the check keeps it current. A literal GMT timestamp,
|
||||||
|
// because a relative string would be read in the site's time zone against a GMT column.
|
||||||
|
'date_query' => [[
|
||||||
|
'before' => gmdate('Y-m-d H:i:s', time() - self::ORPHAN_AGE_SECONDS),
|
||||||
|
'column' => 'post_modified_gmt',
|
||||||
|
]],
|
||||||
|
'posts_per_page' => 20,
|
||||||
|
'no_found_rows' => true,
|
||||||
|
'suppress_filters' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$removed = [];
|
||||||
|
foreach (is_array($posts) ? $posts : [] as $post) {
|
||||||
|
if (! $post instanceof WP_Post || ! str_starts_with((string) $post->post_title, $this->titlePrefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (self::delete((int) $post->ID)) {
|
||||||
|
$removed[] = (int) $post->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int The new ID; 0 when WordPress refused (logged). */
|
||||||
|
private function create(string $postType): int
|
||||||
|
{
|
||||||
|
$id = wp_insert_post([
|
||||||
|
'post_type' => $postType,
|
||||||
|
'post_status' => 'draft',
|
||||||
|
// Without it a draft keeps post_date_gmt and post_modified_gmt at 0000-00-00 and looks ancient.
|
||||||
|
'post_date_gmt' => gmdate('Y-m-d H:i:s'),
|
||||||
|
'post_title' => $this->titlePrefix . ' ' . gmdate('Y-m-d H:i'),
|
||||||
|
'post_content' => '',
|
||||||
|
'meta_input' => [$this->marker => '1'],
|
||||||
|
], true);
|
||||||
|
if ($id instanceof WP_Error) {
|
||||||
|
Log::failure($this->logContext, 'could not create the entry: ' . $id->get_error_message());
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ((int) $id <= 0) {
|
||||||
|
Log::failure($this->logContext, 'could not create the entry: no ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, (int) $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function delete(int $id): bool
|
||||||
|
{
|
||||||
|
return wp_delete_post($id, true) instanceof WP_Post;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Integrations\Divi\Release;
|
namespace LIT\MCP\Integrations\Divi\Release;
|
||||||
|
|
||||||
use LIT\MCP\Abilities\Errors;
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use LIT\MCP\Compatibility\ThrowawayPost;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||||
use LIT\MCP\Log;
|
|
||||||
use Throwable;
|
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ defined('ABSPATH') || exit;
|
|||||||
* → Serializer → DiviSaver, with the release lock opened for this page only), checked (content, builder flags,
|
* → Serializer → DiviSaver, with the release lock opened for this page only), checked (content, builder flags,
|
||||||
* Divi renders every module without a PHP warning from Divi's files) and always deleted for good – in finally,
|
* Divi renders every module without a PHP warning from Divi's files) and always deleted for good – in finally,
|
||||||
* not to the trash, with its revisions and meta. Pages a dead check left behind carry MARKER; removeOrphans()
|
* not to the trash, with its revisions and meta. Pages a dead check left behind carry MARKER; removeOrphans()
|
||||||
* deletes them. No change log entry. Not final so CompatibilityCheck can be tested with a mock.
|
* deletes them (both in ThrowawayPost). No change log entry. Not final so CompatibilityCheck can be tested with a mock.
|
||||||
*/
|
*/
|
||||||
class WriteProbe
|
class WriteProbe
|
||||||
{
|
{
|
||||||
@@ -70,9 +69,6 @@ class WriteProbe
|
|||||||
'litcheck-accordion', 'litcheck-tab', 'litcheck-icon-list',
|
'litcheck-accordion', 'litcheck-tab', 'litcheck-icon-list',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Only pages older than this are orphans – a running check's page is younger. */
|
|
||||||
private const ORPHAN_AGE = '10 minutes ago';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string|null $diviDir PHP warnings from files below it fail the check; null = DiviEnvironment::themeDir().
|
* @param string|null $diviDir PHP warnings from files below it fail the check; null = DiviEnvironment::themeDir().
|
||||||
*/
|
*/
|
||||||
@@ -86,66 +82,28 @@ class WriteProbe
|
|||||||
/** @return list<string> German problems; [] = green. */
|
/** @return list<string> German problems; [] = green. */
|
||||||
public function run(): array
|
public function run(): array
|
||||||
{
|
{
|
||||||
$id = wp_insert_post([
|
return self::throwaway()->run('page', fn (int $id): array => $this->exercise($id));
|
||||||
'post_type' => 'page',
|
|
||||||
'post_status' => 'draft',
|
|
||||||
'post_title' => self::TITLE . ' ' . gmdate('Y-m-d H:i'),
|
|
||||||
'post_content' => '',
|
|
||||||
'meta_input' => [self::MARKER => '1'],
|
|
||||||
], true);
|
|
||||||
if ($id instanceof WP_Error || (int) $id <= 0) {
|
|
||||||
return ['Die Testseite konnte nicht angelegt werden: '
|
|
||||||
. ($id instanceof WP_Error ? $id->get_error_message() : 'keine ID')];
|
|
||||||
}
|
|
||||||
$id = (int) $id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$problems = $this->exercise($id);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
// The report reaches MCP clients through the lock reason; the exception text stays in the log.
|
|
||||||
Log::failure('divi write test', $e);
|
|
||||||
$problems = ['Schreibtest abgebrochen (Details im PHP-Error-Log).'];
|
|
||||||
} finally {
|
|
||||||
if (! self::delete($id)) {
|
|
||||||
$problems[] = sprintf(
|
|
||||||
'Die Testseite %d konnte nicht gelöscht werden; bitte unter Seiten endgültig löschen.',
|
|
||||||
$id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $problems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<int> IDs of check pages a dead check left behind, deleted now. Only drafts whose title starts
|
* @return list<int> IDs of check pages a dead check left behind, deleted now (ThrowawayPost::removeOrphans():
|
||||||
* with self::TITLE are considered – the marker meta alone is not enough (it is only a query hint here).
|
* marked drafts with the check title only).
|
||||||
*/
|
*/
|
||||||
public static function removeOrphans(): array
|
public static function removeOrphans(): array
|
||||||
{
|
{
|
||||||
$posts = get_posts([
|
return self::throwaway()->removeOrphans('page');
|
||||||
'post_type' => 'page',
|
|
||||||
'post_status' => 'draft',
|
|
||||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a handful of marked pages at most.
|
|
||||||
'meta_key' => self::MARKER,
|
|
||||||
'date_query' => [['before' => self::ORPHAN_AGE, 'column' => 'post_date_gmt']],
|
|
||||||
'fields' => 'all',
|
|
||||||
'posts_per_page' => 20,
|
|
||||||
'no_found_rows' => true,
|
|
||||||
'suppress_filters' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$removed = [];
|
|
||||||
foreach (is_array($posts) ? $posts : [] as $post) {
|
|
||||||
if (! $post instanceof WP_Post || ! str_starts_with((string) $post->post_title, self::TITLE)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (self::delete((int) $post->ID)) {
|
|
||||||
$removed[] = (int) $post->ID;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $removed;
|
private static function throwaway(): ThrowawayPost
|
||||||
|
{
|
||||||
|
return new ThrowawayPost(
|
||||||
|
self::MARKER,
|
||||||
|
self::TITLE,
|
||||||
|
'Schreibtest abgebrochen (Details im PHP-Error-Log).',
|
||||||
|
'Die Testseite konnte nicht angelegt werden (Details im PHP-Error-Log).',
|
||||||
|
'Die Testseite %d konnte nicht gelöscht werden; bitte unter Seiten endgültig löschen.',
|
||||||
|
'divi write test'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<string> */
|
/** @return list<string> */
|
||||||
@@ -219,9 +177,4 @@ class WriteProbe
|
|||||||
|
|
||||||
return [$html, $warnings];
|
return [$html, $warnings];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function delete(int $id): bool
|
|
||||||
{
|
|
||||||
return wp_delete_post($id, true) instanceof WP_Post;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Compatibility;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Compatibility\ThrowawayPost;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class ThrowawayPostTest extends TestCase
|
||||||
|
{
|
||||||
|
private const ABORTED = 'abgebrochen';
|
||||||
|
private const CREATE_FAILED = 'nicht angelegt';
|
||||||
|
private const DELETE_FAILED = 'Eintrag %d nicht gelöscht';
|
||||||
|
|
||||||
|
/** @var list<int> */
|
||||||
|
private array $deleted = [];
|
||||||
|
|
||||||
|
/** @var array<string, mixed>|null The wp_insert_post arguments. */
|
||||||
|
private ?array $inserted = null;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->deleted = [];
|
||||||
|
$this->inserted = null;
|
||||||
|
Functions\when('wp_insert_post')->alias(function (array $post): int {
|
||||||
|
$this->inserted = $post;
|
||||||
|
|
||||||
|
return 41;
|
||||||
|
});
|
||||||
|
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 throwaway(): ThrowawayPost
|
||||||
|
{
|
||||||
|
return new ThrowawayPost('_marker', '[Prefix]', self::ABORTED, self::CREATE_FAILED, self::DELETE_FAILED, 'ctx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Captures what Log writes to the PHP error log while $run runs. */
|
||||||
|
private function logged(Closure $run): string
|
||||||
|
{
|
||||||
|
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||||
|
$previous = ini_set('error_log', $log);
|
||||||
|
try {
|
||||||
|
$run();
|
||||||
|
|
||||||
|
return (string) file_get_contents($log);
|
||||||
|
} finally {
|
||||||
|
ini_set('error_log', (string) $previous);
|
||||||
|
unlink($log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRunsTheExerciseOnAMarkedDraftAndDeletesItForGood(): void
|
||||||
|
{
|
||||||
|
$seen = null;
|
||||||
|
$problems = $this->throwaway()->run('lit_person', static function (int $id) use (&$seen): array {
|
||||||
|
$seen = $id;
|
||||||
|
|
||||||
|
return ['rot'];
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['rot'], $problems);
|
||||||
|
self::assertSame(41, $seen);
|
||||||
|
self::assertSame([41], $this->deleted);
|
||||||
|
self::assertSame('lit_person', $this->inserted['post_type']);
|
||||||
|
self::assertSame('draft', $this->inserted['post_status']);
|
||||||
|
self::assertStringStartsWith('[Prefix] ', $this->inserted['post_title']);
|
||||||
|
self::assertSame(['_marker' => '1'], $this->inserted['meta_input']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** wp_insert_post leaves post_date_gmt AND post_modified_gmt at 0000-00-00 for a draft without a GMT date. */
|
||||||
|
public function testTheDraftGetsAGmtDateSoItsAgeIsKnown(): void
|
||||||
|
{
|
||||||
|
$this->throwaway()->run('page', static fn (): array => []);
|
||||||
|
|
||||||
|
self::assertMatchesRegularExpression('/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/', $this->inserted['post_date_gmt']);
|
||||||
|
self::assertEqualsWithDelta(time(), strtotime($this->inserted['post_date_gmt'] . ' UTC'), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGreenIsAnEmptyList(): void
|
||||||
|
{
|
||||||
|
self::assertSame([], $this->throwaway()->run('page', static fn (): array => []));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnExceptionBecomesTheFixedTextAndALogLineAndTheEntryIsDeleted(): void
|
||||||
|
{
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems): void {
|
||||||
|
$problems = $this->throwaway()->run('page', static function (): never {
|
||||||
|
throw new RuntimeException('Innen kaputt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame([self::ABORTED], $problems);
|
||||||
|
self::assertStringContainsString('ctx failed', $logged);
|
||||||
|
self::assertStringContainsString('Innen kaputt', $logged);
|
||||||
|
self::assertSame([41], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnInsertErrorIsAFixedTextAndTheMessageGoesToTheLogOnly(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_insert_post')->justReturn(new WP_Error('x', 'Datenbank voll'));
|
||||||
|
$ran = false;
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems, &$ran): void {
|
||||||
|
$problems = $this->throwaway()->run('page', static function () use (&$ran): array {
|
||||||
|
$ran = true;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame([self::CREATE_FAILED], $problems);
|
||||||
|
self::assertStringContainsString('Datenbank voll', $logged);
|
||||||
|
self::assertFalse($ran);
|
||||||
|
self::assertSame([], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnInsertWithoutAnIdIsTheSameFixedText(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_insert_post')->justReturn(0);
|
||||||
|
|
||||||
|
$problems = $this->logged(fn () => self::assertSame(
|
||||||
|
[self::CREATE_FAILED],
|
||||||
|
$this->throwaway()->run('page', static fn (): array => [])
|
||||||
|
));
|
||||||
|
|
||||||
|
self::assertStringContainsString('ctx', $problems);
|
||||||
|
self::assertSame([], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnInsertThatThrowsIsAbortedAndNothingIsDeleted(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_insert_post')->alias(static function (): never {
|
||||||
|
throw new RuntimeException('Hook kaputt');
|
||||||
|
});
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems): void {
|
||||||
|
$problems = $this->throwaway()->run('page', static fn (): array => []);
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame([self::ABORTED], $problems);
|
||||||
|
self::assertStringContainsString('Hook kaputt', $logged);
|
||||||
|
self::assertSame([], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnEntryThatCannotBeDeletedIsReportedNextToTheProblems(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_delete_post')->justReturn(false);
|
||||||
|
|
||||||
|
$problems = $this->throwaway()->run('page', static fn (): array => ['rot']);
|
||||||
|
|
||||||
|
self::assertSame(['rot', 'Eintrag 41 nicht gelöscht'], $problems);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drafts have post_date_gmt 0000-00-00 00:00:00: only post_modified_gmt tells how old they are. A fake
|
||||||
|
* get_posts applies the column of the date query to the rows, like WP_Query would.
|
||||||
|
*
|
||||||
|
* @param list<array{ID: int, post_title: string, post_date_gmt: string, post_modified_gmt: string}> $rows
|
||||||
|
*/
|
||||||
|
private function fakeQuery(array $rows, ?array &$query = null): void
|
||||||
|
{
|
||||||
|
Functions\when('get_posts')->alias(static function (array $args) use ($rows, &$query): array {
|
||||||
|
$query = $args;
|
||||||
|
$filter = $args['date_query'][0];
|
||||||
|
$found = array_filter(
|
||||||
|
$rows,
|
||||||
|
static fn (array $row): bool => $row[$filter['column']] < $filter['before']
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_map(static fn (array $row): WP_Post => new WP_Post($row), array_values($found));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{ID: int, post_title: string, post_date_gmt: string, post_modified_gmt: string} */
|
||||||
|
private static function draft(int $id, int $ageSeconds, string $title = '[Prefix] 2026-11-06 10:00'): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ID' => $id,
|
||||||
|
'post_title' => $title,
|
||||||
|
'post_date_gmt' => '0000-00-00 00:00:00',
|
||||||
|
'post_modified_gmt' => gmdate('Y-m-d H:i:s', time() - $ageSeconds),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testARunningChecksDraftIsKeptEvenThoughItsDateIsZero(): void
|
||||||
|
{
|
||||||
|
$this->fakeQuery([self::draft(5, 60)]);
|
||||||
|
|
||||||
|
self::assertSame([], $this->throwaway()->removeOrphans('page'));
|
||||||
|
self::assertSame([], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testADraftOlderThanElevenMinutesIsRemoved(): void
|
||||||
|
{
|
||||||
|
$query = null;
|
||||||
|
$this->fakeQuery([self::draft(5, 660), self::draft(6, 60)], $query);
|
||||||
|
|
||||||
|
self::assertSame([5], $this->throwaway()->removeOrphans('page'));
|
||||||
|
self::assertSame([5], $this->deleted);
|
||||||
|
self::assertSame('post_modified_gmt', $query['date_query'][0]['column']);
|
||||||
|
self::assertMatchesRegularExpression('/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/', $query['date_query'][0]['before']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheQueryIsNarrowedToMarkedDraftsOfTheGivenTypes(): void
|
||||||
|
{
|
||||||
|
$query = null;
|
||||||
|
$this->fakeQuery([], $query);
|
||||||
|
|
||||||
|
$this->throwaway()->removeOrphans(['post', 'lit_person']);
|
||||||
|
self::assertSame(['post', 'lit_person'], $query['post_type']);
|
||||||
|
self::assertSame('draft', $query['post_status']);
|
||||||
|
self::assertSame('_marker', $query['meta_key']);
|
||||||
|
|
||||||
|
$this->throwaway()->removeOrphans('page');
|
||||||
|
self::assertSame('page', $query['post_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAMarkedDraftWithAnotherTitleIsKept(): void
|
||||||
|
{
|
||||||
|
$this->fakeQuery([self::draft(5, 900, 'Echte Person')]);
|
||||||
|
|
||||||
|
self::assertSame([], $this->throwaway()->removeOrphans('page'));
|
||||||
|
self::assertSame([], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnOrphanThatCannotBeDeletedIsNotReportedAsRemoved(): void
|
||||||
|
{
|
||||||
|
$this->fakeQuery([self::draft(5, 900)]);
|
||||||
|
Functions\when('wp_delete_post')->justReturn(false);
|
||||||
|
|
||||||
|
self::assertSame([], $this->throwaway()->removeOrphans('page'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,28 +156,28 @@ final class WriteProbeTest extends TestCase
|
|||||||
self::assertSame(['_lit_mcp_check_page' => '1'], $captured['meta_input']);
|
self::assertSame(['_lit_mcp_check_page' => '1'], $captured['meta_input']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRemovesOrphanedCheckPages(): void
|
public function testRemovesOrphanedCheckPagesThroughTheSharedCleanup(): void
|
||||||
{
|
{
|
||||||
Functions\expect('get_posts')->once()->with(Mockery::on(static fn (array $query): bool =>
|
Functions\expect('get_posts')->once()->with(Mockery::on(static fn (array $query): bool =>
|
||||||
$query['meta_key'] === '_lit_mcp_check_page'
|
$query['meta_key'] === '_lit_mcp_check_page'
|
||||||
|
&& $query['post_type'] === 'page'
|
||||||
&& $query['post_status'] === 'draft'
|
&& $query['post_status'] === 'draft'
|
||||||
&& $query['fields'] === 'all'
|
&& ($query['date_query'][0]['column'] ?? null) === 'post_modified_gmt'))->andReturn([
|
||||||
&& ($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' => 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']),
|
new WP_Post(['ID' => 60, 'post_title' => 'Ein anderer Entwurf']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
self::assertSame([], WriteProbe::removeOrphans());
|
self::assertSame([51], WriteProbe::removeOrphans());
|
||||||
|
self::assertSame([51], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAPageThatCannotBeCreatedIsAFixedTextWithoutTheWordPressMessage(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_insert_post')->justReturn(new \WP_Error('x', 'Datenbank voll'));
|
||||||
|
|
||||||
|
$problems = (new WriteProbe($this->composer(), $this->renderer(''), '/nonexistent-divi'))->run();
|
||||||
|
|
||||||
|
self::assertSame(['Die Testseite konnte nicht angelegt werden (Details im PHP-Error-Log).'], $problems);
|
||||||
self::assertSame([], $this->deleted);
|
self::assertSame([], $this->deleted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user