fix: tb-create-template rolls back posts Divi inserted before failing (ruling Q19)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 16:23:01 +02:00
co-authored by Claude Opus 5.5
parent 42b9b384d5
commit 2827fd4bd8
7 changed files with 406 additions and 14 deletions
@@ -9,6 +9,7 @@ use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\InsertedBeforeFailure;
use LIT\MCP\Integrations\Divi\ThemeBuilder\TbCreateTemplateAbility;
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
use LIT\MCP\Integrations\Divi\ThemeBuilder\TemplateConditions;
@@ -32,6 +33,7 @@ final class TbCreateTemplateAbilityTest extends TestCase
parent::setUp();
// The default template's global header 1061 is a published layout the user may edit (ruling Q8).
LibraryFixtures::given([ThemeBuilderFixtures::layout(ThemeBuilderFixtures::HEADER)]);
Functions\when('wp_delete_object_term_relationships')->justReturn(null);
Functions\when('admin_url')->alias(static fn (string $path): string => 'https://kunde.test/wp-admin/' . $path);
}
@@ -383,4 +385,60 @@ final class TbCreateTemplateAbilityTest extends TestCase
self::assertFalse($schema['additionalProperties']);
self::assertSame(TbCreateTemplateAbility::SOURCES, $schema['properties']['body']['properties']['source']['enum']);
}
/** Ruling Q19: Divi threw after inserting the template (e.g. a hook's wp_die) – that template goes too. */
public function testATemplateInsertedBeforeDiviThrewIsRolledBack(): void
{
$store = self::store();
$store->shouldReceive('insertLayout')->once()->andReturn(2101);
$store->shouldReceive('storeTemplate')->once()->andThrow(
new InsertedBeforeFailure('Divi called wp_die(): hook', [2100], new RuntimeException('hook'))
);
$store->shouldReceive('attach')->never();
$deleted = [];
self::deletions($deleted);
try {
$this->ability($store)->execute(self::input());
self::fail('no WriteFailed');
} catch (WriteFailed $e) {
self::assertTrue($e->rolledBack);
}
self::assertSame([2100, 2101], $deleted);
}
public function testALayoutInsertedBeforeDiviThrewIsRolledBack(): void
{
$store = self::store();
$store->shouldReceive('insertLayout')->once()->andThrow(
new InsertedBeforeFailure('Divi called wp_die(): hook', [2101], new RuntimeException('hook'))
);
$store->shouldReceive('storeTemplate')->never();
$deleted = [];
self::deletions($deleted);
$this->expectException(WriteFailed::class);
try {
$this->ability($store)->execute(self::input());
} finally {
self::assertSame([2101], $deleted);
}
}
/** Ruling Q19: the default template counts only when the user may edit it (like tb-list, Q8/B11). */
public function testTheGlobalHeaderNeedsAnEditableDefaultTemplate(): void
{
LibraryFixtures::given([ThemeBuilderFixtures::layout(ThemeBuilderFixtures::HEADER)]);
Functions\when('current_user_can')->alias(
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post'
&& (int) ($args[0] ?? 0) === ThemeBuilderFixtures::DEFAULT_TEMPLATE)
);
$store = self::store();
$store->shouldReceive('insertLayout')->never();
$result = $this->ability($store)->execute(self::input());
self::assertToolError($result, 'validation_failed');
self::assertArrayHasKey('header.source', self::errorFields($result));
}
}