fix: final review minors m-1 to m-8

- m-1: the no-change Divi result carries preview_url/edit_url too
- m-2: overview names replace-text's per-page tree_rev
- m-3: divi-shortform documents validation_failed vs not_found for unknown IDs
- m-4: DiviWriter/DiviPageLoader final (no mocks left), neutral NewDraft
  log detail, snapshots are Etappe 4
- m-5: Tested up to 7.1, changelog completed
- m-6: content-create warns when the extras changed the stored layout
- m-7: deleting a whole section outlines nothing instead of the next one
- m-8: uninstall removes the user meta lit_mcp_log_per_page; readme lists it

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:
2026-09-26 19:35:24 +02:00
co-authored by Claude Opus 5.5
parent b24eb6c3e9
commit 097fef2e32
15 changed files with 136 additions and 21 deletions
+4 -1
View File
@@ -86,7 +86,10 @@ ist dort das Admin-Label). Beim Einfügen über `attrs` (`fieldItem.advanced.typ
den betroffenen Abschnitt als `outline`, `warnings` (Qualität, siehe unten) und `revision_id`. den betroffenen Abschnitt als `outline`, `warnings` (Qualität, siehe unten) und `revision_id`.
Fehler: `validation_failed` bzw. `invalid_attr_path` mit `fields` (Eingabepfad → Problem, z. B. Fehler: `validation_failed` bzw. `invalid_attr_path` mit `fields` (Eingabepfad → Problem, z. B.
`nodes[0].rows[0].columns`); dann wurde nichts gespeichert. `nodes[0].rows[0].columns`); dann wurde nichts gespeichert. Eine unbekannte Pfad-ID meldet `lit-divi-update`
mit den übrigen Eingabefehlern gesammelt als `validation_failed` (`fields.changes[i].id`), `lit-divi-move`,
`lit-divi-delete` und `lit/divi-duplicate-node` als `not_found` – in beiden Fällen mit `lit-divi-outline`
neu lesen.
## Spaltenbreiten ändern ## Spaltenbreiten ändern
+2 -1
View File
@@ -47,7 +47,8 @@ Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows`, `media`,
- Erfolg: `ok`, `summary`, `changed`, `warnings`, `rev`, `revision_id` (Revision mit dem Stand vorher, nur bei - Erfolg: `ok`, `summary`, `changed`, `warnings`, `rev`, `revision_id` (Revision mit dem Stand vorher, nur bei
Tools, die Inhalt überschreiben, sonst `null`), `log_id`, URLs. Divi-Werkzeuge zusätzlich `tree_rev` (Stand Tools, die Inhalt überschreiben, sonst `null`), `log_id`, URLs. Divi-Werkzeuge zusätzlich `tree_rev` (Stand
des Seiteninhalts, Pflicht für die nächste Divi-Änderung) und `outline` (betroffener Abschnitt). des Seiteninhalts, Pflicht für die nächste Divi-Änderung) und `outline` (betroffener Abschnitt);
`lit/divi-replace-text` nennt `tree_rev` je Seite in `results` (Probelauf: in `matches`).
- Fehler: JSON mit `code` (z. B. `validation_failed`, `rev_mismatch`, `write_failed`, `partial_write`), - Fehler: JSON mit `code` (z. B. `validation_failed`, `rev_mismatch`, `write_failed`, `partial_write`),
`message`, `hint` (und ggf. `fields`). Dem `hint` folgen. `message`, `hint` (und ggf. `fields`). Dem `hint` folgen.
- `confirm_required`: Nutzer fragen, dann mit `confirm: true` wiederholen. - `confirm_required`: Nutzer fragen, dann mit `confirm: true` wiederholen.
+15 -5
View File
@@ -2,7 +2,7 @@
Contributors: loosenit Contributors: loosenit
Tags: mcp, claude, divi, pods, ai Tags: mcp, claude, divi, pods, ai
Requires at least: 6.9 Requires at least: 6.9
Tested up to: 7.0 Tested up to: 7.1
Requires PHP: 8.1 Requires PHP: 8.1
Stable tag: 0.3.0 Stable tag: 0.3.0
License: GPL-2.0-or-later License: GPL-2.0-or-later
@@ -40,7 +40,8 @@ Einstellungen: Einstellungen → LIT MCP (Verbindung, Zugriff, Tools, Protokoll,
sich unter Einstellungen → LIT MCP → Tools ändern. sich unter Einstellungen → LIT MCP → Tools ändern.
* Eigene Daten: Optionen lit_mcp_allowed_roles, lit_mcp_exposure, lit_mcp_limits, lit_mcp_db_version, * Eigene Daten: Optionen lit_mcp_allowed_roles, lit_mcp_exposure, lit_mcp_limits, lit_mcp_db_version,
Tabelle {prefix}lit_mcp_log (nur Verweise, keine Inhalte), täglicher Cron-Job lit_mcp_log_cleanup, Tabelle {prefix}lit_mcp_log (nur Verweise, keine Inhalte), täglicher Cron-Job lit_mcp_log_cleanup,
Transient lit_mcp_endpoint_check. Transient lit_mcp_endpoint_check, Benutzer-Meta lit_mcp_log_per_page (Ansichtsoption „Einträge pro Seite“
im Protokoll).
* Divi-Änderungen über LIT MCP speichern den Seiteninhalt für Benutzer ohne die Berechtigung * Divi-Änderungen über LIT MCP speichern den Seiteninhalt für Benutzer ohne die Berechtigung
unfiltered_html ohne die kses-Filterung von WordPress, weil diese sonst jeden unveränderten Divi-Block neu unfiltered_html ohne die kses-Filterung von WordPress, weil diese sonst jeden unveränderten Divi-Block neu
schreiben würde. Alle Werte, die LIT MCP dabei schreibt, werden vorher bereinigt (wp_kses_post, schreiben würde. Alle Werte, die LIT MCP dabei schreibt, werden vorher bereinigt (wp_kses_post,
@@ -103,7 +104,8 @@ Es gibt keine allgemeine "Rückgängig"-Funktion; was sich wie zurücksetzen lä
* Deaktivieren entfernt den Cron-Job und lässt WordPress die Rewrite-Regeln neu erzeugen; der * Deaktivieren entfernt den Cron-Job und lässt WordPress die Rewrite-Regeln neu erzeugen; der
MCP-Endpunkt und die OAuth-Adressen sind danach nicht mehr erreichbar. Einstellungen und Protokoll MCP-Endpunkt und die OAuth-Adressen sind danach nicht mehr erreichbar. Einstellungen und Protokoll
bleiben erhalten. bleiben erhalten.
* Löschen entfernt zusätzlich alle eigenen Optionen und die Protokolltabelle (auf Multisite je Website). * Löschen entfernt zusätzlich alle eigenen Optionen und die Protokolltabelle (auf Multisite je Website)
sowie die Ansichtsoption „Einträge pro Seite“ des Protokolls bei allen Benutzern.
* Nicht entfernt werden die Daten von mcp-oauth (Option mcp_jwt_secret, Option * Nicht entfernt werden die Daten von mcp-oauth (Option mcp_jwt_secret, Option
wpmedia_mcp_oauth_rewrite_version, Benutzer-Meta mcp_refresh_jti_*, Anwendungspasswörter der wpmedia_mcp_oauth_rewrite_version, Benutzer-Meta mcp_refresh_jti_*, Anwendungspasswörter der
Verbindungen), weil diese Bibliothek auch von anderen Plugins genutzt werden kann. Verbindungen Verbindungen), weil diese Bibliothek auch von anderen Plugins genutzt werden kann. Verbindungen
@@ -137,8 +139,16 @@ Designsystem. Nach einer Änderung im Builder liest Claude die Seite neu, bevor
Module einfügen, ändern, verschieben, duplizieren und löschen (Kurzform, Vollform, Platzhalter wie Module einfügen, ändern, verschieben, duplizieren und löschen (Kurzform, Vollform, Platzhalter wie
{{color:primary}}), Text seitenübergreifend ersetzen mit Probelauf, Qualitätswarnungen, neue Seiten {{color:primary}}), Text seitenübergreifend ersetzen mit Probelauf, Qualitätswarnungen, neue Seiten
direkt mit Divi-Inhalt. direkt mit Divi-Inhalt.
* Tools nach Gruppen einstellbar (direkt/indirekt/deaktiviert, „nur lesen“ je Gruppe); überarbeitete * Tools nach Gruppen einstellbar (direkt/indirekt/deaktiviert, „nur lesen“ je Gruppe; gilt auch für
Einstellungsseite. Divi-Inhalt über lit-content-create); überarbeitete Einstellungsseite, Protokoll als WordPress-Liste mit
Filtern, Zugriff mit „Zuletzt genutzt“ je Verbindung.
* OAuth-Discovery: /.well-known/oauth-* antwortet direkt statt per Weiterleitung (wichtig bei Permalinks mit
Schrägstrich am Ende); nicht freigegebene Benutzer können keine Verbindung mehr herstellen.
* Sicherheit: kein Divi-Markup über content, eigene Prüfung privater Adressbereiche beim Import per URL
(auch bei Weiterleitungen) mit neutraler Fehlermeldung, Pods-Feldrechte, kein direkter Aufruf der
PHP-Dateien.
* Kurzform: level auch für Kontaktformular, CTA, Toggle, Akkordeon, Slide, Zähler und Teammitglied;
lit/menu-create liefert menu_rev.
= 0.2.0 = = 0.2.0 =
* WordPress-Basis: Schema-Erkennung inkl. Pods, Inhalte lesen/anlegen/ändern, Status, Kopie und * WordPress-Basis: Schema-Erkennung inkl. Pods, Inhalte lesen/anlegen/ändern, Status, Kopie und
@@ -18,10 +18,9 @@ defined('ABSPATH') || exit;
/** /**
* Loads an entry for the Divi tools: rights and status via PostGuard (spec §4.5), then the * Loads an entry for the Divi tools: rights and status via PostGuard (spec §4.5), then the
* block tree. Divi 4 shortcode content and unbalanced markup are refused with a hint – * block tree. Divi 4 shortcode content and unbalanced markup are refused with a hint –
* spec §3.4: Divi 4 content is kept, never edited. Not final so abilities can be tested * spec §3.4: Divi 4 content is kept, never edited.
* with a mock.
*/ */
class DiviPageLoader final class DiviPageLoader
{ {
public const OUTLINE_HINT = 'Aktuellen Stand mit lit/divi-outline (direkt: lit-divi-outline) neu lesen und die ' public const OUTLINE_HINT = 'Aktuellen Stand mit lit/divi-outline (direkt: lit-divi-outline) neu lesen und die '
. 'Änderung mit der neuen tree_rev wiederholen.'; . 'Änderung mit der neuen tree_rev wiederholen.';
+7 -5
View File
@@ -23,10 +23,9 @@ defined('ABSPATH') || exit;
* (ruling P16; a mismatch is rev_mismatch and nothing is written), let the ability change the * (ruling P16; a mismatch is rev_mismatch and nothing is written), let the ability change the
* tree – only when its whole input validated (all or nothing) –, serialize, save via DiviSaver, * tree – only when its whole input validated (all or nothing) –, serialize, save via DiviSaver,
* then lit_mcp_after_save, change log and the ToolResult with changed nodes, affected outline, * then lit_mcp_after_save, change log and the ToolResult with changed nodes, affected outline,
* quality warnings (spec §5.5), revision_id, the new rev and tree_rev. Not final so abilities * quality warnings (spec §5.5), revision_id, the new rev and tree_rev.
* can be tested with a mock.
*/ */
class DiviWriter final class DiviWriter
{ {
public const OUTLINE_TRUNCATED = 'Die Übersicht der geänderten Abschnitte ist gekürzt; vollständig mit ' public const OUTLINE_TRUNCATED = 'Die Übersicht der geänderten Abschnitte ist gekürzt; vollständig mit '
. 'lit-divi-outline lesen.'; . 'lit-divi-outline lesen.';
@@ -89,6 +88,7 @@ class DiviWriter
->rev(Rev::ofPost($post)) ->rev(Rev::ofPost($post))
->with('tree_rev', $page->treeRev()) ->with('tree_rev', $page->treeRev())
->warnings($change->warnings()) ->warnings($change->warnings())
->urls((string) get_preview_post_link($post), $page->editUrl())
->toArray(); ->toArray();
} }
@@ -143,7 +143,8 @@ class DiviWriter
/** /**
* Outline of the sections that contain changed nodes (breadth limit of ruling S18). A removed * Outline of the sections that contain changed nodes (breadth limit of ruling S18). A removed
* node has no block any more, but its ID from before the removal still names the (surviving) * node has no block any more, but its ID from before the removal still names the (surviving)
* section. * section – unless the removed node was that section itself: its ID now names the next section,
* which did not change (review m-7).
* *
* @return array{text: ?string, truncated: bool} * @return array{text: ?string, truncated: bool}
*/ */
@@ -152,7 +153,8 @@ class DiviWriter
$sections = []; $sections = [];
foreach ($change->entries() as $entry) { foreach ($change->entries() as $entry) {
$id = DiviChange::idOf($entry, $index); $id = DiviChange::idOf($entry, $index);
if ($id !== null) { $wholeSection = $entry['block'] === null && $id !== null && ! str_contains($id, '.');
if ($id !== null && ! $wholeSection) {
$sections[explode('.', $id)[0]] = true; $sections[explode('.', $id)[0]] = true;
} }
} }
@@ -34,6 +34,10 @@ defined('ABSPATH') || exit;
*/ */
final class ContentCreateAbility implements Ability final class ContentCreateAbility implements Ability
{ {
/** The extras (written after the layout) re-saved the entry and changed the stored layout (review m-6). */
public const LAYOUT_CHANGED = 'Der Divi-Inhalt wurde beim Speichern der Felder verändert (z. B. durch ein Plugin); '
. 'tree_rev beschreibt den gespeicherten Stand – vor weiteren Änderungen lit-divi-outline lesen.';
public function __construct( public function __construct(
private ContentSchema $schema, private ContentSchema $schema,
private PostWriter $writer, private PostWriter $writer,
@@ -161,6 +165,7 @@ final class ContentCreateAbility implements Ability
return $draft->refuse($postId, $layout); return $draft->refuse($postId, $layout);
} }
$stored = is_array($layout) ? self::content($postId) : null;
try { try {
$this->writer->writeExtras($postId, $postType, $prepared, $input); $this->writer->writeExtras($postId, $postType, $prepared, $input);
} catch (Throwable $e) { } catch (Throwable $e) {
@@ -168,6 +173,9 @@ final class ContentCreateAbility implements Ability
} }
$warnings = array_merge($prepared['warnings'], $layout['warnings'] ?? []); $warnings = array_merge($prepared['warnings'], $layout['warnings'] ?? []);
if ($stored !== null && self::content($postId) !== $stored) {
$warnings[] = self::LAYOUT_CHANGED;
}
return $this->result($postId, $postType, $input, $warnings, $layout); return $this->result($postId, $postType, $input, $warnings, $layout);
} }
@@ -188,6 +196,13 @@ final class ContentCreateAbility implements Ability
return $this->divi?->check($postType, self::sections($input)); return $this->divi?->check($postType, self::sections($input));
} }
private static function content(int $postId): ?string
{
$post = get_post($postId);
return $post instanceof WP_Post ? (string) $post->post_content : null;
}
/** /**
* @param array<string, mixed> $input * @param array<string, mixed> $input
* @return list<mixed> * @return list<mixed>
@@ -106,7 +106,7 @@ final class NewDraft
static fn () => self::deleteDraft($postId) static fn () => self::deleteDraft($postId)
); );
$detail = sprintf('Extras of new post %d could not be written: %s', $postId, $cause->getMessage()); $detail = sprintf('Writing new post %d failed after the insert: %s', $postId, $cause->getMessage());
if ($problems === []) { if ($problems === []) {
throw WriteFailed::rolledBack($reason, $cause, $detail); throw WriteFailed::rolledBack($reason, $cause, $detail);
} }
+1 -1
View File
@@ -48,7 +48,7 @@ final class Limits
return $this->all()['log_retention_days']; return $this->all()['log_retention_days'];
} }
/** Reserved for design-system snapshots (Etappe 3); LimitsTab keeps the field hidden until then. */ /** Reserved for design-system snapshots (Etappe 4, spec §12); LimitsTab keeps the field hidden until then. */
public function snapshotCount(): int public function snapshotCount(): int
{ {
return $this->all()['snapshot_count']; return $this->all()['snapshot_count'];
+9 -2
View File
@@ -9,14 +9,15 @@ defined('ABSPATH') || exit;
/** /**
* Removes WP-LIT-MCP's own data (uninstall.php, Task 12): options, the endpoint-check * Removes WP-LIT-MCP's own data (uninstall.php, Task 12): options, the endpoint-check
* transient, the log-cleanup cron hook and the change log table – on every site of a * transient, the log-cleanup cron hook and the change log table – on every site of a
* network. * network – and the Protokoll tab's per-user screen option.
* *
* Deliberately self-contained (no reference to any other LIT\MCP\* class): uninstall.php * Deliberately self-contained (no reference to any other LIT\MCP\* class): uninstall.php
* is executed by WordPress core standalone, without loading the plugin's main file first, * is executed by WordPress core standalone, without loading the plugin's main file first,
* so the Composer/Jetpack autoloader is not guaranteed to have run. The option/hook/table * so the Composer/Jetpack autoloader is not guaranteed to have run. The option/hook/table
* names below are therefore duplicated from Access::OPTION, Abilities\ExposurePolicy::OPTION, * names below are therefore duplicated from Access::OPTION, Abilities\ExposurePolicy::OPTION,
* Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Admin\ConnectionStatus:: * Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Admin\ConnectionStatus::
* ENDPOINT_TRANSIENT, ChangeLog\ChangeLogCleanup::HOOK and ChangeLog\ChangeLog::TABLE on * ENDPOINT_TRANSIENT, ChangeLog\ChangeLogCleanup::HOOK, ChangeLog\ChangeLog::TABLE and
* Admin\LogTab::PER_PAGE_OPTION on
* purpose; tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in * purpose; tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in
* sync with those constants. * sync with those constants.
* *
@@ -32,8 +33,14 @@ final class Uninstaller
private const CRON_HOOK = 'lit_mcp_log_cleanup'; private const CRON_HOOK = 'lit_mcp_log_cleanup';
private const TABLE = 'lit_mcp_log'; private const TABLE = 'lit_mcp_log';
/** Screen option "Einträge pro Seite" of the Protokoll tab (Admin\LogTab::PER_PAGE_OPTION): user meta. */
private const USER_META = 'lit_mcp_log_per_page';
public static function run(): void public static function run(): void
{ {
// User meta is shared by every site of a network: removed once, for all users.
delete_metadata('user', 0, self::USER_META, '', true);
if (! is_multisite()) { if (! is_multisite()) {
self::removeSiteData(); self::removeSiteData();
@@ -105,10 +105,43 @@ final class DiviWriterTest extends TestCase
$writer = $this->writer($saver, FakeChangeLog::forbidden()); $writer = $this->writer($saver, FakeChangeLog::forbidden());
$page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]);
Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true');
$result = $writer->save('lit/divi-update', $page, new DiviChange('nichts'), []); $result = $writer->save('lit/divi-update', $page, new DiviChange('nichts'), []);
self::assertSame(Rev::of($content), $result['tree_rev']); self::assertSame(Rev::of($content), $result['tree_rev']);
self::assertStringStartsWith('Keine Änderung', $result['summary']); self::assertStringStartsWith('Keine Änderung', $result['summary']);
// Review m-1: the same result shape as a save, URLs included.
self::assertSame('https://kunde.test/?page_id=7&et_fb=1', $result['edit_url']);
self::assertSame('https://kunde.test/?page_id=7&preview=true', $result['preview_url']);
}
/** Review m-7: a deleted whole section has no section left to outline (its ID now names the next one). */
public function testDeletingAWholeSectionOutlinesNothing(): void
{
$content = DiviFixtures::markup('synthetic-basic.html');
DiviFixtures::givenPost($content);
$saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andReturnUsing(static function (WP_Post $post, string $new): array {
DiviFixtures::givenPost($new);
return ['revision_id' => 811, 'warnings' => []];
});
$input = ['post_id' => 7, 'tree_rev' => Rev::of($content)];
Functions\when('do_action')->justReturn(null);
Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true');
$writer = $this->writer($saver, new FakeChangeLog(55));
$page = $writer->page($input);
$section = $page->index->find('s0');
$page->document->children()[0]->removeChild($section);
$result = $writer->save('lit/divi-delete', $page, (new DiviChange('gelöscht'))->removed('s0', $section), $input);
self::assertNull($result['outline']);
self::assertSame([['s0', 'divi/section', 'deleted']], array_map(
static fn (array $c): array => [$c['id'], $c['type'], $c['action']],
$result['changed']
));
} }
public function testSavesAndReportsChangedNodesOutlineAndTheNewTreeRev(): void public function testSavesAndReportsChangedNodesOutlineAndTheNewTreeRev(): void
@@ -123,6 +123,46 @@ final class ContentCreateDiviTest extends TestCase
self::assertArrayHasKey('divi', $this->ability(null)->inputSchema()['properties']); self::assertArrayHasKey('divi', $this->ability(null)->inputSchema()['properties']);
} }
/**
* Review m-6: the extras are written after the layout; if that re-saves the entry (e.g. Pods
* object fields call wp_update_post, a save_post listener) and the layout changes, say so.
*/
public function testALayoutChangedByTheExtrasIsReported(): void
{
$composer = $this->composer();
$composer->allows('write')->andReturn(['warnings' => [], 'outline' => 's0 divi/section']);
Functions\when('wp_insert_post')->justReturn(42);
$this->stubStoredDraft();
$writer = Mockery::mock(PostWriter::class);
$writer->allows('prepare')->andReturn(['fields' => [], 'terms' => [], 'warnings' => []]);
$writer->allows('postData')->andReturn(['post_title' => 'Start']);
$writer->allows('deniedMeta')->andReturn([]);
$writer->expects('writeExtras')->andReturnUsing(static function (): void {
Functions\when('get_post')->justReturn(new WP_Post([
'ID' => 42, 'post_title' => 'Start', 'post_type' => 'page', 'post_status' => 'draft',
'post_content' => '<!-- wp:divi/placeholder /-->',
]));
});
$result = $this->ability($composer, $writer)->execute(self::INPUT + ['fields' => ['role' => 'x']]);
self::assertSame(Rev::of('<!-- wp:divi/placeholder /-->'), $result['tree_rev'], 'the stored state');
self::assertContains(ContentCreateAbility::LAYOUT_CHANGED, $result['warnings']);
$this->stubStoredDraft();
$unchanged = $this->ability($this->composerWriting(), $this->writer())->execute(self::INPUT);
self::assertNotContains(ContentCreateAbility::LAYOUT_CHANGED, $unchanged['warnings']);
}
/** @return ContentComposer&MockInterface */
private function composerWriting(): ContentComposer
{
$composer = $this->composer();
$composer->allows('write')->andReturn(['warnings' => [], 'outline' => null]);
return $composer;
}
public function testWithoutDiviTheComposerIsNotUsed(): void public function testWithoutDiviTheComposerIsNotUsed(): void
{ {
$composer = Mockery::mock(ContentComposer::class); $composer = Mockery::mock(ContentComposer::class);
@@ -33,7 +33,7 @@ final class NewDraftTest extends TestCase
self::assertSame(42, $e->postId); self::assertSame(42, $e->postId);
self::assertSame('Grund für den Client.', $e->reason); self::assertSame('Grund für den Client.', $e->reason);
self::assertSame( self::assertSame(
'Extras of new post 42 could not be written: layout kaputt; rollback: term 15 bleibt; ' 'Writing new post 42 failed after the insert: layout kaputt; rollback: term 15 bleibt; '
. 'draft 42 could not be deleted: delete_post hook kaputt [rollback incomplete]', . 'draft 42 could not be deleted: delete_post hook kaputt [rollback incomplete]',
$e->getMessage() $e->getMessage()
); );
+1
View File
@@ -35,6 +35,7 @@ final class UninstallTest extends TestCase
Functions\expect('restore_current_blog')->twice(); Functions\expect('restore_current_blog')->twice();
Functions\expect('delete_transient')->twice()->with(ConnectionStatus::ENDPOINT_TRANSIENT); Functions\expect('delete_transient')->twice()->with(ConnectionStatus::ENDPOINT_TRANSIENT);
Functions\expect('wp_clear_scheduled_hook')->twice()->with(ChangeLogCleanup::HOOK); Functions\expect('wp_clear_scheduled_hook')->twice()->with(ChangeLogCleanup::HOOK);
Functions\expect('delete_metadata')->once()->with('user', 0, 'lit_mcp_log_per_page', '', true);
$deleted = []; $deleted = [];
Functions\when('delete_option')->alias(static function (string $option) use (&$deleted): bool { Functions\when('delete_option')->alias(static function (string $option) use (&$deleted): bool {
$deleted[] = $option; $deleted[] = $option;
+4
View File
@@ -8,6 +8,7 @@ use Brain\Monkey\Functions;
use LIT\MCP\Abilities\ExposurePolicy; use LIT\MCP\Abilities\ExposurePolicy;
use LIT\MCP\Access; use LIT\MCP\Access;
use LIT\MCP\Admin\ConnectionStatus; use LIT\MCP\Admin\ConnectionStatus;
use LIT\MCP\Admin\LogTab;
use LIT\MCP\ChangeLog\ChangeLogCleanup; use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller; use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Limits; use LIT\MCP\Limits;
@@ -37,6 +38,8 @@ final class UninstallerTest extends TestCase
Functions\expect('restore_current_blog')->never(); Functions\expect('restore_current_blog')->never();
Functions\expect('delete_transient')->once()->with(ConnectionStatus::ENDPOINT_TRANSIENT); Functions\expect('delete_transient')->once()->with(ConnectionStatus::ENDPOINT_TRANSIENT);
Functions\expect('wp_clear_scheduled_hook')->once()->with(ChangeLogCleanup::HOOK); Functions\expect('wp_clear_scheduled_hook')->once()->with(ChangeLogCleanup::HOOK);
// Review m-8: the Protokoll screen option is user meta (network-wide): removed once.
Functions\expect('delete_metadata')->once()->with('user', 0, LogTab::PER_PAGE_OPTION, '', true);
$deleted = []; $deleted = [];
Functions\when('delete_option')->alias(static function (string $option) use (&$deleted): bool { Functions\when('delete_option')->alias(static function (string $option) use (&$deleted): bool {
$deleted[] = $option; $deleted[] = $option;
@@ -62,6 +65,7 @@ final class UninstallerTest extends TestCase
Functions\expect('restore_current_blog')->twice(); Functions\expect('restore_current_blog')->twice();
Functions\expect('delete_transient')->twice()->with(ConnectionStatus::ENDPOINT_TRANSIENT); Functions\expect('delete_transient')->twice()->with(ConnectionStatus::ENDPOINT_TRANSIENT);
Functions\expect('wp_clear_scheduled_hook')->twice()->with(ChangeLogCleanup::HOOK); Functions\expect('wp_clear_scheduled_hook')->twice()->with(ChangeLogCleanup::HOOK);
Functions\expect('delete_metadata')->once()->with('user', 0, LogTab::PER_PAGE_OPTION, '', true);
Functions\when('delete_option')->justReturn(true); Functions\when('delete_option')->justReturn(true);
Uninstaller::run(); Uninstaller::run();
+1 -1
View File
@@ -6,7 +6,7 @@
* Version: 0.3.0 * Version: 0.3.0
* Requires at least: 6.9 * Requires at least: 6.9
* Requires PHP: 8.1 * Requires PHP: 8.1
* Tested up to: 7.0 * Tested up to: 7.1
* Author: Jan-Niclas Loosen - Loosen-IT * Author: Jan-Niclas Loosen - Loosen-IT
* Author URI: https://loosen-it.de * Author URI: https://loosen-it.de
* License: GPL-2.0-or-later * License: GPL-2.0-or-later