feat: change log table with retention cron and limits option
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\ChangeLog;
|
||||
|
||||
use LIT\MCP\Server\RequestContext;
|
||||
|
||||
/**
|
||||
* Change log table {prefix}lit_mcp_log (spec §8.3). Stores references only,
|
||||
* never content. Deliberately not final so abilities can be unit-tested with a mock.
|
||||
*/
|
||||
class ChangeLog
|
||||
{
|
||||
public const TABLE = 'lit_mcp_log';
|
||||
|
||||
public function __construct(private ?\wpdb $db = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function tableName(): string
|
||||
{
|
||||
return $this->db()->prefix . self::TABLE;
|
||||
}
|
||||
|
||||
public function charsetCollate(): string
|
||||
{
|
||||
return (string) $this->db()->get_charset_collate();
|
||||
}
|
||||
|
||||
public function record(
|
||||
string $ability,
|
||||
string $objectType,
|
||||
string $objectId,
|
||||
string $summary,
|
||||
?int $revisionId = null,
|
||||
?string $snapshotId = null,
|
||||
string $result = 'ok'
|
||||
): ?int {
|
||||
$wpdb = $this->db();
|
||||
$inserted = $wpdb->insert(
|
||||
$this->tableName(),
|
||||
[
|
||||
'created_at' => current_time('mysql', true),
|
||||
'user_id' => get_current_user_id(),
|
||||
'client' => $this->sanitizedClientName(),
|
||||
'ability' => $ability,
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $objectId,
|
||||
'summary' => mb_substr($summary, 0, 500),
|
||||
'revision_id' => $revisionId,
|
||||
'snapshot_id' => $snapshotId,
|
||||
'result' => $result,
|
||||
],
|
||||
['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($inserted === false) {
|
||||
$message = sprintf('[wp-lit-mcp] change log insert failed for %s %s:%s', $ability, $objectType, $objectId);
|
||||
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
public function purgeOlderThan(int $days): int
|
||||
{
|
||||
$wpdb = $this->db();
|
||||
$table = $this->tableName();
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - max(1, $days) * 86400);
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is our own constant.
|
||||
return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE created_at < %s", $cutoff));
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestContext::clientName() is a client-chosen string (app-password name or
|
||||
* OAuth client_id) – treat it as untrusted input before it lands in the DB.
|
||||
*/
|
||||
private function sanitizedClientName(): string
|
||||
{
|
||||
return mb_substr(sanitize_text_field(RequestContext::clientName()), 0, 191);
|
||||
}
|
||||
|
||||
protected function db(): \wpdb
|
||||
{
|
||||
if ($this->db === null) {
|
||||
global $wpdb;
|
||||
$this->db = $wpdb;
|
||||
}
|
||||
|
||||
return $this->db;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\ChangeLog;
|
||||
|
||||
use LIT\MCP\Limits;
|
||||
|
||||
/**
|
||||
* Daily WP-Cron job that removes log rows older than the configured retention.
|
||||
*/
|
||||
final class ChangeLogCleanup
|
||||
{
|
||||
public const HOOK = 'lit_mcp_log_cleanup';
|
||||
|
||||
public function __construct(private ChangeLog $log, private Limits $limits)
|
||||
{
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action(self::HOOK, [$this, 'run']);
|
||||
add_action('init', [$this, 'ensureScheduled']);
|
||||
}
|
||||
|
||||
public function ensureScheduled(): void
|
||||
{
|
||||
if (wp_next_scheduled(self::HOOK) === false) {
|
||||
wp_schedule_event(time() + 3600, 'daily', self::HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->log->purgeOlderThan($this->limits->logRetentionDays());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\ChangeLog;
|
||||
|
||||
/**
|
||||
* Creates/updates the change log table via dbDelta. Runs on activation and –
|
||||
* because plugin updates never fire the activation hook – whenever the stored
|
||||
* schema version differs.
|
||||
*/
|
||||
final class ChangeLogInstaller
|
||||
{
|
||||
public const VERSION_OPTION = 'lit_mcp_db_version';
|
||||
public const VERSION = '1';
|
||||
|
||||
public function __construct(private ChangeLog $log)
|
||||
{
|
||||
}
|
||||
|
||||
public function maybeInstall(): void
|
||||
{
|
||||
if (get_option(self::VERSION_OPTION) !== self::VERSION) {
|
||||
$this->install();
|
||||
}
|
||||
}
|
||||
|
||||
public function install(): void
|
||||
{
|
||||
if (! function_exists('dbDelta')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
}
|
||||
|
||||
$table = $this->log->tableName();
|
||||
$charset = $this->log->charsetCollate();
|
||||
|
||||
dbDelta("CREATE TABLE {$table} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
created_at datetime NOT NULL,
|
||||
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
client varchar(191) NOT NULL DEFAULT '',
|
||||
ability varchar(100) NOT NULL,
|
||||
object_type varchar(50) NOT NULL DEFAULT '',
|
||||
object_id varchar(191) NOT NULL DEFAULT '',
|
||||
summary text NOT NULL,
|
||||
revision_id bigint(20) unsigned DEFAULT NULL,
|
||||
snapshot_id varchar(64) DEFAULT NULL,
|
||||
result varchar(20) NOT NULL DEFAULT 'ok',
|
||||
PRIMARY KEY (id),
|
||||
KEY created_at (created_at),
|
||||
KEY ability (ability),
|
||||
KEY user_id (user_id)
|
||||
) {$charset};");
|
||||
|
||||
update_option(self::VERSION_OPTION, self::VERSION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP;
|
||||
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
||||
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
||||
|
||||
/**
|
||||
* Activation and deactivation hooks (registered in wp-lit-mcp.php).
|
||||
*/
|
||||
final class Lifecycle
|
||||
{
|
||||
public static function activate(): void
|
||||
{
|
||||
$log = new ChangeLog();
|
||||
(new ChangeLogInstaller($log))->install();
|
||||
(new ChangeLogCleanup($log, new Limits()))->ensureScheduled();
|
||||
}
|
||||
|
||||
public static function deactivate(): void
|
||||
{
|
||||
wp_clear_scheduled_hook(ChangeLogCleanup::HOOK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP;
|
||||
|
||||
/**
|
||||
* Admin-configurable limits (settings tab "Limits", spec §8.4).
|
||||
*/
|
||||
final class Limits
|
||||
{
|
||||
public const OPTION = 'lit_mcp_limits';
|
||||
public const DEFAULTS = ['upload_mb' => 5, 'log_retention_days' => 90, 'snapshot_count' => 20];
|
||||
private const BOUNDS = ['upload_mb' => [1, 100], 'log_retention_days' => [1, 3650], 'snapshot_count' => [1, 100]];
|
||||
|
||||
public function uploadBytes(): int
|
||||
{
|
||||
return $this->uploadMegabytes() * 1024 * 1024;
|
||||
}
|
||||
|
||||
public function uploadMegabytes(): int
|
||||
{
|
||||
return $this->all()['upload_mb'];
|
||||
}
|
||||
|
||||
public function logRetentionDays(): int
|
||||
{
|
||||
return $this->all()['log_retention_days'];
|
||||
}
|
||||
|
||||
public function snapshotCount(): int
|
||||
{
|
||||
return $this->all()['snapshot_count'];
|
||||
}
|
||||
|
||||
/** @return array{upload_mb: int, log_retention_days: int, snapshot_count: int} */
|
||||
public function all(): array
|
||||
{
|
||||
return self::sanitize(get_option(self::OPTION, []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings API sanitize callback: always returns all keys, clamped to their bounds.
|
||||
*
|
||||
* @return array{upload_mb: int, log_retention_days: int, snapshot_count: int}
|
||||
*/
|
||||
public static function sanitize(mixed $input): array
|
||||
{
|
||||
$input = is_array($input) ? $input : [];
|
||||
$clean = self::DEFAULTS;
|
||||
foreach (self::BOUNDS as $key => [$min, $max]) {
|
||||
if (isset($input[$key]) && is_numeric($input[$key])) {
|
||||
$clean[$key] = max($min, min($max, (int) $input[$key]));
|
||||
}
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP;
|
||||
|
||||
use LIT\MCP\Abilities\AbilityRegistrar;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
||||
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
||||
use LIT\MCP\Guides\InstructionsBuilder;
|
||||
use LIT\MCP\Integrations\Core\CoreIntegration;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
@@ -33,6 +36,10 @@ final class Plugin
|
||||
(new Access())->register();
|
||||
(new RequestContext())->register();
|
||||
|
||||
$changeLog = new ChangeLog();
|
||||
(new ChangeLogInstaller($changeLog))->maybeInstall();
|
||||
(new ChangeLogCleanup($changeLog, new Limits()))->register();
|
||||
|
||||
$registry = new IntegrationRegistry(
|
||||
new CoreIntegration(self::pluginDir() . '/docs/guides')
|
||||
);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (! class_exists('wpdb')) {
|
||||
/**
|
||||
* Minimal wpdb for unit tests. prepare()/esc_like() behave like the real ones
|
||||
* closely enough for assertions on the generated SQL; query methods are meant
|
||||
* to be mocked via Mockery::mock(wpdb::class)->makePartial().
|
||||
*/
|
||||
class wpdb
|
||||
{
|
||||
public $prefix = 'wp_';
|
||||
public $posts = 'wp_posts';
|
||||
public $postmeta = 'wp_postmeta';
|
||||
public $usermeta = 'wp_usermeta';
|
||||
public $options = 'wp_options';
|
||||
public $insert_id = 0;
|
||||
|
||||
public function prepare($query, ...$args)
|
||||
{
|
||||
if (count($args) === 1 && is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
// Quotes string placeholders but does not escape, so tests can assert on raw values.
|
||||
$query = str_replace("'%s'", '%s', $query);
|
||||
$query = str_replace('%s', "'%s'", $query);
|
||||
|
||||
return vsprintf($query, $args);
|
||||
}
|
||||
|
||||
public function esc_like($text)
|
||||
{
|
||||
return addcslashes($text, '_%\\');
|
||||
}
|
||||
|
||||
public function get_charset_collate()
|
||||
{
|
||||
return 'DEFAULT CHARACTER SET utf8mb4';
|
||||
}
|
||||
|
||||
public function insert($table, $data, $format = null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function query($query)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function get_col($query = null, $x = 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function get_var($query = null, $x = 0, $y = 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_results($query = null, $output = 'OBJECT')
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\ChangeLog;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
||||
use LIT\MCP\Limits;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class ChangeLogCleanupTest extends TestCase
|
||||
{
|
||||
public function testRunPurgesWithTheConfiguredRetention(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn(['log_retention_days' => 30]);
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->expects('purgeOlderThan')->with(30)->andReturn(0);
|
||||
|
||||
(new ChangeLogCleanup($log, new Limits()))->run();
|
||||
}
|
||||
|
||||
public function testEnsureScheduledSchedulesADailyEventOnce(): void
|
||||
{
|
||||
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||
Functions\expect('wp_schedule_event')->once()->with(Mockery::type('int'), 'daily', 'lit_mcp_log_cleanup');
|
||||
|
||||
(new ChangeLogCleanup(Mockery::mock(ChangeLog::class), new Limits()))->ensureScheduled();
|
||||
}
|
||||
|
||||
public function testEnsureScheduledDoesNothingWhenAlreadyScheduled(): void
|
||||
{
|
||||
Functions\when('wp_next_scheduled')->justReturn(1790330400);
|
||||
Functions\expect('wp_schedule_event')->never();
|
||||
|
||||
(new ChangeLogCleanup(Mockery::mock(ChangeLog::class), new Limits()))->ensureScheduled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\ChangeLog;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class ChangeLogInstallerTest extends TestCase
|
||||
{
|
||||
public function testInstallCreatesTheTableAndStoresTheVersion(): void
|
||||
{
|
||||
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||||
Functions\expect('dbDelta')->once()->with(Mockery::on(static function (string $sql): bool {
|
||||
return str_contains($sql, 'CREATE TABLE wp_lit_mcp_log (')
|
||||
&& str_contains($sql, 'client varchar(191)')
|
||||
&& str_contains($sql, 'revision_id bigint(20) unsigned DEFAULT NULL')
|
||||
&& str_contains($sql, 'snapshot_id varchar(64) DEFAULT NULL')
|
||||
&& str_contains($sql, 'PRIMARY KEY (id)');
|
||||
}));
|
||||
Functions\expect('update_option')->once()->with('lit_mcp_db_version', '1');
|
||||
|
||||
(new ChangeLogInstaller(new ChangeLog($db)))->install();
|
||||
}
|
||||
|
||||
public function testMaybeInstallSkipsWhenTheVersionIsCurrent(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn('1');
|
||||
Functions\expect('dbDelta')->never();
|
||||
|
||||
(new ChangeLogInstaller(new ChangeLog(Mockery::mock(\wpdb::class)->makePartial())))->maybeInstall();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\ChangeLog;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Server\RequestContext;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class ChangeLogTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RequestContext::reset();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testRecordStoresReferencesAndReturnsTheId(): void
|
||||
{
|
||||
Functions\when('current_time')->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 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Lifecycle;
|
||||
use Mockery;
|
||||
|
||||
final class LifecycleTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($GLOBALS['wpdb']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testActivateInstallsTheTableAndSchedulesTheCleanup(): void
|
||||
{
|
||||
$GLOBALS['wpdb'] = Mockery::mock(\wpdb::class)->makePartial();
|
||||
Functions\expect('dbDelta')->once();
|
||||
Functions\expect('update_option')->once()->with('lit_mcp_db_version', '1');
|
||||
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||
Functions\expect('wp_schedule_event')->once();
|
||||
|
||||
Lifecycle::activate();
|
||||
}
|
||||
|
||||
public function testDeactivateUnschedulesTheCleanup(): void
|
||||
{
|
||||
Functions\expect('wp_clear_scheduled_hook')->once()->with('lit_mcp_log_cleanup');
|
||||
|
||||
Lifecycle::deactivate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Limits;
|
||||
|
||||
final class LimitsTest extends TestCase
|
||||
{
|
||||
public function testDefaultsWhenOptionIsMissing(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn([]);
|
||||
$limits = new Limits();
|
||||
|
||||
self::assertSame(5 * 1024 * 1024, $limits->uploadBytes());
|
||||
self::assertSame(5, $limits->uploadMegabytes());
|
||||
self::assertSame(90, $limits->logRetentionDays());
|
||||
self::assertSame(20, $limits->snapshotCount());
|
||||
}
|
||||
|
||||
public function testStoredValuesAreClamped(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn(['upload_mb' => 500, 'log_retention_days' => 0, 'snapshot_count' => '7']);
|
||||
|
||||
self::assertSame(['upload_mb' => 100, 'log_retention_days' => 1, 'snapshot_count' => 7], (new Limits())->all());
|
||||
}
|
||||
|
||||
public function testCorruptOptionFallsBackToDefaults(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn('garbage');
|
||||
|
||||
self::assertSame(Limits::DEFAULTS, (new Limits())->all());
|
||||
}
|
||||
|
||||
public function testSanitizeReturnsACompleteClampedArray(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['upload_mb' => 12, 'log_retention_days' => 90, 'snapshot_count' => 100],
|
||||
Limits::sanitize(['upload_mb' => '12', 'log_retention_days' => 'x', 'snapshot_count' => 1000])
|
||||
);
|
||||
self::assertSame(Limits::DEFAULTS, Limits::sanitize(null));
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/Stubs/WpError.php';
|
||||
require_once __DIR__ . '/Stubs/WpUser.php';
|
||||
require_once __DIR__ . '/Stubs/WpRestRequest.php';
|
||||
require_once __DIR__ . '/Stubs/Wpdb.php';
|
||||
|
||||
@@ -33,6 +33,9 @@ if (! is_readable($lit_mcp_autoload)) {
|
||||
|
||||
require_once $lit_mcp_autoload;
|
||||
|
||||
register_activation_hook(__FILE__, [\LIT\MCP\Lifecycle::class, 'activate']);
|
||||
register_deactivation_hook(__FILE__, [\LIT\MCP\Lifecycle::class, 'deactivate']);
|
||||
|
||||
add_action('plugins_loaded', static function (): void {
|
||||
\LIT\MCP\Plugin::boot(__FILE__);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user