Files
WP-LIT-MCP/tests/Unit/Architecture/CoreIndependenceTest.php
T
Jan-Niclas LoosenandClaude Opus 5.5 b511ef6348 refactor(compat): Prüfbausteine nach src/Compatibility, Matrix und ManifestCheck generisch
Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:03:30 +01:00

198 lines
7.5 KiB
PHP
Raw 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/DiviTab.php', $files, 'the Divi tab belongs to Integrations/Divi/Admin');
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviTab.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;
}
}