feat: Divi manifests, adapter register and boundary test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 00:46:16 +02:00
co-authored by Claude Sonnet 5
parent 905bb8c2f3
commit 5e6d5ff3e1
9 changed files with 543 additions and 0 deletions
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use FilesystemIterator;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\DiviTouches;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
/**
* Spec §3: only the adapters in DiviAdapters::CLASSES touch Divi, and every Divi name an adapter uses stands in its
* manifest (and nothing more).
*/
final class DiviBoundaryTest extends TestCase
{
/**
* Files that still touch Divi outside an adapter (paths below src/). Tasks 3–7 shrink this list to
* DiviCompatibility.php; Task 8 removes it together with testLegacyFilesStillTouchDivi.
*/
private const LEGACY = [
'Integrations/Divi/Design/DesignSystemReader.php',
'Integrations/Divi/DiviCompatibility.php',
'Integrations/Divi/Library/DiviLibraryStore.php',
'Integrations/Divi/Library/DiviRoles.php',
'Integrations/Divi/Library/LibraryItemSpec.php',
'Integrations/Divi/Library/NewLibraryEntry.php',
'Integrations/Divi/Pages/DiviFlags.php',
'Integrations/Divi/Pages/DiviPage.php',
'Integrations/Divi/Pages/DiviSaver.php',
'Integrations/Divi/Pages/DynamicContentMap.php',
'Integrations/Divi/Pages/WpLinkTargets.php',
'Integrations/Divi/Schema/DiviModules.php',
'Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php',
'Integrations/Media/MediaUsageFinder.php',
'Integrations/WordPress/Content/PostPresenter.php',
'Integrations/WordPress/Schema/ContentSchema.php',
];
public function testOnlyAdaptersTouchDivi(): void
{
$offenders = [];
foreach (self::sourceFiles() as $relative => $path) {
$touches = DiviTouches::inFile($path);
if ($touches === [] || in_array($relative, self::LEGACY, true)) {
continue;
}
if (! in_array(self::classOf($relative), DiviAdapters::CLASSES, true)) {
$offenders[] = $relative . ': ' . implode(', ', array_unique(array_column($touches, 'name')));
}
}
self::assertSame([], $offenders, "Divi darf nur ein Adapter aus DiviAdapters::CLASSES berühren:\n"
. implode("\n", $offenders));
}
public function testLegacyFilesStillTouchDivi(): void
{
$files = self::sourceFiles();
foreach (self::LEGACY as $relative) {
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – aus LEGACY streichen');
self::assertNotSame([], DiviTouches::inFile($files[$relative]), $relative . ' ist sauber – aus LEGACY streichen');
self::assertNotContains(self::classOf($relative), DiviAdapters::CLASSES, $relative . ' ist ein Adapter');
}
}
public function testAdaptersDeclareExactlyTheDiviNamesTheyUse(): void
{
foreach (DiviAdapters::CLASSES as $class) {
self::assertTrue(is_subclass_of($class, DiviAdapter::class), $class . ' implementiert DiviAdapter nicht');
$declared = $class::manifest()->names();
$used = array_values(array_unique(array_column(
DiviTouches::inFile((string) (new ReflectionClass($class))->getFileName()),
'name'
)));
self::assertSame([], array_values(array_diff($used, $declared)), $class . ': nicht im Manifest');
self::assertSame(
[],
array_values(array_diff(array_filter($declared, [DiviTouches::class, 'detects']), $used)),
$class . ': im Manifest, aber nicht benutzt'
);
}
}
public function testMirrorIdsAreUniqueAndFilesRelative(): void
{
$ids = [];
foreach (DiviAdapters::mirrors() as $mirror) {
self::assertNotContains($mirror->id, $ids, 'doppelte Mirror-ID ' . $mirror->id);
self::assertStringStartsNotWith('/', $mirror->file);
$ids[] = $mirror->id;
}
}
/** @return array<string, string> Path below src/ => absolute path. */
private static function sourceFiles(): array
{
$root = dirname(__DIR__, 5) . '/src/';
$files = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, 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;
}
private static function classOf(string $relative): string
{
return 'LIT\\MCP\\' . str_replace(['/', '.php'], ['\\', ''], $relative);
}
}