feat: design-system filters and global module ids in node-get/outline (S19)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-27 00:44:58 +02:00
co-authored by Claude Opus 5.5
parent 858aa6a182
commit a3a807051f
8 changed files with 187 additions and 19 deletions
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase;
use stdClass;
@@ -26,7 +27,7 @@ final class OutlineTest extends TestCase
self::assertSame(
['id' => 's0.r0.c1.m0', 'type' => 'heading', 'label' => '', 'excerpt' => 'Willkommen bei Müller & Söhne',
'info' => '', 'presets' => ['Hero-Titel'], 'children' => 0, 'editable' => true, 'depth' => 3,
'truncated' => false],
'truncated' => false, 'global_id' => null],
$rows[5]
);
self::assertSame('Spalten 8_24+16_24', $rows[1]['info']);
@@ -40,6 +41,22 @@ final class OutlineTest extends TestCase
);
}
public function testGlobalInstancesShowTheirLibraryIdAndRootType(): void
{
$content = DiviFixtures::markup('stage3/page--globale-instanzen.html');
$outline = new Outline();
$rows = $outline->rows(NodeIndex::build((new Parser())->parse($content)));
$byId = array_column($rows, null, 'id');
self::assertSame('1058', $byId['g0']['global_id']);
self::assertSame('section', $byId['g0']['type']);
self::assertFalse($byId['g0']['editable']);
self::assertSame('1059', $byId['s0.r0.c0.g0']['global_id']);
self::assertSame('text', $byId['s0.r0.c0.g0']['type']);
self::assertNull($byId['s0']['global_id']);
self::assertStringContainsString('g0 section "Global Abschnitt" [global 1058]', $outline->text($rows));
}
public function testSubtreeAndDepthLimit(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
@@ -104,7 +121,7 @@ final class OutlineTest extends TestCase
$text = (new Outline())->text((new Outline())->rows($index));
self::assertStringContainsString('x0 core/paragraph (nur lesbar)', $text);
self::assertStringContainsString('s1.g1 row "Globale Zeile" (nur lesbar)', $text);
self::assertStringContainsString('s1.g1 row "Globale Zeile" [global 913] (nur lesbar)', $text);
self::assertStringContainsString('s0 section [specialty]', $text);
self::assertStringContainsString('s0.c1.r0 row-inner [Spalten 1_2+1_2]', $text);
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
use LIT\MCP\Tests\Support\DiviFixtures;
@@ -41,4 +42,38 @@ final class DiviDesignSystemAbilityTest extends TestCase
self::assertToolError($result, 'forbidden');
}
public function testFiltersPartsAndPresetsOfOneModule(): void
{
Functions\when('current_user_can')->justReturn(true);
$reader = Mockery::mock(DesignSystemReader::class);
$reader->allows('read')->andReturn(DiviFixtures::designSystem());
$ability = new DiviDesignSystemAbility($reader);
$result = $ability->execute(['only' => ['presets'], 'module' => 'button']);
self::assertSame([], $result['colors']);
self::assertSame([], $result['variables']);
self::assertSame(['p-1', 'p-2'], array_column($result['presets'], 'id'));
self::assertSame(['divi/button'], array_values(array_unique(array_column($result['presets'], 'module'))));
self::assertNotEmpty($result['fonts']['heading']);
self::assertContains('Nicht angefordert: colors, variables.', $result['notes']);
}
public function testCapsThePresetList(): void
{
Functions\when('current_user_can')->justReturn(true);
$items = [];
for ($i = 0; $i < 250; ++$i) {
$items['p' . $i] = ['id' => 'p' . $i, 'name' => 'Preset ' . $i, 'moduleName' => 'divi/text'];
}
$design = DesignSystem::fromDivi([], [], ['module' => ['divi/text' => ['default' => '', 'items' => $items]]], []);
$reader = Mockery::mock(DesignSystemReader::class);
$reader->allows('read')->andReturn($design);
$result = (new DiviDesignSystemAbility($reader))->execute([]);
self::assertCount(DiviDesignSystemAbility::PRESET_LIMIT, $result['presets']);
self::assertContains('200 von 250 Presets gezeigt; mit module auf ein Modul eingrenzen.', $result['notes']);
}
}
@@ -123,4 +123,19 @@ final class DiviReadAbilitiesTest extends TestCase
self::assertToolError($ability->execute(['post_id' => 7, 'id' => 'hero']), 'validation_failed');
self::assertToolError($ability->execute(['post_id' => 7, 'id' => 's5.r0']), 'not_found');
}
public function testNodeGetNamesTheLibraryEntryOfAGlobalModule(): void
{
DiviFixtures::givenPost(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
$global = $ability->execute(['post_id' => 7, 'id' => 'g0']);
$plain = $ability->execute(['post_id' => 7, 'id' => 's0']);
self::assertSame('1058', $global['global']['id']);
self::assertSame('divi/section', $global['global']['block_name']);
self::assertStringContainsString('post_id 1058', $global['global']['hint']);
self::assertFalse($global['editable']);
self::assertNull($plain['global']);
}
}