Files
janniclas.loosenandClaude Opus 5.5 44c1804c4c feat: lit/library-make-global – node to global module, all or nothing (E10)
- NewLibraryEntry: spec and creation of a new library entry, shared by
  lit/library-save and lit/library-make-global (NewDraft, P7, StoredNodeGuard)
- UsageMatcher::referencesAny() (ruling Q7): a global entry never names any
  entry in any spelling Divi renders (P23/P24 forms, Divi 4 shortcodes inside
  Divi 5 values, values casting to 0); used by library-save scope global and
  make-global
- make-global: entry created, node replaced by GlobalInstance::of, page through
  DiviWriter::commit; refused or rolled-back page deletes the entry, a partial
  page write keeps it (the page may show it); sources are pages and posts only

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 21:35:48 +02:00

163 lines
8.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use LIT\MCP\Integrations\Divi\Library\UsageMatcher;
use PHPUnit\Framework\TestCase;
/**
* Ruling P23 (re-review F1): every reference Divi renders counts – Divi 5 openers in any JSON
* spelling, Divi 4 shortcodes in any quoting; an escaped opener that cannot be decoded counts as a
* possible reference (fail closed). Escape bytes are built with chr(92), never written inline.
*/
final class UsageMatcherTest extends TestCase
{
private static function u(string $hex): string
{
return chr(92) . 'u' . $hex;
}
/** @return array<string, array{string, bool}> Content => whether it shows entry 123. */
public static function contents(): array
{
$escapedKey = '"' . self::u('0067') . 'lobalModule"';
return [
'divi 5 canonical' => ['<!-- wp:divi/global-layout {"globalModule":"123","blockName":"divi/text"} /-->', true],
'divi 5 leading zeros' => ['<!-- wp:divi/global-layout {"globalModule":"0123","blockName":"divi/text"} /-->', true],
'divi 5 escaped key' => ['<!-- wp:divi/global-layout {' . $escapedKey . ':"123","blockName":"divi/text"} /-->', true],
'divi 5 escaped value' => [
'<!-- wp:divi/global-layout {"globalModule":"' . self::u('0031') . '23","blockName":"divi/text"} /-->',
true,
],
'divi 5 escaped key with leading zeros' => [
'<!-- wp:divi/global-layout {' . $escapedKey . ':"00123"} /-->',
true,
],
'divi 5 another entry' => ['<!-- wp:divi/global-layout {"globalModule":"1234","blockName":"divi/text"} /-->', false],
'divi 5 escaped key, another entry' => ['<!-- wp:divi/global-layout {' . $escapedKey . ':"124"} /-->', false],
'divi 5 id in local attributes' => [
'<!-- wp:divi/global-layout {"globalModule":"5","localAttrs":{"x":{"globalModule":"123"}}} /-->',
true,
],
'divi 4 double quotes' => ['[et_pb_section global_module="123"][/et_pb_section]', true],
'divi 4 single quotes and zeros' => ["[et_pb_text global_module='0123']x[/et_pb_text]", true],
'divi 4 unquoted' => ['[et_pb_row global_module=123 admin_label="x"][/et_pb_row]', true],
'divi 4 typographic quotes' => ['[et_pb_row global_module=' . "\u{201D}" . '123' . "\u{201D}" . '][/et_pb_row]', true],
'divi 4 another entry' => ['[et_pb_section global_module="1230"][/et_pb_section]', false],
'undecodable escaped opener' => [
'<!-- wp:divi/global-layout {' . $escapedKey . ':"123",} /-->',
true,
],
'undecodable escaped opener naming nothing readable' => [
'<!-- wp:divi/text {' . self::u('0067') . 'kaputt} /-->',
true,
],
'escaped markup only' => [
'<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"' . self::u('003c') . 'p'
. self::u('003e') . 'x"}}}} /-->',
false,
],
'plain page' => ['<!-- wp:divi/section --><!-- /wp:divi/section -->', false],
// Ruling P24 G1: a Divi 4 shortcode inside a Divi 5 attribute value (quotes serialized as escapes).
'shortcode in a divi 5 text value' => [self::inText('[et_pb_section global_module=' . self::u('0022') . '123'
. self::u('0022') . '][/et_pb_section]'), true],
'shortcode in a divi 5 text value, another entry' => [self::inText('[et_pb_section global_module='
. self::u('0022') . '124' . self::u('0022') . '][/et_pb_section]'), false],
'shortcode in a divi 5 text value, escaped digits' => [self::inText('[et_pb_section global_module='
. self::u('0022') . self::u('0031') . self::u('0032') . '3' . self::u('0022') . '][/et_pb_section]'), true],
// G2: parsed like shortcode_parse_atts, cast with (int) like Divi.
'divi 4 leading space' => ['[et_pb_section global_module=" 123"][/et_pb_section]', true],
'divi 4 exponent' => ['[et_pb_section global_module="1.23e2"][/et_pb_section]', true],
'divi 4 upper-case attribute' => ['[et_pb_section GLOBAL_MODULE="123"][/et_pb_section]', true],
'divi 4 zero renders nothing' => ['[et_pb_section global_module="0"][/et_pb_section]', false],
'divi 4 empty renders nothing' => ['[et_pb_section global_module=""][/et_pb_section]', false],
'global_module as plain text' => ['<p>global_module="123" steht hier nur als Text</p>', false],
];
}
/** A Divi 5 text module whose content value holds $text, JSON-encoded like WordPress does. */
private static function inText(string $text): string
{
return '<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"' . $text . '"}}}} /-->';
}
/**
* Ruling P24 G3: a global_module Divi casts to 0 renders the newest library or TB layout
* (et_pb_load_global_module, WP_Query p=0) – a reference to every entry.
*
* @return array<string, array{string}>
*/
public static function wildcards(): array
{
return [
'entity before the id' => ['[et_pb_section global_module="&quot;123"][/et_pb_section]'],
'letters' => ['[et_pb_row global_module="abc"][/et_pb_row]'],
'typographic quotes' => ['[et_pb_row global_module=' . "\u{201D}" . '123' . "\u{201D}" . '][/et_pb_row]'],
'double zero' => ['[et_pb_row global_module="00"][/et_pb_row]'],
'inside a divi 5 value' => [self::inText('[et_pb_text global_module=' . self::u('0022') . 'x'
. self::u('0022') . ']a[/et_pb_text]')],
];
}
/** @dataProvider wildcards */
public function testAValueThatCastsToZeroShowsEveryEntry(string $content): void
{
self::assertTrue(UsageMatcher::shows($content, 123));
self::assertTrue(UsageMatcher::shows($content, 98765));
}
/** @dataProvider contents */
public function testFindsEveryReferenceDiviRenders(string $content, bool $shows): void
{
self::assertSame($shows, UsageMatcher::shows($content, 123));
}
/**
* Ruling Q7: referencesAny() refuses every reference Divi renders, to any entry – what shows() finds for
* some ID, and the values that cast to 0 (the newest layout – possibly the entry being created).
*
* @dataProvider contents
*/
public function testReferencesAnyFindsEveryReferenceToAnyEntry(string $content, bool $shows): void
{
$any = UsageMatcher::shows($content, 123) || UsageMatcher::shows($content, 124)
|| UsageMatcher::shows($content, 1234) || UsageMatcher::shows($content, 1230);
self::assertSame($any, UsageMatcher::referencesAny($content));
}
/** @dataProvider wildcards */
public function testReferencesAnyFindsValuesThatCastToZero(string $content): void
{
self::assertTrue(UsageMatcher::referencesAny($content));
}
/** @return array<string, array{string, bool}> */
public static function anyContents(): array
{
return [
'divi 5 non-numeric id' => ['<!-- wp:divi/global-layout {"globalModule":"abc","blockName":"divi/text"} /-->', true],
'divi 5 numeric id' => ['<!-- wp:divi/global-layout {"globalModule":7} /-->', true],
'divi 5 textual id in raw text' => ['<p>"globalModule":"9"</p>', true],
'divi 4 other tag' => ['[et_pb_blurb global_module="55"][/et_pb_blurb]', true],
'divi 4 zero' => ['[et_pb_section global_module="0"][/et_pb_section]', false],
'plain text' => ['<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"<p>Global</p>"}}}} /-->', false],
];
}
/** @dataProvider anyContents */
public function testReferencesAnyFailsClosed(string $content, bool $references): void
{
self::assertSame($references, UsageMatcher::referencesAny($content));
}
public function testTheEscapeBytesAreReal(): void
{
self::assertSame(6, strlen(self::u('0067')));
self::assertSame(92, ord(self::u('0067')[0]));
}
}