feat: lit/divi-design-restore stellt theme_options wieder her; Hinweis auf Etappe 4d entfällt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
2e3b740260
commit
bba5a618ed
@@ -17,7 +17,7 @@ defined('ABSPATH') || exit;
|
||||
* lit/divi-design-restore (spec §5, G6, ruling V10): writes a snapshot back through the change pipeline – dry run with
|
||||
* the diff to the current state, confirm with design_rev, and first a snapshot of the current state, so a restore can
|
||||
* be undone as well. The area comes from the snapshot ID; its RestorePlanner brings the rights, the Divi features and
|
||||
* the plan. Areas without a planner (theme_options until Etappe 4d) are not restorable yet.
|
||||
* the plan. A snapshot of an area without a planner is refused as unsupported, never treated as a no-op.
|
||||
*/
|
||||
final class DiviDesignRestoreAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
@@ -51,8 +51,9 @@ final class DiviDesignRestoreAbility implements Ability, RequiresDiviFeatures
|
||||
return 'Stellt einen Snapshot des Designsystems wieder her (snapshot_id aus lit/divi-design-snapshots oder '
|
||||
. 'einer Änderungsantwort). Zuerst Probelauf (Unterschied zum aktuellen Stand, design_rev), nach Rückfrage '
|
||||
. 'beim Nutzer confirm: true; vorher wird der aktuelle Stand selbst gesichert. Bereiche: presets, colors '
|
||||
. '(Farben, die es im Snapshot nicht gibt, entfallen) und variables (Variablen, die erst nach dem Snapshot '
|
||||
. 'entstanden, bleiben archiviert erhalten).';
|
||||
. '(Farben, die es im Snapshot nicht gibt, entfallen), variables (Variablen, die erst nach dem Snapshot '
|
||||
. 'entstanden, bleiben archiviert erhalten) und theme_options (Theme-Optionen, die im Snapshot nicht '
|
||||
. 'gesetzt sind, kehren zum Divi-Standard zurück).';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
@@ -111,8 +112,8 @@ final class DiviDesignRestoreAbility implements Ability, RequiresDiviFeatures
|
||||
$planner = $this->planners[$area] ?? null;
|
||||
if ($planner === null) {
|
||||
return Errors::unsupported(
|
||||
'Snapshots dieses Bereichs lassen sich noch nicht wiederherstellen.',
|
||||
'Theme-Optionen folgen mit Etappe 4d.'
|
||||
'Snapshots dieses Bereichs lassen sich hier nicht wiederherstellen.',
|
||||
'Den Stand des Snapshots im Divi-Builder bzw. -Customizer von Hand herstellen.'
|
||||
);
|
||||
}
|
||||
$snapshot = $this->snapshots->get($area, $id);
|
||||
|
||||
@@ -14,6 +14,9 @@ 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\ThemeOptionBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
@@ -118,7 +121,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
||||
self::assertToolError($ability->execute($input), 'forbidden');
|
||||
}
|
||||
|
||||
public function testUnknownSnapshotAndThemeOptions(): void
|
||||
public function testUnknownSnapshots(): void
|
||||
{
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->allows('get')->andReturn(null);
|
||||
@@ -127,36 +130,71 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
||||
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(
|
||||
self::assertToolError(
|
||||
$ability->execute(['snapshot_id' => 'theme_options-20261016T100000Z-abcdef']),
|
||||
'unsupported'
|
||||
'not_found'
|
||||
);
|
||||
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. */
|
||||
/** A snapshot area whose planner is not wired stays unsupported (W11) – never a silent no-op. */
|
||||
public function testAnAreaWithoutAPlannerIsUnsupported(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->shouldNotReceive('get');
|
||||
$ability = new DiviDesignRestoreAbility(
|
||||
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
||||
$snapshots,
|
||||
new PresetRestorePlanner($writer)
|
||||
);
|
||||
|
||||
$error = self::assertToolError(
|
||||
$ability->execute(['snapshot_id' => ThemeOptionsRestorePlannerTest::SNAPSHOT_ID]),
|
||||
'unsupported'
|
||||
);
|
||||
self::assertStringNotContainsString('4d', $error['hint']);
|
||||
}
|
||||
|
||||
/** The ability with the planners of all four areas; the user has every right unless a store says otherwise. */
|
||||
private static function allAreas(
|
||||
SnapshotStore $snapshots,
|
||||
GlobalColorsAdapter $colors,
|
||||
GlobalVariablesAdapter $variables,
|
||||
?FakeChangeLog $log = null
|
||||
?FakeChangeLog $log = null,
|
||||
?ThemeOptionsAdapter $options = null
|
||||
): DiviDesignRestoreAbility {
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$writer->allows('presets')->andReturn(PresetBookTest::stored());
|
||||
$writer->shouldNotReceive('savePresets');
|
||||
$noOptions = [];
|
||||
|
||||
return new DiviDesignRestoreAbility(
|
||||
new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()),
|
||||
$snapshots,
|
||||
new PresetRestorePlanner($writer),
|
||||
new ColorRestorePlanner($writer, $colors),
|
||||
new VariableRestorePlanner($writer, $variables)
|
||||
new VariableRestorePlanner($writer, $variables),
|
||||
new ThemeOptionsRestorePlanner($writer, $options ?? self::optionStore($noOptions))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A theme options adapter that reads what it last wrote, starting with $stored (ThemeOptionsAreaTest::adapter()).
|
||||
*
|
||||
* @param array<string, int|float|string|false> $stored
|
||||
*/
|
||||
private static function optionStore(array &$stored, bool $customize = true): ThemeOptionsAdapter
|
||||
{
|
||||
$writes = [];
|
||||
|
||||
return ThemeOptionsAreaTest::adapter($stored, $writes, null, $customize);
|
||||
}
|
||||
|
||||
/**
|
||||
* A colors adapter that reads what it last saved, starting with $stored.
|
||||
*
|
||||
@@ -247,12 +285,54 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
||||
self::assertSame('archived', $stored['numbers']['gvid-neu0000001']['status']);
|
||||
}
|
||||
|
||||
public function testRestoresThemeOptionsAndResetsTheKeysTheSnapshotLacks(): void
|
||||
{
|
||||
$id = ThemeOptionsRestorePlannerTest::SNAPSHOT_ID;
|
||||
$stored = ThemeOptionsRestorePlannerTest::current();
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->allows('get')->with('theme_options', $id)
|
||||
->andReturn(self::snapshotIn('theme_options', $id, ThemeOptionsRestorePlannerTest::snapshot()));
|
||||
$snapshots->expects('save')
|
||||
->with(
|
||||
'theme_options',
|
||||
ThemeOptionBook::fromDivi(ThemeOptionsRestorePlannerTest::current())->data(),
|
||||
'lit/divi-design-restore'
|
||||
)
|
||||
->andReturn('theme_options-5');
|
||||
$snapshots->expects('attachLog');
|
||||
$ability = self::allAreas(
|
||||
$snapshots,
|
||||
self::colorStore([]),
|
||||
self::variableStore([]),
|
||||
new FakeChangeLog(8),
|
||||
self::optionStore($stored)
|
||||
);
|
||||
|
||||
$dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required');
|
||||
self::assertSame(['added', 'changed', 'reset'], array_column($dry['diff'], 'action'));
|
||||
self::assertStringContainsString('Divi-Standard', $dry['warnings'][1]);
|
||||
|
||||
$result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]);
|
||||
self::assertSame('theme_options-5', $result['snapshot_id']);
|
||||
self::assertSame(
|
||||
ThemeOptionBook::fromDivi(ThemeOptionsRestorePlannerTest::snapshot())->data(),
|
||||
ThemeOptionBook::fromDivi($stored)->data()
|
||||
);
|
||||
}
|
||||
|
||||
/** 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));
|
||||
$options = [];
|
||||
$ability = self::allAreas(
|
||||
$snapshots,
|
||||
self::colorStore([]),
|
||||
self::variableStore([], false),
|
||||
null,
|
||||
self::optionStore($options, false)
|
||||
);
|
||||
|
||||
self::assertToolError(
|
||||
$ability->permission(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]),
|
||||
@@ -262,6 +342,10 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
||||
$ability->execute(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]),
|
||||
'forbidden'
|
||||
);
|
||||
self::assertToolError(
|
||||
$ability->permission(['snapshot_id' => ThemeOptionsRestorePlannerTest::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');
|
||||
@@ -284,7 +368,12 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
||||
$ability->diviFeatures(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa'])
|
||||
);
|
||||
self::assertSame(
|
||||
[DiviFeature::DesignRead, DiviFeature::PresetWrite, DiviFeature::ColorWrite, DiviFeature::VariableWrite],
|
||||
[DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite],
|
||||
$ability->diviFeatures(['snapshot_id' => ThemeOptionsRestorePlannerTest::SNAPSHOT_ID])
|
||||
);
|
||||
self::assertSame(
|
||||
[DiviFeature::DesignRead, DiviFeature::PresetWrite, DiviFeature::ColorWrite, DiviFeature::VariableWrite,
|
||||
DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite],
|
||||
$ability->diviFeatures([])
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user