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,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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user