feat: Divi save pipeline – tree_rev lock, kses-safe save, revision, flags, cache, ToolResult

DiviSaver/DiviWriter/DiviComposition/DiviChange, WpValueSanitizer (S4),
WpMediaResolver, DiviFlags (S11), DynamicContentMap from Divi's live
registry (S22), S31 wiring, S35 control strip at apply() entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 14:56:47 +02:00
co-authored by Claude Opus 5.5
parent ea1fd607b2
commit 7762b76305
20 changed files with 1716 additions and 2 deletions
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\Document;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class DiviCompositionTest extends TestCase
{
/** @var list<int> */
private array $registryCalls = [];
private function composition(): DiviComposition
{
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$registry = new DynamicContentMap(function (int $postId): array {
$this->registryCalls[] = $postId;
return DynamicContentMapTest::registry();
});
return new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $registry);
}
private static function siteTitle(): string
{
return VariableRef::serialize('content', 'site_title', ['before' => '', 'after' => '']);
}
public function testOneLiveMapIsSharedByTheFullAndTheShortForm(): void
{
$composition = $this->composition()->forPost(7);
// Full form (AttrWriter).
$full = $composition->attrWriter()->apply(
'divi/text',
[],
['content.innerContent' => ['desktop' => self::siteTitle()]],
'attrs'
);
self::assertSame([], $full['errors']);
self::assertSame(self::siteTitle(), AttrMap::desktop($full['attrs'], 'content.innerContent'));
// Short form through the Composer (ShortformWriter) – same map, same AttrWriter.
$short = $composition->composer()->compose(
[['type' => 'button', 'text' => self::siteTitle()]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $short['errors']);
self::assertStringContainsString('site_title', json_encode($short['blocks'][0]->attrs(), JSON_THROW_ON_ERROR));
self::assertSame([7], $this->registryCalls, 'the registry is read once per post');
self::assertSame($composition->attrWriter(), $composition->attrWriter());
self::assertSame($composition->placeholders(), $composition->placeholders());
}
public function testWithoutAPostTheDefaultMapApplies(): void
{
$composition = $this->composition();
$result = $composition->attrWriter()->apply(
'divi/text',
[],
['content.innerContent' => ['desktop' => self::siteTitle()]],
'attrs'
);
self::assertStringContainsString('Unbekannter Name "site_title"', $result['errors']['attrs.content.innerContent']);
self::assertSame([], $this->registryCalls);
}
public function testForPostIsAFreshCompositionPerPost(): void
{
$base = $this->composition();
$seven = $base->forPost(7);
$eight = $base->forPost(8);
self::assertNotSame($base, $seven);
self::assertNotSame($seven->placeholders(), $eight->placeholders());
self::assertNotSame($base->placeholders(), $seven->placeholders());
self::assertSame([7, 8], $this->registryCalls);
}
public function testRulesEditorAndWarnings(): void
{
$composition = $this->composition();
self::assertNull($composition->rules()->violation(Block::create('divi/column'), 'divi/button'));
self::assertNotNull($composition->rules()->violation(null, 'divi/button'));
self::assertSame([], $composition->warnings()->collect(NodeIndex::build(new Document()), []));
self::assertInstanceOf(TreeEditor::class, $composition->editor());
}
}