Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
7.6 KiB
PHP
198 lines
7.6 KiB
PHP
<?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 and none of the CORE_ROOTS files 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, the one listed exception – wires up. Divi's own UI (settings tab, WP-CLI command) lives in
|
||
* Integrations/Divi. Divi's own names (et_* …) are DiviBoundaryTest's business, not this test's.
|
||
*/
|
||
final class CoreIndependenceTest extends TestCase
|
||
{
|
||
/** Directories below src/ (scanned recursively) that must not use Divi classes. */
|
||
private const CORE_PATHS = [
|
||
'Abilities',
|
||
'Admin',
|
||
'ChangeLog',
|
||
'Cli',
|
||
'Compatibility',
|
||
'Guides',
|
||
'Integrations/Core',
|
||
'Integrations/Media',
|
||
'Integrations/Pods',
|
||
'Integrations/WordPress',
|
||
'Server',
|
||
];
|
||
|
||
/** CORE_PATHS that may be absent: no generic WP-CLI command exists yet (Divi's lives in Integrations/Divi/Cli). */
|
||
private const OPTIONAL_PATHS = ['Cli'];
|
||
|
||
/** Directories below src/ whose PHP files directly inside (not below) must not use Divi classes either. */
|
||
private const CORE_ROOTS = ['', 'Integrations'];
|
||
|
||
/** Files of the scan that may use Divi classes, each with its reason. */
|
||
private const EXCEPTIONS = [
|
||
'Plugin.php' => 'Composition root: builds DiviIntegration and wires its ports (DiviFeatureGate) into the core.',
|
||
];
|
||
|
||
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 testTheScanFindsImportsQualifiedNamesGroupUsesAliasesAndClassStrings(): 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";
|
||
use LIT\MCP\Integrations\Divi as D;
|
||
PHP;
|
||
|
||
self::assertSame(
|
||
[
|
||
'Divi\Pages\DiviPages',
|
||
'LIT\MCP\Integrations\Divi',
|
||
'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 => '',
|
||
};
|
||
$diviClass = '/^(?:LIT\\\\MCP\\\\Integrations\\\\)?Divi\\\\[A-Za-z0-9_\\\\]+$/';
|
||
if (preg_match($diviClass, $text) === 1 || $text === 'LIT\\MCP\\Integrations\\Divi') {
|
||
$names[] = $text;
|
||
}
|
||
}
|
||
$names = array_values(array_unique($names));
|
||
sort($names);
|
||
|
||
return $names;
|
||
}
|
||
|
||
public function testEveryScannedPathExists(): void
|
||
{
|
||
$root = self::root();
|
||
foreach ([...self::CORE_PATHS, ...self::CORE_ROOTS] as $directory) {
|
||
if (! in_array($directory, self::OPTIONAL_PATHS, true)) {
|
||
self::assertDirectoryExists($root . $directory, $directory . ' fehlt – Scan-Liste anpassen');
|
||
}
|
||
}
|
||
}
|
||
|
||
public function testTheDiviUiLivesInTheDiviIntegration(): void
|
||
{
|
||
$files = self::coreFiles();
|
||
|
||
self::assertArrayHasKey('Plugin.php', $files, 'root files are scanned');
|
||
self::assertArrayHasKey('Integrations/IntegrationRegistry.php', $files, 'Integrations root files are scanned');
|
||
self::assertArrayNotHasKey('Admin/DiviAdminExtras.php', $files, 'the Divi additions belong to Integrations/Divi/Admin');
|
||
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviAdminExtras.php');
|
||
self::assertFileExists(self::root() . 'Integrations/Divi/Cli/DiviCheckCommand.php');
|
||
}
|
||
|
||
private static function root(): string
|
||
{
|
||
return dirname(__DIR__, 3) . '/src/';
|
||
}
|
||
|
||
/**
|
||
* @return array<string, string> Path below src/ => absolute path of every PHP file in CORE_PATHS (recursively) and
|
||
* directly in CORE_ROOTS.
|
||
*/
|
||
private static function coreFiles(): array
|
||
{
|
||
$root = self::root();
|
||
$files = [];
|
||
foreach (self::CORE_PATHS as $directory) {
|
||
if (! is_dir($root . $directory)) {
|
||
continue; // Only an OPTIONAL_PATHS entry may be missing (testEveryScannedPathExists).
|
||
}
|
||
$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();
|
||
}
|
||
}
|
||
}
|
||
foreach (self::CORE_ROOTS as $directory) {
|
||
foreach (glob($root . ($directory === '' ? '' : $directory . '/') . '*.php') ?: [] as $path) {
|
||
$files[substr($path, strlen($root))] = $path;
|
||
}
|
||
}
|
||
ksort($files);
|
||
|
||
return $files;
|
||
}
|
||
}
|