test: split DiviWriteAbilitiesTest per tool (T5)

One test class per Divi write tool on a shared DiviWriteAbilityTestCase;
the 30 tests move verbatim (per-test assertion counts identical). Adds the
ledger T4 test for a TreeEditor::move() StructureRules refusal.

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-26 18:27:59 +02:00
co-authored by Claude Opus 5.5
parent afa19ffcd8
commit 7963aa19bd
9 changed files with 746 additions and 653 deletions
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Tests\Support\DiviFixtures;
final class DiviMoveAbilityTest extends DiviWriteAbilityTestCase
{
/** Ruling S14: move there and back restores the exact bytes (full-content assertSame, no strlen). */
public function testMoveThereAndBackIsByteExact(): void
{
$moved = $this->move()->execute([
'post_id' => 7, 'tree_rev' => $this->treeRev(), 'id' => 's0.r0.c1.m2', 'parent' => 's1.r0.c0',
]);
self::assertSame([['id' => 's1.r0.c0.m1', 'type' => 'divi/button', 'action' => 'moved', 'label' => 'Hero – Button']], $moved['changed']);
self::assertStringContainsString('Leistungen', (string) $moved['outline'], 'the outline is embedded in the result');
$back = $this->move()->execute([
'post_id' => 7, 'tree_rev' => Rev::of($this->saved[0]), 'id' => 's1.r0.c0.m1', 'parent' => 's0.r0.c1',
]);
self::assertSame([['id' => 's0.r0.c1.m2', 'type' => 'divi/button', 'action' => 'moved', 'label' => 'Hero – Button']], $back['changed']);
self::assertSame($this->original, $this->saved[1], 'move there and back restores the exact bytes (S14)');
}
/** StructureRules apply on move: a module directly into a section, or into a specialty column. */
public function testMoveRefusedByStructureRules(): void
{
$mixed = DiviFixtures::markup('synthetic-mixed.html');
$this->post->post_content = $mixed;
$treeRev = Rev::of($mixed);
// s1 is a regular section: it holds rows, not modules directly.
$intoSection = $this->move()->execute([
'post_id' => 7, 'tree_rev' => $treeRev, 'id' => 's0.c0.m0', 'parent' => 's1',
]);
self::assertToolError($intoSection, 'validation_failed');
self::assertStringContainsString('Zeilen (row)', self::clientError($intoSection)['fields']['position']);
// s0.c1 is a specialty column (specialtyColumns set, holds a row-inner): refuses every child.
$intoSpecialtyColumn = $this->move()->execute([
'post_id' => 7, 'tree_rev' => $treeRev, 'id' => 's0.c0.m0', 'parent' => 's0.c1',
]);
self::assertToolError($intoSpecialtyColumn, 'validation_failed');
self::assertStringContainsString('Spezialspalten', self::clientError($intoSpecialtyColumn)['fields']['position']);
self::assertSame([], $this->saved);
self::assertSame($mixed, $this->post->post_content);
}
public function testMoveIntoItsOwnDescendantIsRefused(): void
{
$result = $this->move()->execute([
'post_id' => 7, 'tree_rev' => $this->treeRev(), 'id' => 's0', 'parent' => 's0.r0.c1',
]);
self::assertToolError($result, 'validation_failed');
self::assertStringContainsString(
'nicht in sich selbst verschoben',
self::clientError($result)['fields']['position']
);
self::assertSame([], $this->saved);
}
public function testMoveRefusesAStaleTreeRev(): void
{
$result = $this->move()->execute([
'post_id' => 7, 'tree_rev' => '000000000000', 'id' => 's0.r0.c1.m2', 'parent' => 's1.r0.c0',
]);
self::assertToolError($result, 'rev_mismatch');
self::assertSame('rev_mismatch', self::clientError($result)['code'], 'the client sees the code without lit_');
self::assertSame([], $this->saved);
}
}