fix: log partial menu writes; describe that the link gate covers new items only

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 01:26:40 +02:00
co-authored by Claude Opus 5.5
parent ff0da9f406
commit b3c9109e47
2 changed files with 33 additions and 3 deletions
@@ -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'];
@@ -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());
}
}