refactor: Kern ohne Divi – Ports BuilderDetector, MediaUsageSource, EnvironmentComponent und ausgeschlossene Post-Typen im ContentSchema; Divi-Implementierungen und Verdrahtung über DiviIntegration/Plugin; CoreIndependenceTest

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 21:49:28 +02:00
co-authored by Claude Sonnet 5.5
parent abeb4b0eed
commit 06148fb8c1
33 changed files with 952 additions and 154 deletions
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Architecture;
use FilesystemIterator;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* The generic core knows no Divi class: nothing below the CORE_PATHS imports or names a class of
* LIT\MCP\Integrations\Divi. Divi plugs in through ports (AbilityGuard, ToolRefusal, BuilderDetector,
* MediaUsageSource, EnvironmentComponent, ContentSchema's excluded post types) that Plugin.php – the composition
* root, outside these paths – wires up. Divi's own names (et_* …) are DiviBoundaryTest's business, not this test's.
*/
final class CoreIndependenceTest extends TestCase
{
/** Directories below src/ that must not use Divi classes. */
private const CORE_PATHS = [
'Abilities',
'Integrations/Core',
'Integrations/WordPress',
'Integrations/Media',
'Admin',
'Guides',
'Server',
];
/** Files inside CORE_PATHS that may use Divi classes, each with its reason. */
private const EXCEPTIONS = [
'Admin/DiviTab.php' => 'The settings tab "Divi" is Divi UI: release state, admin check and release/withdraw.',
];
public function testTheCoreUsesNoDiviClass(): void
{
$offenders = [];
foreach (self::coreFiles() as $relative => $path) {
if (isset(self::EXCEPTIONS[$relative])) {
continue;
}
$names = self::diviNames((string) file_get_contents($path));
if ($names !== []) {
$offenders[] = $relative . ': ' . implode(', ', $names);
}
}
self::assertSame([], $offenders, "Der Kern darf keine Divi-Klasse benutzen (Port über Plugin.php verdrahten):\n"
. implode("\n", $offenders));
}
public function testEveryExceptionExistsAndStillNeedsIt(): void
{
$files = self::coreFiles();
foreach (array_keys(self::EXCEPTIONS) as $relative) {
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – Ausnahme streichen');
self::assertNotSame(
[],
self::diviNames((string) file_get_contents($files[$relative])),
$relative . ' braucht die Ausnahme nicht mehr – streichen'
);
}
}
public function testTheScanFindsImportsQualifiedNamesGroupUsesAndClassStrings(): void
{
$source = <<<'PHP'
<?php
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\{Media\Finder, Divi\Pages\DiviPages};
$a = \LIT\MCP\Integrations\Divi\Schema\DiviModules::isAvailable();
$b = 'LIT\\MCP\\Integrations\\Divi\\Design\\DesignSystemReader';
$c = "LIT\\MCP\\Integrations\\Divi\\Library\\LibraryAccess";
PHP;
self::assertSame(
[
'Divi\Pages\DiviPages',
'LIT\MCP\Integrations\Divi\Design\DesignSystemReader',
'LIT\MCP\Integrations\Divi\Library\LibraryAccess',
'LIT\MCP\Integrations\Divi\Release\ReleaseGate',
'LIT\MCP\Integrations\Divi\Schema\DiviModules',
],
self::diviNames($source)
);
}
public function testTheScanIgnoresCommentsOtherNamespacesAndPlainWords(): void
{
$source = <<<'PHP'
<?php
// use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
/** @see \LIT\MCP\Integrations\Divi\Pages\DiviPages */
use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Abilities\DiviMarkup;
$x = 'Divi';
$y = 'Ein Divi-Seitenaufbau';
PHP;
self::assertSame([], self::diviNames($source));
}
/** @return list<string> Sorted, unique names of Divi classes the source uses. */
private static function diviNames(string $source): array
{
$names = [];
foreach (token_get_all($source) as $token) {
if (! is_array($token)) {
continue;
}
$text = match ($token[0]) {
T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED => ltrim($token[1], '\\'),
T_CONSTANT_ENCAPSED_STRING => ltrim(str_replace('\\\\', '\\', substr($token[1], 1, -1)), '\\'),
default => '',
};
if (preg_match('/^(?:LIT\\\\MCP\\\\Integrations\\\\)?Divi\\\\[A-Za-z0-9_\\\\]+$/', $text) === 1) {
$names[] = $text;
}
}
$names = array_values(array_unique($names));
sort($names);
return $names;
}
/** @return array<string, string> Path below src/ => absolute path of every PHP file in CORE_PATHS. */
private static function coreFiles(): array
{
$root = dirname(__DIR__, 3) . '/src/';
$files = [];
foreach (self::CORE_PATHS as $directory) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root . $directory, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->getExtension() === 'php') {
$files[substr((string) $file->getPathname(), strlen($root))] = (string) $file->getPathname();
}
}
}
ksort($files);
return $files;
}
}