justReturn('2026-09-25 08:00:00'); Functions\when('get_current_user_id')->justReturn(3); RequestContext::reset(true, 'Claude'); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->expects('insert') ->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool { return $row['created_at'] === '2026-09-25 08:00:00' && $row['user_id'] === 3 && $row['client'] === 'Claude' && $row['ability'] === 'lit/content-update' && $row['object_type'] === 'page' && $row['object_id'] === '12' && $row['summary'] === 'Seite geändert' && $row['revision_id'] === 811 && $row['snapshot_id'] === null && $row['result'] === 'ok'; }), Mockery::type('array')) ->andReturnUsing(static function () use ($db): int { $db->insert_id = 4711; return 1; }); $id = (new ChangeLog($db))->record('lit/content-update', 'page', '12', 'Seite geändert', 811); self::assertSame(4711, $id); } public function testRecordReturnsNullWhenTheInsertFails(): void { Functions\when('current_time')->justReturn('2026-09-25 08:00:00'); Functions\when('get_current_user_id')->justReturn(3); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->allows('insert')->andReturn(false); $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); try { $id = (new ChangeLog($db))->record('lit/content-update', 'page', '12', 'x'); } finally { ini_set('error_log', (string) $previous); } self::assertNull($id); self::assertStringContainsString('change log insert failed', (string) file_get_contents($log)); unlink($log); } public function testRecordTruncatesObjectIdAndAbilityToTheColumnSizes(): void { Functions\when('current_time')->justReturn('2026-09-25 08:00:00'); Functions\when('get_current_user_id')->justReturn(3); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->expects('insert') ->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool { return $row['object_id'] === str_repeat('ä', 191) && $row['ability'] === str_repeat('ö', 100); }), Mockery::type('array')) ->andReturn(1); (new ChangeLog($db))->record(str_repeat('ö', 150), 'option', str_repeat('ä', 300), 'x'); } public function testRecordPartialStoresResultPartial(): void { Functions\when('current_time')->justReturn('2026-09-25 08:00:00'); Functions\when('get_current_user_id')->justReturn(3); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->expects('insert') ->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool { return $row['result'] === 'partial' && $row['revision_id'] === 7 && $row['object_id'] === '12'; }), Mockery::type('array')) ->andReturn(1); (new ChangeLog($db))->recordPartial('lit/content-update', 'page', '12', 'teilweise', 7); } public function testRecordPartialNeverThrows(): void { Functions\when('current_time')->justReturn('2026-09-25 08:00:00'); Functions\when('get_current_user_id')->justReturn(3); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->allows('insert')->andThrow(new \RuntimeException('db gone')); $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); try { (new ChangeLog($db))->recordPartial('lit/menu-set-items', 'nav_menu', '4', 'teilweise'); } finally { ini_set('error_log', (string) $previous); } self::assertStringContainsString('db gone', (string) file_get_contents($log)); unlink($log); } public function testPurgeDeletesRowsOlderThanTheRetention(): void { $db = Mockery::mock(\wpdb::class)->makePartial(); $db->expects('query') ->with(Mockery::on(static fn (string $sql): bool => str_starts_with( $sql, "DELETE FROM wp_lit_mcp_log WHERE created_at < '" ))) ->andReturn(3); self::assertSame(3, (new ChangeLog($db))->purgeOlderThan(90)); } }