Files
WP-LIT-MCP/tests/Unit/Compatibility/ManifestCheckTest.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

323 lines
12 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\Compatibility;
use ArrayObject;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\ConstantRequirement;
use LIT\MCP\Compatibility\GlobalRequirement;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Compatibility\Problem;
use LIT\MCP\Compatibility\SignatureFingerprint;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Unit\TestCase;
use LogicException;
use RuntimeException;
final class ManifestCheckTest extends TestCase
{
private function check(?\Closure $current = null): ManifestCheck
{
$manifests = [
'A' => new Manifest(
DiviAreas::PAGES,
calls: [
new Call('strlen', 1),
new Call('lit_mcp_missing_function', 0),
new Call(ArrayObject::class . '::count', 0),
new Call('Missing\Klass::run', 0),
new Call('str_replace'),
],
constants: ['PHP_VERSION', 'LIT_MCP_MISSING_CONSTANT'],
postTypes: ['et_pb_layout'],
taxonomies: ['scope'],
blockTypes: ['divi/global-layout'],
mirrors: [
new Mirror('same', 'a.php', 'a', null, 'gleich'),
new Mirror('changed', 'b.php', 'b', null, 'geändert'),
new Mirror('unreadable', 'c.php', 'c', null, 'fehlt'),
new Mirror('no-expectation', 'd.php', 'd', null, 'ohne Soll'),
]
),
];
$currentHashes = ['same' => 'h1', 'changed' => 'hX', 'no-expectation' => 'h4'];
return new ManifestCheck(
$manifests,
['same' => 'h1', 'changed' => 'h2', 'unreadable' => 'h3'],
$current ?? static fn (Mirror $mirror): ?string => $currentHashes[$mirror->id] ?? null,
static fn (string $name): bool => false
);
}
public function testCodeChecksCoverExistenceAndArgumentCount(): void
{
self::assertSame(
[
'Seiten: strlen()',
'Seiten: lit_mcp_missing_function()',
'Seiten: ArrayObject::count()',
'Seiten: Klass::run()',
'Seiten: str_replace()',
'Seiten: Konstante PHP_VERSION',
'Seiten: Konstante LIT_MCP_MISSING_CONSTANT',
],
array_keys($this->check()->codeChecks())
);
self::assertSame(
[
'Seiten: lit_mcp_missing_function()',
'Seiten: Klass::run()',
'Seiten: Konstante LIT_MCP_MISSING_CONSTANT',
],
ManifestCheck::failed($this->check()->codeChecks())
);
}
public function testAcceptsChecksTheArgumentCountTheAdapterPasses(): void
{
self::assertTrue(ManifestCheck::accepts(new Call('strlen', 1)));
self::assertFalse(ManifestCheck::accepts(new Call('strlen', 2)), 'strlen takes one argument');
self::assertFalse(ManifestCheck::accepts(new Call('str_replace', 2)), 'str_replace needs three');
self::assertTrue(ManifestCheck::accepts(new Call('sprintf', 5)), 'variadic');
self::assertTrue(ManifestCheck::accepts(new Call('str_replace')), 'existence only');
}
public function testMirrorChecksCompareWithTheExpectedFingerprints(): void
{
$checks = $this->check()->mirrorChecks();
self::assertSame(
[
'Seiten: Nachbau von a() – gleich',
'Seiten: Nachbau von b() – geändert',
'Seiten: Nachbau von c() – fehlt',
'Seiten: Nachbau von d() – ohne Soll',
],
array_keys($checks)
);
self::assertSame(
[
'Seiten: Nachbau von b() – geändert',
'Seiten: Nachbau von c() – fehlt',
'Seiten: Nachbau von d() – ohne Soll',
],
ManifestCheck::failed($checks)
);
}
public function testUnreadableMirrorIsAFailedCheck(): void
{
$check = $this->check(static function (): ?string {
throw new RuntimeException('Divi file gone');
});
self::assertCount(4, ManifestCheck::failed($check->mirrorChecks()));
}
public function testRegistrationChecksAskWordPressAndDivi(): void
{
Functions\when('post_type_exists')->justReturn(true);
Functions\when('taxonomy_exists')->justReturn(false);
$checks = $this->check()->registrationChecks();
self::assertSame(
['Seiten: Post-Typ et_pb_layout', 'Seiten: Taxonomie scope', 'Seiten: Block-Typ divi/global-layout'],
array_keys($checks)
);
self::assertSame(
['Seiten: Taxonomie scope', 'Seiten: Block-Typ divi/global-layout'],
ManifestCheck::failed($checks)
);
}
/** A manifest with one requirement per problem kind; the default feature is page-read. */
private function featureCheck(?\Closure $current = null): ManifestCheck
{
$manifest = new Manifest(
DiviAreas::PAGES,
calls: [
new Call('strlen', 1),
new Call('str_replace', 3, [DiviFeature::PageWrite]),
new Call('trim', 1),
new Call('lit_mcp_missing_function', 0, [DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite]),
],
constants: [
new ConstantRequirement('PHP_VERSION'),
new ConstantRequirement('LIT_MCP_MISSING_CONSTANT', [DiviFeature::Catalog]),
],
postTypes: ['lit_mcp_missing_type'],
mirrors: [
new Mirror('same', 'a.php', 'a', null, 'gleich'),
new Mirror('changed', 'b.php', 'b', null, 'geändert', [DiviFeature::ThemeBuilderWrite]),
new Mirror('no-expectation', 'd.php', 'd', null, 'ohne Soll'),
],
features: [DiviFeature::PageRead]
);
$hashes = ['same' => 'h1', 'changed' => 'hX', 'no-expectation' => 'h4'];
return new ManifestCheck(
['A' => $manifest],
[
'same' => 'h1',
'changed' => 'h2',
'sig:strlen' => (string) SignatureFingerprint::of(new Call('strlen', 1)),
'sig:str_replace' => 'a different signature',
],
$current ?? static fn (Mirror $mirror): ?string => $hashes[$mirror->id] ?? null,
static fn (string $name): bool => false
);
}
/**
* @param list<Problem> $problems
* @return list<array{label: string, kind: string, features: list<string>}>
*/
private static function rows(array $problems): array
{
return array_map(static fn (Problem $problem): array => $problem->toArray(), $problems);
}
public function testCodeProblemsNameKindAndFeaturesOfEveryFailedCall(): void
{
self::assertSame(
[
['label' => 'Seiten: str_replace() – Signatur geändert', 'kind' => 'signature',
'features' => ['page-write']],
['label' => 'Seiten: trim() – Referenz der Signatur fehlt', 'kind' => 'reference-missing',
'features' => ['page-read']],
['label' => 'Seiten: lit_mcp_missing_function()', 'kind' => 'call',
'features' => ['page-write', 'tb-write']],
['label' => 'Seiten: Konstante LIT_MCP_MISSING_CONSTANT', 'kind' => 'constant',
'features' => ['catalog']],
],
self::rows($this->featureCheck(static function (): never {
self::fail('code problems never read Divi files');
})->codeProblems())
);
}
public function testAMirrorWithoutReferenceIsAMirrorProblemLikeAChangedOne(): void
{
self::assertSame(
[
['label' => 'Seiten: Nachbau von b() – geändert', 'kind' => 'mirror', 'features' => ['tb-write']],
['label' => 'Seiten: Nachbau von d() – ohne Soll', 'kind' => 'mirror', 'features' => ['page-read']],
],
self::rows($this->featureCheck()->mirrorProblems())
);
}
public function testAnUnreadableMirrorIsAMirrorProblem(): void
{
$problems = $this->featureCheck(static function (): never {
throw new RuntimeException('Divi file gone');
})->mirrorProblems();
self::assertSame(['mirror', 'mirror', 'mirror'], array_column(self::rows($problems), 'kind'));
}
public function testProblemsAreCodeThenMirrorsAndRegistrationIsNone(): void
{
$check = $this->featureCheck();
self::assertEquals(array_merge($check->codeProblems(), $check->mirrorProblems()), $check->problems());
self::assertNotContains(
'Seiten: Post-Typ lit_mcp_missing_type',
array_column(self::rows($check->problems()), 'label')
);
}
public function testWithoutSignaturesACallWithoutReferenceIsNoProblem(): void
{
$check = new ManifestCheck(
['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 1), new Call('trim', 1)])],
[],
null,
null,
false
);
self::assertSame([], $check->codeProblems());
}
public function testWithoutSignaturesAMissingCallStillFails(): void
{
$check = new ManifestCheck(
['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 2)])],
signatures: false
);
self::assertSame(['call'], array_column(self::rows($check->codeProblems()), 'kind'));
}
public function testAMissingGlobalIsAHardProblemWithItsFeatures(): void
{
$GLOBALS['lit_mcp_present_global'] = 1;
$GLOBALS['lit_mcp_null_global'] = null;
$check = new ManifestCheck(['A' => new Manifest(
DiviAreas::PAGES,
globals: [
new GlobalRequirement('lit_mcp_present_global'),
new GlobalRequirement('lit_mcp_null_global'),
new GlobalRequirement('lit_mcp_missing_global', [DiviFeature::Catalog]),
],
features: [DiviFeature::PageRead]
)], signatures: false);
try {
$problems = $check->problems();
$failed = ManifestCheck::failed($check->codeChecks());
} finally {
unset($GLOBALS['lit_mcp_present_global'], $GLOBALS['lit_mcp_null_global']);
}
self::assertSame(
[
['label' => 'Seiten: Globale Variable lit_mcp_null_global', 'kind' => 'global',
'features' => ['page-read']],
['label' => 'Seiten: Globale Variable lit_mcp_missing_global', 'kind' => 'global',
'features' => ['catalog']],
],
self::rows($problems)
);
self::assertTrue($problems[0]->isHard());
self::assertSame(
['Seiten: Globale Variable lit_mcp_null_global', 'Seiten: Globale Variable lit_mcp_missing_global'],
$failed
);
}
public function testWithoutCurrentClosureEveryMirrorIsUnreadable(): void
{
$check = new ManifestCheck(['A' => new Manifest(
DiviAreas::PAGES,
mirrors: [new Mirror('same', 'a.php', 'a', null, 'gleich')]
)], ['same' => 'h1']);
self::assertSame(['mirror'], array_column(self::rows($check->mirrorProblems()), 'kind'));
}
public function testBlockTypesWithoutKnowsBlockClosureRefuseToBuildTheCheck(): void
{
$this->expectException(LogicException::class);
new ManifestCheck(['A' => new Manifest(DiviAreas::PAGES, blockTypes: ['divi/x'])]);
}
public function testWithoutBlockTypesNoKnowsBlockClosureIsNeeded(): void
{
$check = new ManifestCheck(['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 1)])]);
self::assertSame([], $check->registrationChecks());
}
}