*/ private array $deleted = []; /** @var array|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 $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')); } }