feat: content-create and content-update with schema validation

All-or-nothing writes: validation (fields, terms, featured image, parent,
template) before any write; failed extras delete the new draft (create) or
restore revision, old columns, fields, terms and featured image (update).
content-update requires rev; Rev::ofPost covers content, title, excerpt,
status and post_modified_gmt. MetaFieldSource::write() now throws on failure.

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-25 23:01:48 +02:00
co-authored by Claude Opus 5.5
parent b9d46f3cec
commit 4f9a47fe32
15 changed files with 1355 additions and 9 deletions
@@ -8,6 +8,7 @@ use Brain\Monkey\Functions;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException;
final class RegisteredMetaSourceTest extends TestCase
{
@@ -89,4 +90,54 @@ final class RegisteredMetaSourceTest extends TestCase
['footnotes' => $single, 'tags_list' => $multi, 'old' => $gone]
);
}
public function testUnchangedValueIsNotAFailure(): void
{
$field = new FieldDefinition('count', 'count', 'integer', 'meta', 'writable');
Functions\expect('update_post_meta')->once()->with(5, 'count', 3)->andReturn(false);
Functions\expect('get_post_meta')->once()->with(5, 'count', false)->andReturn(['3']);
(new RegisteredMetaSource())->write(5, 'event', ['count' => 3], ['count' => $field]);
self::addToAssertionCount(1);
}
public function testFailedUpdateThrows(): void
{
$field = new FieldDefinition('count', 'count', 'integer', 'meta', 'writable');
Functions\when('update_post_meta')->justReturn(false);
Functions\when('get_post_meta')->justReturn(['2']);
$this->expectException(RuntimeException::class);
(new RegisteredMetaSource())->write(5, 'event', ['count' => 3], ['count' => $field]);
}
public function testFailedDeleteOfAnExistingKeyThrows(): void
{
$field = new FieldDefinition('old', 'old', 'string', 'meta', 'writable');
Functions\when('delete_post_meta')->justReturn(false);
Functions\expect('metadata_exists')->once()->with('post', 5, 'old')->andReturn(true);
$this->expectException(RuntimeException::class);
(new RegisteredMetaSource())->write(5, 'event', ['old' => null], ['old' => $field]);
}
public function testDeletingAMissingKeyIsNotAFailure(): void
{
$field = new FieldDefinition('old', 'old', 'string', 'meta', 'writable');
Functions\when('delete_post_meta')->justReturn(false);
Functions\expect('metadata_exists')->once()->with('post', 5, 'old')->andReturn(false);
(new RegisteredMetaSource())->write(5, 'event', ['old' => null], ['old' => $field]);
self::addToAssertionCount(1);
}
public function testFailedAddOfAListItemThrows(): void
{
$multi = new FieldDefinition('tags_list', 'tags_list', 'array', 'meta', 'writable', false, true);
Functions\when('delete_post_meta')->justReturn(true);
Functions\when('add_post_meta')->justReturn(false);
$this->expectException(RuntimeException::class);
(new RegisteredMetaSource())->write(5, 'event', ['tags_list' => ['a']], ['tags_list' => $multi]);
}
}