From fc8ffeba9ed25c30b1ebf3600c61fefff3f12183 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Fri, 16 Oct 2026 18:04:34 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20DesignArea=20mit=20restore()=20f=C3=BCrs?= =?UTF-8?q?=20Zur=C3=BCckschreiben=20und=20differences()=20f=C3=BCr=20bere?= =?UTF-8?q?inigte=20Werte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/Integrations/Divi/Design/DesignArea.php | 23 ++++++- src/Integrations/Divi/Design/DesignChange.php | 4 +- src/Integrations/Divi/Design/PresetArea.php | 22 ++++++- .../Divi/Design/DesignChangeTest.php | 65 +++++++++++++++++-- .../Divi/Design/PresetAreaTest.php | 28 ++++++++ 5 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/Integrations/Divi/Design/DesignArea.php b/src/Integrations/Divi/Design/DesignArea.php index 9923e56..0a363cd 100644 --- a/src/Integrations/Divi/Design/DesignArea.php +++ b/src/Integrations/Divi/Design/DesignArea.php @@ -9,7 +9,8 @@ defined('ABSPATH') || exit; /** * One area of the design system for DesignChange (spec §2, §4): colors, variables, presets or theme options – its * raw inventory as Divi stores it (also the snapshot), its design_rev, how it is written through Divi and how a - * read-back is judged. write() throws on failure; it is also how a snapshot is written back ($mayRemove = true). + * read-back is judged. write() applies a planned change, restore() writes a snapshot back (full replace); both throw + * on failure. verify() judges the structure (entries, defaults), differences() the values Divi changed. */ interface DesignArea { @@ -32,10 +33,28 @@ interface DesignArea public function write(array $raw, bool $mayRemove): array; /** - * Whether Divi stored what was written – sanitizing may change values, never which entries exist. + * Writes $raw back as the whole inventory (full replace: entries missing in $raw are removed). + * + * @param array $raw A snapshot. + * @return array The inventory as read back after writing. + */ + public function restore(array $raw): array; + + /** + * Whether Divi stored the entries and defaults that were written – sanitizing may change values, never which + * entries exist. * * @param array $expected * @param array $actual */ public function verify(array $expected, array $actual): bool; + + /** + * German warnings for entries whose values Divi changed while saving (e.g. sanitized settings). + * + * @param array $expected + * @param array $actual + * @return list + */ + public function differences(array $expected, array $actual): array; } diff --git a/src/Integrations/Divi/Design/DesignChange.php b/src/Integrations/Divi/Design/DesignChange.php index c08770c..73122a8 100644 --- a/src/Integrations/Divi/Design/DesignChange.php +++ b/src/Integrations/Divi/Design/DesignChange.php @@ -136,7 +136,7 @@ final class DesignChange return ToolResult::ok($planned->summary) ->snapshotId($snapshotId) ->logId($logId) - ->warnings($planned->warnings) + ->warnings(array_merge($planned->warnings, $area->differences($planned->raw, $saved))) ->with('diff', $planned->diff) ->with('design_rev', $area->rev($saved)) ->toArray(); @@ -203,7 +203,7 @@ final class DesignChange Throwable $cause ): never { $reason = sprintf('Die %s konnten nicht gespeichert werden.', $area->label()); - $failures = Rollback::attempt(static fn () => $area->write($current, true)); + $failures = Rollback::attempt(static fn () => $area->restore($current)); if ($failures === []) { throw WriteFailed::rolledBack( $reason, diff --git a/src/Integrations/Divi/Design/PresetArea.php b/src/Integrations/Divi/Design/PresetArea.php index edabcce..7b15639 100644 --- a/src/Integrations/Divi/Design/PresetArea.php +++ b/src/Integrations/Divi/Design/PresetArea.php @@ -9,7 +9,7 @@ defined('ABSPATH') || exit; /** * The area "presets" (spec §6): the inventory in Divi's stored format (PresetBook), written as REST format through * DesignSystemWriter::savePresets(). Divi's prepare_data() may sanitize values; verify() only insists on the same - * presets and defaults (PresetDiff without "changed"). + * presets and defaults (PresetDiff without "changed"), differences() names the presets whose values changed. */ final class PresetArea implements DesignArea { @@ -43,6 +43,11 @@ final class PresetArea implements DesignArea ->data(); } + public function restore(array $raw): array + { + return $this->write($raw, true); + } + public function verify(array $expected, array $actual): bool { $structural = array_filter( @@ -52,4 +57,19 @@ final class PresetArea implements DesignArea return $structural === []; } + + public function differences(array $expected, array $actual): array + { + $warnings = []; + foreach (PresetDiff::between(PresetBook::fromDivi($expected), PresetBook::fromDivi($actual)) as $row) { + if ($row['action'] === 'changed') { + $warnings[] = sprintf( + 'Divi hat die Einstellungen von Preset „%s“ beim Speichern bereinigt.', + $row['name'] + ); + } + } + + return $warnings; + } } diff --git a/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php b/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php index 8ebf0c7..0ec9ae1 100644 --- a/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php +++ b/tests/Unit/Integrations/Divi/Design/DesignChangeTest.php @@ -22,14 +22,27 @@ use RuntimeException; final class DesignChangeTest extends TestCase { - /** A DesignArea over an array; $failWrite/$failRestore make write()/the rollback throw. */ - private static function area(array &$stored, int $failWrites = 0, bool $verify = true): DesignArea - { - return new class ($stored, $failWrites, $verify) implements DesignArea { + /** + * A DesignArea over an array; the first $failWrites calls of write()/restore() throw, $differences are the + * value warnings of differences(). + */ + private static function area( + array &$stored, + int $failWrites = 0, + bool $verify = true, + array $differences = [] + ): DesignArea { + return new class ($stored, $failWrites, $verify, $differences) implements DesignArea { public array $writes = []; - public function __construct(private array &$stored, private int $failWrites, private bool $verify) - { + public array $restores = []; + + public function __construct( + private array &$stored, + private int $failWrites, + private bool $verify, + private array $differences + ) { } public function name(): string @@ -64,10 +77,27 @@ final class DesignChangeTest extends TestCase return $raw; } + public function restore(array $raw): array + { + $this->restores[] = $raw; + if ($this->failWrites > 0) { + --$this->failWrites; + throw new RuntimeException('divi refused the restore'); + } + $this->stored = $raw; + + return $raw; + } + public function verify(array $expected, array $actual): bool { return $this->verify; } + + public function differences(array $expected, array $actual): array + { + return $this->differences; + } }; } @@ -178,6 +208,26 @@ final class DesignChangeTest extends TestCase self::assertSame('presets-1', $log->records[0]['snapshot_id']); } + /** Final review I-3b: values Divi changed while saving are reported as warnings. */ + public function testValuesDiviChangedWhileSavingAreWarnings(): void + { + $stored = ['v' => 1]; + $area = self::area($stored, 0, true, ['Divi hat die Einstellungen von Preset „CTA“ beim Speichern bereinigt.']); + $snapshots = Mockery::mock(SnapshotStore::class); + $snapshots->expects('save')->andReturn('presets-1'); + $snapshots->expects('attachLog'); + + $result = (new DesignChange($snapshots, new FakeChangeLog(9)))->run( + 'lit/divi-presets-set', + $area, + ['confirm' => true, 'design_rev' => $area->rev($stored)], + self::plan(['v' => 2]) + ); + + self::assertTrue($result['ok']); + self::assertContains('Divi hat die Einstellungen von Preset „CTA“ beim Speichern bereinigt.', $result['warnings']); + } + public function testLockedDiviWritesNoSnapshot(): void { ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.16.0')); @@ -234,7 +284,8 @@ final class DesignChangeTest extends TestCase self::assertSame('presets-1', $e->details['snapshot_id']); } - self::assertSame([[['v' => 2], false], [['v' => 1], true]], $area->writes); + self::assertSame([[['v' => 2], false]], $area->writes); + self::assertSame([['v' => 1]], $area->restores, 'the rollback writes the snapshot back with restore()'); self::assertSame(['v' => 1], $stored); } diff --git a/tests/Unit/Integrations/Divi/Design/PresetAreaTest.php b/tests/Unit/Integrations/Divi/Design/PresetAreaTest.php index 7ff765f..c97b68d 100644 --- a/tests/Unit/Integrations/Divi/Design/PresetAreaTest.php +++ b/tests/Unit/Integrations/Divi/Design/PresetAreaTest.php @@ -43,4 +43,32 @@ final class PresetAreaTest extends TestCase self::assertFalse($area->verify($expected, $missing)); self::assertFalse($area->verify($expected, $otherDefault)); } + + /** Final review I-3a: a snapshot is written back with full-replace semantics. */ + public function testRestoreReplacesTheWholeInventory(): void + { + $writer = Mockery::mock(DesignSystemWriter::class); + $stored = PresetBookTest::stored(); + $writer->expects('savePresets')->with(PresetBook::fromDivi($stored)->toRest(), true)->andReturn($stored); + + self::assertSame(PresetBook::fromDivi($stored)->data(), (new PresetArea($writer))->restore($stored)); + } + + /** Final review I-3b: values Divi changed while saving, by preset name; structure is verify()'s job. */ + public function testDifferencesNameThePresetsDiviSanitized(): void + { + $area = new PresetArea(Mockery::mock(DesignSystemWriter::class)); + $expected = PresetBookTest::stored(); + $sanitized = $expected; + $sanitized['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = ['button' => ['x' => 'bereinigt']]; + $missing = $expected; + unset($missing['module']['divi/button']['items']['bbbbbbbbbb']); + + self::assertSame([], $area->differences($expected, $expected)); + self::assertSame([], $area->differences($expected, $missing)); + self::assertSame( + ['Divi hat die Einstellungen von Preset „CTA primär“ beim Speichern bereinigt.'], + $area->differences($expected, $sanitized) + ); + } }