Files
Jan-Niclas LoosenandClaude Opus 5.5 b40b81f585 refactor(divi): DiviTab geht im Reiter „Compatibility“ auf
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>
2026-11-06 13:32:29 +01:00

198 lines
7.6 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\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;
}
}