Files
janniclas.loosenandClaude Opus 5.5 4137882008 fix: harden the Divi BlockTree against opaque/invalid writes
Final fix wave for stage 2a, load-bearing for 2b's writers:
- Block::setAttrs()/Container::insertChild()/removeChild() refuse on an opaque
  block (undecodable attrs or a global module) instead of silently corrupting it.
- Container::append() is now @internal (Parser only) and refuses on a void
  block, so a child can no longer be silently dropped from the serialized output.
- Block::create() validates the block name against namespace/block-name.
- Json::decodeObject() returns null (-> opaque) for a top-level object whose
  keys are a plain list ("0","1",...) rather than returning [] and letting the
  Serializer re-encode it as a corrupting JSON array.
- AttrMap::set() rejects empty path segments and keeps an existing stdClass
  value an object when rewrapping it (no silent object->list flip); AttrMap::get()
  is updated in step so it can still read back through such an object.
- Block::copy() deep-clones stdClass attribute values instead of sharing them.
- Json::encode()'s existing JsonException-on-invalid-UTF-8 behaviour is documented.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 13:12:49 +02:00

60 lines
1.6 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\BlockTree;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use PHPUnit\Framework\TestCase;
use stdClass;
final class AttrMapTest extends TestCase
{
/** @return array<string, array{string}> */
public static function emptySegmentPaths(): array
{
return [
'double dot' => ['a..b'],
'leading dot' => ['.a'],
'trailing dot' => ['a.'],
'empty' => [''],
];
}
/** @dataProvider emptySegmentPaths */
public function testSetRejectsEmptyPathSegments(string $path): void
{
$this->expectException(InvalidArgumentException::class);
AttrMap::set([], $path, 'z');
}
public function testSetKeepsAnObjectAnObjectWhenRewrapping(): void
{
$attrs = ['a' => new stdClass()];
$changed = AttrMap::set($attrs, 'a.0', 'z');
self::assertInstanceOf(stdClass::class, $changed['a']);
self::assertSame('z', $changed['a']->{'0'});
self::assertSame('{"a":{"0":"z"}}', json_encode($changed));
}
public function testSetStillBuildsPlainArraysWhenNoObjectExisted(): void
{
$changed = AttrMap::set([], 'a.b', 'z');
self::assertSame(['a' => ['b' => 'z']], $changed);
}
/** get() must still read back through a value set() kept as an object (regression guard). */
public function testGetReadsThroughAnObjectRewrappedBySet(): void
{
$attrs = ['a' => new stdClass()];
$changed = AttrMap::set($attrs, 'a.b', 'z');
self::assertSame('z', AttrMap::get($changed, 'a.b'));
}
}