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
@@ -4,13 +4,16 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
use Brain\Monkey\Actions;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\InsertedBeforeFailure;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
use WP_Error;
use WP_Post;
final class DiviThemeBuilderTest extends TestCase
{
@@ -194,6 +197,96 @@ final class DiviThemeBuilderTest extends TestCase
}
}
/**
* The hooks the adapter watches while Divi runs (ruling Q19), captured so a fake Divi can fire them like
* wp_insert_post() does: wp_insert_post_data before the row, clean_post_cache after it.
*
* @return \ArrayObject<string, callable|null> "data" and "id".
*/
private static function captureInsertWatch(): \ArrayObject
{
$hooks = new \ArrayObject(['data' => null, 'id' => null]);
Filters\expectAdded('wp_insert_post_data')->whenHappen(static function (callable $callback) use ($hooks): void {
$hooks['data'] = $callback;
});
Actions\expectAdded('clean_post_cache')->whenHappen(static function (callable $callback) use ($hooks): void {
$hooks['id'] = $callback;
});
return $hooks;
}
/**
* What wp_insert_post() does around the row of post $id of $type.
*
* @param \ArrayObject<string, callable|null> $hooks
*/
private static function insert(\ArrayObject $hooks, int $id, string $type, bool $update = false): void
{
($hooks['data'])(['post_type' => $type], [], [], $update);
($hooks['id'])($id, new WP_Post(['ID' => $id, 'post_type' => $type]));
}
/** Ruling Q19: a throw after Divi's own wp_insert_post() (e.g. a hook's wp_die) still names the new post. */
public function testAThrowAfterTheLayoutInsertNamesTheNewLayout(): void
{
Functions\when('current_user_can')->justReturn(true);
$hooks = self::captureInsertWatch();
Functions\when('et_theme_builder_insert_layout')->alias(static function () use ($hooks): int {
self::insert($hooks, 2101, 'et_body_layout');
throw new RuntimeException('Divi called wp_die(): hook');
});
try {
(new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt');
self::fail('no exception');
} catch (InsertedBeforeFailure $e) {
self::assertSame([2101], $e->postIds);
self::assertSame('Divi called wp_die(): hook', $e->getMessage());
}
self::assertFalse(has_filter('wp_insert_post_data'));
self::assertFalse(has_action('clean_post_cache'));
}
public function testAThrowAfterTheTemplateInsertNamesTheNewTemplateOnly(): void
{
Functions\when('current_user_can')->justReturn(true);
Functions\when('et_')->justReturn(null);
$hooks = self::captureInsertWatch();
Functions\when('et_theme_builder_store_template')->alias(static function () use ($hooks): int {
// An existing template updated and another type inserted meanwhile are never named.
($hooks['id'])(1066, new WP_Post(['ID' => 1066, 'post_type' => 'et_template']));
self::insert($hooks, 1066, 'et_template', true);
self::insert($hooks, 3000, 'page');
self::insert($hooks, 2100, 'et_template');
($hooks['id'])(2100, new WP_Post(['ID' => 2100, 'post_type' => 'et_template']));
throw new RuntimeException('meta hook failed');
});
try {
(new DiviThemeBuilder())->storeTemplate(1064, ['title' => 'P', 'layouts' => [], 'use_on' => [], 'exclude_from' => []]);
self::fail('no exception');
} catch (InsertedBeforeFailure $e) {
self::assertSame([2100], $e->postIds);
}
}
public function testAThrowBeforeAnyInsertIsAPlainFailure(): void
{
Functions\when('current_user_can')->justReturn(true);
self::captureInsertWatch();
Functions\when('et_theme_builder_insert_layout')->alias(static function (): int {
throw new RuntimeException('early');
});
try {
(new DiviThemeBuilder())->insertLayout('body', 'Inhalt');
self::fail('no exception');
} catch (RuntimeException $e) {
self::assertNotInstanceOf(InsertedBeforeFailure::class, $e);
}
}
/** Nothing Divi prints reaches the MCP response, on success and on failure; buffers stay balanced. */
public function testOutputOfDiviNeverLeaks(): void
{