From 3d760113ecaaad2d6ec23948ae33d3197c9394f0 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Fri, 16 Oct 2026 18:10:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Standard-Preset=20l=C3=B6schen=20?= =?UTF-8?q?=E2=80=93=20nur=20ausdr=C3=BCckliche=20Verweise=20blockieren,?= =?UTF-8?q?=20implizite=20Nutzung=20im=20Probelauf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../Divi/Design/DiviPresetsDeleteAbility.php | 22 ++++++++-- .../Design/DiviPresetsDeleteAbilityTest.php | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/Integrations/Divi/Design/DiviPresetsDeleteAbility.php b/src/Integrations/Divi/Design/DiviPresetsDeleteAbility.php index 540d789..64dde7a 100644 --- a/src/Integrations/Divi/Design/DiviPresetsDeleteAbility.php +++ b/src/Integrations/Divi/Design/DiviPresetsDeleteAbility.php @@ -17,8 +17,9 @@ defined('ABSPATH') || exit; /** * lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme * Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused - * with its usages (in_use); lit/divi-design-detach (Etappe 4b) removes the references first. A default preset loses - * its default. Through the change pipeline with Divi's delete semantics (legacy option cleaned). + * with its usages (in_use); lit/divi-design-detach (Etappe 4b) removes the references first. Only explicit references + * block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry + * run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned). */ final class DiviPresetsDeleteAbility implements Ability { @@ -28,6 +29,8 @@ final class DiviPresetsDeleteAbility implements Ability . 'Divi-Builder; danach erneut löschen.'; /** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */ + private const DEFAULT_WARNING = 'Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.'; + public function __construct( private DesignSystemWriter $writer, private DesignChange $change, @@ -158,7 +161,9 @@ final class DiviPresetsDeleteAbility implements Ability } } } - $report = $this->usages->reportFor(new PresetUsageQuery($targets), false); + // Spec §6.4: only explicit references block; a default preset's implicit use is listed in the dry run. + $explicit = array_map(static fn (array $target): array => ['default' => false] + $target, $targets); + $report = $this->usages->reportFor(new PresetUsageQuery($explicit), false); if ($nested !== [] || $report->exists()) { return Errors::inUse( 'Die Presets werden noch verwendet; nichts wurde gelöscht.', @@ -173,6 +178,15 @@ final class DiviPresetsDeleteAbility implements Ability $diff = PresetDiff::between($before, $book); - return new DesignPlan($book->data(), $diff, PresetDiff::summary($diff), null, [], true); + $defaults = array_values(array_filter($targets, static fn (array $target): bool => $target['default'])); + + return new DesignPlan( + $book->data(), + $diff, + PresetDiff::summary($diff), + $defaults === [] ? null : $this->usages->reportFor(new PresetUsageQuery($defaults)), + $defaults === [] ? [] : [self::DEFAULT_WARNING], + true + ); } } diff --git a/tests/Unit/Integrations/Divi/Design/DiviPresetsDeleteAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviPresetsDeleteAbilityTest.php index 930d33a..e1b846a 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviPresetsDeleteAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviPresetsDeleteAbilityTest.php @@ -8,12 +8,14 @@ use Brain\Monkey\Functions; use LIT\MCP\Integrations\Divi\Design\DesignChange; use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter; use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility; +use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery; use LIT\MCP\Integrations\Divi\Design\SnapshotStore; use LIT\MCP\Integrations\Divi\Library\GlobalUsages; use LIT\MCP\Integrations\Divi\Library\UsageReport; use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; +use ReflectionProperty; final class DiviPresetsDeleteAbilityTest extends TestCase { @@ -135,6 +137,47 @@ final class DiviPresetsDeleteAbilityTest extends TestCase self::assertToolError($this->ability(self::unused(), null, null, false)->execute($input), 'confirm_required'); } + /** @return list The default flags of a PresetUsageQuery's targets. */ + private static function defaultFlags(PresetUsageQuery $query): array + { + $targets = (new ReflectionProperty(PresetUsageQuery::class, 'targets'))->getValue($query); + + return array_column($targets, 'default'); + } + + /** Final review I-6 (spec §6.4): implicit use of a default preset does not block; the dry run lists it. */ + public function testImplicitUseOfADefaultPresetIsListedNotBlocking(): void + { + $used = new UsageReport(1, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']], + 0, 1, [], 0, false); + Functions\when('current_user_can')->justReturn(true); + $writer = Mockery::mock(DesignSystemWriter::class); + $writer->allows('presets')->andReturn(PresetBookTest::stored()); + $writer->allows('canUseVisualBuilder')->andReturn(true); + $usages = Mockery::mock(GlobalUsages::class); + $usages->expects('reportFor') + ->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [false]), false) + ->andReturn(self::unused()); + $usages->expects('reportFor') + ->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [true])) + ->andReturn($used); + $ability = new DiviPresetsDeleteAbility( + $writer, + new DesignChange(Mockery::mock(SnapshotStore::class), FakeChangeLog::forbidden()), + $usages, + static fn (): bool => true + ); + + $dry = self::assertToolError( + $ability->execute(['items' => [['module' => 'text', 'preset' => 'Fließtext']]]), + 'confirm_required' + ); + + self::assertSame(3, $dry['affected'][0]['post_id']); + self::assertSame(1, $dry['affected_count']); + self::assertContains('Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.', $dry['warnings']); + } + public function testUnknownPreset(): void { self::assertToolError(