feat: lit/divi-colors-set löst {{color:…}} mit relative gegen den geplanten Bestand auf, lehnt Zyklen ab und warnt vor Verwendungen in Presets und relativen Farben

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 14:07:54 +02:00
co-authored by Claude Sonnet 5.5
parent 337b87b572
commit 2fd03f3900
2 changed files with 259 additions and 11 deletions
@@ -7,8 +7,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\ColorBook;
use LIT\MCP\Integrations\Divi\Design\ColorDiff;
use LIT\MCP\Integrations\Divi\Design\ColorValue;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviColorsSetAbility;
@@ -300,4 +302,140 @@ final class DiviColorsSetAbilityTest extends TestCase
}
self::assertSame([false, true], $calls, 'the merge failed, the snapshot replaced all colors');
}
/** Ruling V5: {{color:…}} with relative is stored as Divi reference on the named color's ID. */
public function testAPlaceholderBecomesARelativeColorOfTheNamedColor(): void
{
$ability = $this->ability(null, self::snapshots());
$input = ['items' => [
['label' => 'Holz hell', 'color' => '{{color:Holz dunkel}}', 'relative' => ['lightness' => 30]],
]];
$dry = self::assertToolError($ability->execute($input), 'confirm_required');
$ability->execute($input + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
$created = array_values(array_diff_key($this->stored, ColorAreaTest::stored()));
self::assertSame(
VariableRef::serialize('color', 'gcid-holz000001', ColorValue::relative(['lightness' => 30])),
$created[0]['color']
);
}
/** Ruling V5: the placeholder is resolved against the planned inventory, also colors of the same call. */
public function testAPlaceholderMayNameAColorOfTheSameCall(): void
{
$ability = $this->ability(null, self::snapshots());
$input = ['items' => [
['label' => 'Basis', 'color' => '#112233'],
['label' => 'Basis hell', 'color' => '{{color:basis}}', 'relative' => ['lightness' => 20]],
]];
$dry = self::assertToolError($ability->execute($input), 'confirm_required');
self::assertSame(['added', 'added'], array_column($dry['diff'], 'action'));
$ability->execute($input + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
$byLabel = [];
foreach ($this->stored as $id => $entry) {
$byLabel[$entry['label']] = ['id' => $id] + $entry;
}
self::assertSame(
VariableRef::serialize('color', $byLabel['Basis']['id'], ColorValue::relative(['lightness' => 20])),
$byLabel['Basis hell']['color']
);
}
public function testPlaceholderProblemsAreCollected(): void
{
$error = $this->ability()->execute(['items' => [
['label' => 'A', 'color' => '{{color:Fehlt}}', 'relative' => ['lightness' => 10]],
['label' => 'B', 'color' => '#ffffff', 'relative' => ['lightness' => 10]],
['id' => 'primary', 'color' => '{{color:Holz dunkel}}', 'relative' => ['lightness' => 10]],
['label' => 'C', 'color' => '{{color:Holz dunkel}}', 'relative' => ['farbe' => 10]],
['id' => 'Sand', 'relative' => ['lightness' => 10]],
['label' => 'D', 'color' => VariableRef::serialize('color', 'gcid-holz000001', [])],
['label' => 'E', 'color' => '{{color:Holz dunkel}}'],
]]);
self::assertToolError($error, 'validation_failed');
self::assertSame(
['items[0].color', 'items[1].relative', 'items[2].color', 'items[3].relative', 'items[4].relative',
'items[5].color', 'items[6].relative'],
array_keys(self::errorFields($error))
);
}
/** Ruling V5: Sand is built on Holz dunkel – Holz dunkel built on Sand would be a cycle. */
public function testRelativeColorsMustNotFormACycle(): void
{
$error = $this->ability()->execute(['items' => [
['id' => 'Holz dunkel', 'color' => '{{color:Sand}}', 'relative' => ['lightness' => -40]],
]]);
self::assertToolError($error, 'validation_failed');
$fields = self::errorFields($error);
self::assertSame(['items'], array_keys($fields));
self::assertStringContainsString('Sand', $fields['items']);
self::assertSame([], $this->saves);
}
/** Ruling V8: presets and relative colors built on a changed color are warnings, never a block. */
public function testChangesWarnAboutPresetsAndRelativeColorsBuiltOnTheColor(): void
{
$dry = self::assertToolError(
$this->ability(null, null, PresetBookTest::storedWithReferences())
->execute(['items' => [['id' => 'Holz dunkel', 'color' => '#000000']]]),
'confirm_required'
);
self::assertSame([
'Die Änderung an Farbe „Holz dunkel“ wirkt auch auf die Presets CTA primär.',
'Die relativen Farben Sand bauen auf Farbe „Holz dunkel“ auf und ändern sich mit.',
], $dry['warnings']);
}
public function testDeactivatingAUsedColorWarnsAndListsTheUsages(): void
{
$dry = self::assertToolError(
$this->ability(self::usages(self::used()), null, PresetBookTest::storedWithReferences())
->execute(['items' => [['id' => 'Holz dunkel', 'status' => 'inactive']]]),
'confirm_required'
);
self::assertSame(7, $dry['affected'][0]['post_id']);
self::assertSame([
'Farbe „Holz dunkel“ wird deaktiviert, aber noch von Presets verwendet: CTA primär.',
'Farbe „Holz dunkel“ wird deaktiviert, aber die relativen Farben Sand bauen auf ihr auf.',
'Die unter affected genannten Inhalte verweisen auf geänderte oder deaktivierte Farben; deaktivierte '
. 'Farben bleiben dort als Verweis stehen.',
], $dry['warnings']);
}
public function testNewColorsHaveNoWarnings(): void
{
$dry = self::assertToolError(
$this->ability(null, null, PresetBookTest::storedWithReferences())
->execute(['items' => [['label' => 'Weiß', 'color' => 'white']]]),
'confirm_required'
);
self::assertSame([], $dry['warnings']);
}
/** Review note: ColorBook::with() replaces, so a changed color keeps usedInPosts and its other fields. */
public function testChangingAColorKeepsItsUsedInPostsAndOtherFields(): void
{
$this->stored['gcid-holz000001']['usedInPosts'] = [7, 9];
$ability = $this->ability();
$input = ['items' => [['id' => 'Holz dunkel', 'color' => '#000000']]];
$dry = self::assertToolError($ability->execute($input), 'confirm_required');
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('save')->andReturn('colors-1');
$snapshots->allows('attachLog');
$this->ability(null, $snapshots)->execute($input + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
$saved = $this->saves[0][0]['gcid-holz000001'];
self::assertSame([7, 9], $saved['usedInPosts']);
self::assertSame(['Holz dunkel', '#000000'], [$saved['label'], $saved['color']]);
}
}