feat: lit/divi-design-restore stellt Farben und Variablen wieder her; nur theme_options folgt mit 4d (V10)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 14:23:10 +02:00
co-authored by Claude Sonnet 5.5
parent bd2043564b
commit 88a939787d
4 changed files with 195 additions and 16 deletions
@@ -5,14 +5,21 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Design\ColorBook;
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviDesignRestoreAbility;
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
use LIT\MCP\Integrations\Divi\Design\PresetRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
final class DiviDesignRestoreAbilityTest extends TestCase
{
@@ -111,21 +118,174 @@ final class DiviDesignRestoreAbilityTest extends TestCase
self::assertToolError($ability->execute($input), 'forbidden');
}
public function testUnknownSnapshotAndOtherAreas(): void
public function testUnknownSnapshotAndThemeOptions(): void
{
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('get')->andReturn(null);
$ability = self::allAreas($snapshots, self::colorStore([]), self::variableStore([]));
self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-abcdef']), 'not_found');
self::assertToolError($ability->execute(['snapshot_id' => 'colors-20261016T100000Z-abcdef']), 'not_found');
self::assertToolError($ability->execute(['snapshot_id' => 'variables-20261016T100000Z-abcdef']), 'not_found');
$themeOptions = self::assertToolError(
$ability->execute(['snapshot_id' => 'theme_options-20261016T100000Z-abcdef']),
'unsupported'
);
self::assertSame('Theme-Optionen folgen mit Etappe 4d.', $themeOptions['hint']);
self::assertToolError($ability->execute(['snapshot_id' => 'kaputt']), 'validation_failed');
}
/** The ability with the planners of all three areas; the user has every right unless $manage is false. */
private static function allAreas(
SnapshotStore $snapshots,
GlobalColorsAdapter $colors,
GlobalVariablesAdapter $variables,
?FakeChangeLog $log = null
): DiviDesignRestoreAbility {
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('get')->andReturn(null);
$ability = new DiviDesignRestoreAbility(
new DesignChange($snapshots, FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer)
);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->shouldNotReceive('savePresets');
self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-abcdef']), 'not_found');
self::assertToolError($ability->execute(['snapshot_id' => 'colors-20261016T100000Z-abcdef']), 'unsupported');
self::assertToolError($ability->execute(['snapshot_id' => 'kaputt']), 'validation_failed');
return new DiviDesignRestoreAbility(
new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer),
new ColorRestorePlanner($writer, $colors),
new VariableRestorePlanner($writer, $variables)
);
}
/**
* A colors adapter that reads what it last saved, starting with $stored.
*
* @param array<string, array<string, mixed>> $stored
*/
private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
$colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$stored): void {
self::assertTrue($replace, 'a restore replaces every color (V4)');
$stored = $entries;
});
return $colors;
}
/**
* A variables adapter that reads what it last saved, starting with $stored.
*
* @param array<string, array<string, array<string, mixed>>> $stored
*/
private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
$variables->allows('save')->andReturnUsing(static function (array $data) use (&$stored): void {
$stored = $data;
});
return $variables;
}
/** @param array<array-key, mixed> $data */
private static function snapshotIn(string $area, string $id, array $data): array
{
return ['id' => $id, 'data' => $data, 'created' => 'x', 'user_id' => 1, 'ability' => 'lit/divi-' . $area . '-set',
'log_id' => 2, 'size' => 1];
}
public function testRestoresColorsWithDryRunAndASnapshotOfTheCurrentColors(): void
{
$id = ColorRestorePlannerTest::SNAPSHOT_ID;
$colors = self::colorStore(ColorRestorePlannerTest::current());
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('get')->with('colors', $id)
->andReturn(self::snapshotIn('colors', $id, ColorRestorePlannerTest::snapshot()));
$snapshots->expects('save')
->with('colors', ColorBook::fromDivi(ColorRestorePlannerTest::current())->data(), 'lit/divi-design-restore')
->andReturn('colors-5');
$snapshots->expects('attachLog');
$ability = self::allAreas($snapshots, $colors, self::variableStore([]), new FakeChangeLog(8));
$dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required');
self::assertSame(['changed', 'removed'], array_column($dry['diff'], 'action'));
self::assertStringContainsString('„Akzent“', $dry['warnings'][0]);
$result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]);
self::assertSame('colors-5', $result['snapshot_id']);
self::assertSame(ColorBook::fromDivi(ColorRestorePlannerTest::snapshot())->data(), $colors->read());
}
public function testRestoresVariablesAndKeepsNewerOnesArchived(): void
{
$id = VariableRestorePlannerTest::SNAPSHOT_ID;
$variables = self::variableStore(VariableRestorePlannerTest::current());
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('get')->with('variables', $id)
->andReturn(self::snapshotIn('variables', $id, VariableRestorePlannerTest::snapshot()));
$snapshots->expects('save')
->with('variables', Mockery::type('array'), 'lit/divi-design-restore')
->andReturn('variables-5');
$snapshots->expects('attachLog');
$ability = self::allAreas($snapshots, self::colorStore([]), $variables, new FakeChangeLog(8));
$dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required');
self::assertStringContainsString('„Neu“', $dry['warnings'][0]);
$result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]);
self::assertSame('variables-5', $result['snapshot_id']);
$stored = $variables->read();
self::assertSame('4px', $stored['numbers']['gvid-radius0001']['value']);
self::assertSame('archived', $stored['numbers']['gvid-neu0000001']['status']);
}
/** Spec §10, V10: the rights are those of the snapshot's area. */
public function testRightsFollowTheSnapshotArea(): void
{
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->shouldNotReceive('get');
$ability = self::allAreas($snapshots, self::colorStore([]), self::variableStore([], false));
self::assertToolError(
$ability->permission(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]),
'forbidden'
);
self::assertToolError(
$ability->execute(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]),
'forbidden'
);
self::assertTrue($ability->permission(['snapshot_id' => ColorRestorePlannerTest::SNAPSHOT_ID]));
self::assertTrue($ability->permission(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']));
self::assertToolError($ability->permission(['snapshot_id' => 'kaputt']), 'forbidden', 'fail closed');
}
public function testFeaturesFollowTheSnapshotArea(): void
{
$ability = self::allAreas(Mockery::mock(SnapshotStore::class), self::colorStore([]), self::variableStore([]));
self::assertSame(
[DiviFeature::DesignRead, DiviFeature::ColorWrite],
$ability->diviFeatures(['snapshot_id' => ColorRestorePlannerTest::SNAPSHOT_ID])
);
self::assertSame(
[DiviFeature::DesignRead, DiviFeature::VariableWrite],
$ability->diviFeatures(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID])
);
self::assertSame(
[DiviFeature::DesignRead, DiviFeature::PresetWrite],
$ability->diviFeatures(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa'])
);
self::assertSame(
[DiviFeature::DesignRead, DiviFeature::PresetWrite, DiviFeature::ColorWrite, DiviFeature::VariableWrite],
$ability->diviFeatures([])
);
}
}