From b3c9109e4797cc5d72458cc13df970be84b141dd Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 01:26:40 +0200 Subject: [PATCH] fix: log partial menu writes; describe that the link gate covers new items only Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- .../WordPress/Menus/MenuSetItemsAbility.php | 18 ++++++++++++++++-- .../Menus/MenuSetItemsAbilityTest.php | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php b/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php index a78706d..6d11f12 100644 --- a/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php +++ b/src/Integrations/WordPress/Menus/MenuSetItemsAbility.php @@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Ability; use LIT\MCP\Abilities\Errors; use LIT\MCP\Abilities\FollowUp; use LIT\MCP\Abilities\ToolResult; +use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; use WP_Error; @@ -50,7 +51,8 @@ final class MenuSetItemsAbility implements Ability . '#anker, http(s), mailto, tel) und title, jeweils optional children. Verlinkt werden nur für dich ' . 'lesbare, veröffentlichte Inhalte und Begriffe öffentlicher Taxonomien; private Inhalte nie. ' . 'Entwürfe/ausstehende/geplante Inhalte nur mit allow_unpublished: true – WordPress zeigt sie sofort ' - . 'im Menü, Besucher erhalten bis zur Veröffentlichung einen toten Link. Das ' + . 'im Menü, Besucher erhalten bis zur Veröffentlichung einen toten Link. Diese Prüfung gilt nur für ' + . 'neu verlinkte Einträge; bestehende Einträge bleiben unabhängig vom Status ihres Ziels erhalten. Das ' . 'Ziel eines bestehenden Eintrags ändern: entfernen und neu anlegen. Alles wird vorab geprüft, dann ' . 'ganz oder gar nicht geschrieben. confirm: true ist nur nötig, wenn dabei Einträge wegfallen – ' . 'Menüeinträge haben keinen Papierkorb und werden endgültig gelöscht. Die Antwort enthält ' @@ -166,7 +168,19 @@ final class MenuSetItemsAbility implements Ability } // Throws WriteFailed (after its own rollback) – the registrar turns it into the error. - $stats = $this->writer->apply($menuId, $tree, $current); + try { + $stats = $this->writer->apply($menuId, $tree, $current); + } catch (WriteFailed $e) { + if (! $e->rolledBack) { + $this->log->recordPartial( + $this->name(), + 'nav_menu', + (string) $menuId, + sprintf('Menü "%s": Änderung nur teilweise zurückgenommen', $menu->name) + ); + } + throw $e; + } // The menu is saved – from here on failures are logged and reported as warnings only. $warnings = $plan['warnings']; diff --git a/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php b/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php index c5a9b78..9888fc0 100644 --- a/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php @@ -204,7 +204,22 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); $writer->expects('apply')->andThrow(new WriteFailed('Menü nicht gespeichert.', true)); - $ability = new MenuSetItemsAbility($writer, new MenuOperations(), Mockery::mock(ChangeLog::class)); + $log = Mockery::mock(ChangeLog::class); + $log->shouldNotReceive('recordPartial'); + $ability = new MenuSetItemsAbility($writer, new MenuOperations(), $log); + + $this->expectException(WriteFailed::class); + $ability->execute(['menu_id' => 3, 'items' => [['id' => 10]]]); + } + + public function testAPartialWriteIsLoggedAsPartial(): void + { + $writer = Mockery::mock(MenuWriter::class); + $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); + $writer->expects('apply')->andThrow(new WriteFailed('Menü nicht gespeichert.', false)); + $log = Mockery::mock(ChangeLog::class); + $log->expects('recordPartial')->with('lit/menu-set-items', 'nav_menu', '3', Mockery::type('string')); + $ability = new MenuSetItemsAbility($writer, new MenuOperations(), $log); $this->expectException(WriteFailed::class); $ability->execute(['menu_id' => 3, 'items' => [['id' => 10]]]); @@ -249,5 +264,6 @@ final class MenuSetItemsAbilityTest extends TestCase self::assertStringContainsString('confirm: true', $ability->description()); self::assertStringContainsString('nur nötig', $ability->description()); self::assertStringContainsString('CSS-Klassen', $ability->description()); + self::assertStringContainsString('nur für neu verlinkte Einträge', $ability->description()); } }