refactor(compat): Freigabe-Bausteine generisch

ReleaseStore, CheckReportStore, CheckReport, CheckStep und CompatibilityCheck liegen im Kern und arbeiten je Profil (Optionen lit_mcp_<id>_releases|_check_report; Divi behält Namen und Schlüssel divi_version). Divi hängt Katalog-, Format- und Schreibtest über Profile::extraSteps an. Ein roter Prüfbericht für den aktuellen Stand sperrt LockOnFailure-Profile bis zur nächsten grünen Prüfung; Sperrgründe nennen nur Schritt-Labels, Ausnahmen gehen ins Error-Log. Profile bekommt testedVersions, stateKey, extraSteps, manifestCheck und reportVersionKey und wird mit benannten Argumenten gebaut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 12:51:03 +01:00
co-authored by Claude Opus 5.5
parent c85131603b
commit 7d6a97a54e
48 changed files with 1469 additions and 647 deletions
@@ -59,7 +59,7 @@ final class CompatibilityGatesTest extends TestCase
$ability->allows('requirements')->andReturn($ids);
$ability->allows('permission')->andReturn(true);
$free = new Profile('fake', 'Fake Plugin', Tier::LockOnFailure, [FakeCapability::Read]);
$free = new Profile(id: 'fake', label: 'Fake Plugin', tier: Tier::LockOnFailure, capabilities: [FakeCapability::Read]);
$compat = new Compatibility([DiviProfile::profile(), PodsCompatibility::profile(), $free]);
$integration = Mockery::mock(Integration::class);
$integration->allows('id')->andReturn('core');
@@ -40,17 +40,17 @@ final class CapabilityGateTest extends TestCase
private function gate(?FakeChangeLog $log = null, array $delegated = []): CapabilityGate
{
$lock = new Profile(
'fake',
'Fake Plugin',
Tier::LockOnFailure,
[FakeCapability::Read, FakeCapability::Write],
[new Manifest('Fake', calls: [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])])]
id: 'fake',
label: 'Fake Plugin',
tier: Tier::LockOnFailure,
capabilities: [FakeCapability::Read, FakeCapability::Write],
manifests: [new Manifest('Fake', calls: [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])])]
);
$release = new Profile(
'release',
'Release',
Tier::ReleasePerVersion,
[FakeCapability::Other],
id: 'release',
label: 'Release',
tier: Tier::ReleasePerVersion,
capabilities: [FakeCapability::Other],
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allIncompatible(
[FakeCapability::Other],
'would lock'
@@ -18,7 +18,7 @@ final class CapabilityLockedTest extends TestCase
{
public function testTheErrorNamesPluginCapabilitiesAndProblems(): void
{
$profile = new Profile('fake', 'Fake Plugin', Tier::LockOnFailure, [FakeCapability::Write]);
$profile = new Profile(id: 'fake', label: 'Fake Plugin', tier: Tier::LockOnFailure, capabilities: [FakeCapability::Write]);
$locked = CapabilityLocked::of(
[new FeatureStatus(FakeCapability::Write, ReleaseState::Incompatible, ['Fake: x() fehlt'])],
[$profile]
@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Compatibility;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Unit\TestCase;
final class CheckReportTest extends TestCase
{
/** @var array<string, mixed> */
private array $options = [];
protected function setUp(): void
{
parent::setUp();
$this->options = [];
Functions\when('get_option')->alias(
fn (string $name, mixed $default = false): mixed => $this->options[$name] ?? $default
);
Functions\when('update_option')->alias(function (string $name, mixed $value, mixed $autoload = null): bool {
$this->options[$name] = $value;
self::assertFalse($autoload, 'the report is read in the admin only – no autoload');
return true;
});
}
/** @param list<CheckStep> $steps */
private static function report(array $steps): CheckReport
{
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '5.3.0', '0.6.0', 'key', $steps);
}
/** @return list<CheckStep> */
private static function green(): array
{
return array_map(
static fn (string $key): CheckStep => new CheckStep($key, []),
[CheckStep::MANIFEST, CheckStep::MIRRORS, 'catalog']
);
}
/** What the Divi report looked like before the generic version: no label per step, key divi_version. */
private static function oldDiviReport(): array
{
return [
'id' => 'r1',
'created_at' => '2026-10-01T08:00:00+00:00',
'user_id' => 1,
'divi_version' => '5.3.0',
'plugin_version' => '0.6.0',
'state_key' => 'key',
'steps' => [
['key' => 'manifest', 'problems' => [], 'skipped' => false],
['key' => 'mirrors', 'problems' => ['Theme Builder: Nachbau von x() – y'], 'skipped' => false],
['key' => 'write', 'problems' => [], 'skipped' => true],
],
];
}
public function testGreenOnlyWithEveryStepGreenAndNoneSkipped(): void
{
self::assertTrue(self::report(self::green())->green());
self::assertFalse(self::report([])->green());
$red = self::green();
$red[2] = new CheckStep('catalog', ['divi/text: unbekanntes Modul']);
self::assertFalse(self::report($red)->green());
$skipped = self::green();
$skipped[2] = new CheckStep('catalog', [], true);
self::assertFalse(self::report($skipped)->green());
}
public function testFreshOnlyForTheSameVersionsAndStateKey(): void
{
$report = self::report(self::green());
self::assertTrue($report->freshFor('5.3.0', '0.6.0', 'key'));
self::assertFalse($report->freshFor('5.3.1', '0.6.0', 'key'), 'plugin under test updated');
self::assertFalse($report->freshFor('5.3.0', '0.7.0', 'key'), 'this plugin updated');
self::assertFalse($report->freshFor('5.3.0', '0.6.0', 'other'), 'a source file changed');
}
public function testRoundTripsThroughAnArrayWithTheLabel(): void
{
$steps = self::green();
$steps[1] = new CheckStep(CheckStep::MIRRORS, ['x'], false, 'Fingerabdrücke');
$report = self::report($steps);
self::assertEquals($report, CheckReport::fromArray($report->toArray()));
self::assertSame('Fingerabdrücke', $report->step(CheckStep::MIRRORS)?->label());
self::assertSame('Fingerabdrücke', $report->toArray()['steps'][1]['label']);
self::assertNull(CheckReport::fromArray('broken'));
self::assertNull(CheckReport::fromArray(['id' => 'r1']));
}
public function testAStepWithoutLabelShowsItsKeyAndIsWrittenWithoutOne(): void
{
$step = new CheckStep('catalog', []);
self::assertSame('catalog', $step->label());
self::assertArrayNotHasKey('label', $step->toArray());
}
public function testTheGenericStepsHaveOneLabelSourceWithTheProfileName(): void
{
self::assertSame('Pods-Funktionen, Klassen und Registrierungen', CheckStep::genericLabel(CheckStep::MANIFEST, 'Pods'));
self::assertSame('Fingerabdrücke nachgebauter Pods-Funktionen', CheckStep::genericLabel(CheckStep::MIRRORS, 'Pods'));
self::assertNull(CheckStep::genericLabel('catalog', 'Pods'));
}
public function testAReportWithoutStepsIsNotGreen(): void
{
self::assertFalse(self::report([])->green());
}
public function testAnOldDiviReportIsReadAndWrittenUnchanged(): void
{
$old = self::oldDiviReport();
$report = CheckReport::fromArray($old, 'divi_version');
self::assertSame('5.3.0', $report?->version);
self::assertSame('manifest', $report?->steps[0]->label(), 'no stored label: the raw key');
self::assertSame($old, $report?->toArray('divi_version'));
self::assertNull(CheckReport::fromArray($old), 'the default key is "version"');
}
public function testTheStoreKeepsTheDiviOptionAndFormat(): void
{
$this->options['lit_mcp_divi_check_report'] = self::oldDiviReport();
$store = new CheckReportStore('divi', 'divi_version');
$report = $store->latest();
self::assertSame('r1', $report?->id);
$store->save($report);
self::assertSame(self::oldDiviReport(), $this->options['lit_mcp_divi_check_report']);
}
public function testTheStoresOfAProfileUseItsIdAndVersionKey(): void
{
$profile = new Profile(
id: 'divi',
label: 'Divi',
tier: Tier::ReportOnly,
capabilities: [FakeCapability::Read],
reportVersionKey: 'divi_version'
);
$this->options['lit_mcp_divi_check_report'] = self::oldDiviReport();
$this->options['lit_mcp_divi_releases'] = [[
'divi_version' => '5.2.2', 'plugin_version' => '0.6.0', 'user_id' => 3,
'released_at' => '2026-10-01T08:00:00+00:00', 'report_id' => 'r',
]];
self::assertSame('r1', CheckReportStore::forProfile($profile)->latest()?->id);
self::assertSame('r', ReleaseStore::forProfile($profile)->find('5.2.2', '0.6.0')['report_id'] ?? null);
}
public function testStoredReportReadsTheDiviFormatForAProfileWithItsKey(): void
{
$profile = new Profile(
id: 'divi',
label: 'Divi',
tier: Tier::ReportOnly,
capabilities: [FakeCapability::Read],
reportVersionKey: 'divi_version'
);
$this->options['lit_mcp_divi_check_report'] = self::oldDiviReport();
self::assertSame('5.3.0', Compatibility::storedReport($profile)?->version);
}
public function testTheStoreUsesAnOptionAndVersionKeyPerProfile(): void
{
$store = new CheckReportStore('pods');
self::assertNull($store->latest());
$store->save(self::report(self::green()));
self::assertSame(['lit_mcp_pods_check_report'], array_keys($this->options));
self::assertSame('5.3.0', $this->options['lit_mcp_pods_check_report']['version']);
self::assertSame('5.3.0', $store->latest()?->version);
self::assertNull((new CheckReportStore('divi', 'divi_version'))->latest());
}
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Compatibility;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
final class CompatibilityCheckTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('wp_generate_uuid4')->justReturn('uuid-1');
}
/** @param list<Manifest> $manifests */
private static function profile(array $manifests = [], ?Closure $extraSteps = null, ?Closure $stateKey = null): Profile
{
return new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: [FakeCapability::Read],
manifests: $manifests,
version: static fn (): ?string => '2.1.0',
stateKey: $stateKey,
extraSteps: $extraSteps
);
}
private static function withMirrors(): Manifest
{
return new Manifest('Fake', mirrors: [new Mirror('m', 'f.php', 'fn', null, 'x')]);
}
/** @param array<string, Closure(): bool> $code */
private static function check(Profile $profile, array $code = [], array $mirrors = []): ManifestCheck
{
$check = Mockery::mock(ManifestCheck::class);
$check->allows('codeChecks')->andReturn($code);
$check->allows('registrationChecks')->andReturn([]);
$check->allows('mirrorChecks')->andReturn($mirrors);
return $check;
}
/** @return list<string> */
private static function keys(CompatibilityCheck $check): array
{
return array_map(static fn (CheckStep $step): string => $step->key, $check->run(7)->steps);
}
public function testAProfileWithoutMirrorsGetsOnlyTheManifestStep(): void
{
$profile = self::profile([new Manifest('Fake')]);
$report = (new CompatibilityCheck($profile, self::check($profile)))->run(7);
self::assertTrue($report->green());
self::assertSame(['manifest'], array_map(static fn (CheckStep $s): string => $s->key, $report->steps));
self::assertSame('Fake-Funktionen, Klassen und Registrierungen', $report->steps[0]->label());
self::assertSame(['uuid-1', 7, '2.1.0', Plugin::VERSION, ''], [
$report->id, $report->userId, $report->version, $report->pluginVersion, $report->stateKey,
]);
}
public function testAManifestWithMirrorsAddsTheFingerprintStep(): void
{
$profile = self::profile([self::withMirrors()], stateKey: static fn (string $v): string => 'key-' . $v);
$check = new CompatibilityCheck($profile, self::check($profile, [], ['TB: Nachbau von y()' => static fn (): bool => false]));
$report = $check->run(7);
self::assertSame(['manifest', 'mirrors'], array_map(static fn (CheckStep $s): string => $s->key, $report->steps));
self::assertSame(['TB: Nachbau von y()'], $report->step(CheckStep::MIRRORS)?->problems);
self::assertSame('Fingerabdrücke nachgebauter Fake-Funktionen', $report->step(CheckStep::MIRRORS)?->label());
self::assertSame('key-2.1.0', $report->stateKey);
self::assertFalse($report->green());
}
public function testExtraStepsFollowAndKnowWhetherTheManifestIsClean(): void
{
$seen = [];
$extra = static function (bool $clean) use (&$seen): array {
$seen[] = $clean;
return [new CheckStep('extra', [])];
};
$profile = self::profile([self::withMirrors()], $extra);
$clean = new CompatibilityCheck($profile, self::check($profile));
$broken = new CompatibilityCheck($profile, self::check($profile, ['Fake: f()' => static fn (): bool => false]));
self::assertSame(['manifest', 'mirrors', 'extra'], self::keys($clean));
self::assertSame(['manifest', 'mirrors', 'extra'], self::keys($broken));
self::assertSame([true, false], $seen);
}
public function testAThrowingManifestCheckIsARedStep(): void
{
$profile = self::profile([new Manifest('Fake')]);
$manifests = Mockery::mock(ManifestCheck::class);
$manifests->allows('codeChecks')->andThrow(new RuntimeException('kaputt'));
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$report = (new CompatibilityCheck($profile, $manifests))->run(7);
$logged = (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
self::assertSame(
['Prüfschritt abgebrochen (Details im PHP-Error-Log).'],
$report->step(CheckStep::MANIFEST)?->problems
);
self::assertStringContainsString('kaputt', $logged, 'the exception text goes to the error log');
}
public function testForProfileUsesTheManifestCheckOfTheProfile(): void
{
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::ReportOnly,
capabilities: [FakeCapability::Read],
manifestCheck: static fn (): ManifestCheck => new ManifestCheck(
[new Manifest('Fake', [new \LIT\MCP\Compatibility\Call('lit_mcp_missing_fn', 0)])],
signatures: false
)
);
$report = CompatibilityCheck::forProfile($profile)->run(1);
self::assertSame(['Fake: lit_mcp_missing_fn()'], $report->step(CheckStep::MANIFEST)?->problems);
}
public function testCompatibilityChecksAProfileWithoutManifestCheckClosureByItsManifests(): void
{
$profile = self::profile([new Manifest('Fake', [new \LIT\MCP\Compatibility\Call('lit_mcp_missing_fn', 0)])]);
$report = (new Compatibility([$profile]))->check($profile, 3);
self::assertSame(3, $report->userId);
self::assertSame(['Fake: lit_mcp_missing_fn()'], $report->step(CheckStep::MANIFEST)?->problems);
}
}
@@ -22,16 +22,16 @@ final class CompatibilityComponentTest extends TestCase
private function profile(bool $active, bool $broken = false): Profile
{
return new Profile(
'fake',
'Fake Plugin',
Tier::LockOnFailure,
[FakeCapability::Read, FakeCapability::Write],
[new Manifest(
id: 'fake',
label: 'Fake Plugin',
tier: Tier::LockOnFailure,
capabilities: [FakeCapability::Read, FakeCapability::Write],
manifests: [new Manifest(
'Fake',
calls: $broken ? [new Call('lit_mcp_missing_function', 0, [FakeCapability::Write])] : []
)],
static fn (): bool => $active,
static fn (): string => '4.2.0'
active: static fn (): bool => $active,
version: static fn (): string => '4.2.0'
);
}
@@ -85,10 +85,10 @@ final class CompatibilityComponentTest extends TestCase
public function testAReleasePerVersionProfileHasItsOwnComponent(): void
{
$profile = new Profile(
'rel',
'Rel',
Tier::ReleasePerVersion,
[FakeCapability::Other],
id: 'rel',
label: 'Rel',
tier: Tier::ReleasePerVersion,
capabilities: [FakeCapability::Other],
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Other])
);
@@ -101,10 +101,10 @@ final class CompatibilityComponentTest extends TestCase
{
$plain = $this->profile(true);
$release = new Profile(
'rel',
'Rel',
Tier::ReleasePerVersion,
[FakeCapability::Other],
id: 'rel',
label: 'Rel',
tier: Tier::ReleasePerVersion,
capabilities: [FakeCapability::Other],
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Other])
);
+158 -24
View File
@@ -6,6 +6,8 @@ namespace LIT\MCP\Tests\Unit\Compatibility;
use Closure;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\Manifest;
@@ -13,6 +15,7 @@ use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Unit\TestCase;
use LogicException;
@@ -36,26 +39,26 @@ final class CompatibilityTest extends TestCase
Tier $tier = Tier::LockOnFailure
): Profile {
return new Profile(
'fake',
'Fake Plugin',
$tier,
self::CAPS,
$broken ? [self::brokenManifest()] : [],
$active,
$version,
$minVersion
id: 'fake',
label: 'Fake Plugin',
tier: $tier,
capabilities: self::CAPS,
manifests: $broken ? [self::brokenManifest()] : [],
active: $active,
version: $version,
minVersion: $minVersion
);
}
public function testReportOnlyNeverLocksNotEvenWhenInactiveOrBroken(): void
{
$profile = new Profile(
'fake',
'Fake',
Tier::ReportOnly,
self::CAPS,
[self::brokenManifest()],
static fn (): bool => false,
id: 'fake',
label: 'Fake',
tier: Tier::ReportOnly,
capabilities: self::CAPS,
manifests: [self::brokenManifest()],
active: static fn (): bool => false,
minVersion: '9'
);
$compat = new Compatibility([$profile]);
@@ -155,10 +158,10 @@ final class CompatibilityTest extends TestCase
public function testReleasePerVersionTakesItsMatrixFromTheClosure(): void
{
$profile = new Profile(
'fake',
'Fake',
Tier::ReleasePerVersion,
self::CAPS,
id: 'fake',
label: 'Fake',
tier: Tier::ReleasePerVersion,
capabilities: self::CAPS,
matrix: static fn (): CompatibilityMatrix => CompatibilityMatrix::allIncompatible(self::CAPS, 'gesperrt')
);
$compat = new Compatibility([$profile]);
@@ -173,10 +176,10 @@ final class CompatibilityTest extends TestCase
$matrices = 0;
$lock = self::lockProfile();
$release = new Profile(
'release',
'Release',
Tier::ReleasePerVersion,
[FakeCapability::Other],
id: 'release',
label: 'Release',
tier: Tier::ReleasePerVersion,
capabilities: [FakeCapability::Other],
matrix: static function () use (&$matrices): CompatibilityMatrix {
$matrices++;
@@ -203,7 +206,7 @@ final class CompatibilityTest extends TestCase
public function testLookups(): void
{
$lock = self::lockProfile();
$other = new Profile('other', 'Other', Tier::ReportOnly, [FakeCapability::Other]);
$other = new Profile(id: 'other', label: 'Other', tier: Tier::ReportOnly, capabilities: [FakeCapability::Other]);
$compat = new Compatibility([$lock, $other]);
self::assertSame([$lock, $other], $compat->profiles());
@@ -222,7 +225,7 @@ final class CompatibilityTest extends TestCase
new Compatibility([
self::lockProfile(),
new Profile('other', 'Other', Tier::ReportOnly, [FakeCapability::Read]),
new Profile(id: 'other', label: 'Other', tier: Tier::ReportOnly, capabilities: [FakeCapability::Read]),
]);
}
@@ -230,4 +233,135 @@ final class CompatibilityTest extends TestCase
{
self::assertFalse((new Compatibility([]))->usable(FakeCapability::Read));
}
private static function report(bool $green, string $version = '2.1.0', string $stateKey = 'k1'): CheckReport
{
return new CheckReport('r1', 'd', 1, $version, Plugin::VERSION, $stateKey, [
new CheckStep('probe', $green ? [] : ['Schreibprobe fehlgeschlagen']),
]);
}
private static function reported(?CheckReport $report, bool $withStateKey = true): Compatibility
{
$profile = new Profile(
id: 'fake',
label: 'Fake Plugin',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
version: static fn (): ?string => '2.1.0',
stateKey: $withStateKey ? static fn (string $version): string => 'k1' : null
);
return new Compatibility([$profile], report: static fn (Profile $p): ?CheckReport => $report);
}
public function testARedReportOfTheCurrentStateLocksEveryCapability(): void
{
$compat = self::reported(self::report(false));
self::assertSame(ReleaseState::Incompatible, $compat->matrix($compat->profile('fake'))->state());
self::assertNotNull($compat->lockOf(self::CAPS));
$reason = implode(' ', $compat->problems($compat->profile('fake')));
self::assertStringContainsString('(probe)', $reason);
self::assertStringNotContainsString('Schreibprobe fehlgeschlagen', $reason, 'step texts may hold exception text');
}
public function testAReportWithoutStepsIsNoResultAndDoesNotLock(): void
{
$empty = new CheckReport('r1', 'd', 1, '2.1.0', Plugin::VERSION, 'k1', []);
self::assertNull(self::reported($empty)->lockOf(self::CAPS));
}
public function testActiveAndVersionRunTheirClosuresOncePerInstance(): void
{
$calls = 0;
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
active: static function () use (&$calls): bool {
$calls++;
return true;
},
version: static function () use (&$calls): string {
$calls++;
return '1.0';
}
);
$compat = new Compatibility([$profile]);
foreach ([1, 2, 3] as $ignored) {
self::assertTrue($compat->active($profile));
self::assertSame('1.0', $compat->version($profile));
}
self::assertSame(2, $calls);
}
public function testAThrowingActiveClosureIsLoggedOnlyOnce(): void
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
active: static fn (): bool => throw new RuntimeException('einmal')
);
$compat = new Compatibility([$profile]);
try {
$compat->active($profile);
$compat->active($profile);
$compat->lockOf(self::CAPS);
$logged = (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
self::assertSame(1, substr_count($logged, 'einmal'));
}
public function testAnOtherStateOrAGreenReportOrNoneDoesNotLock(): void
{
foreach ([self::report(false, stateKey: 'old'), self::report(false, version: '2.0.0'), self::report(true), null] as $report) {
self::assertNull(self::reported($report)->lockOf(self::CAPS));
}
}
public function testWithoutAStateKeyTheVersionDecides(): void
{
self::assertNotNull(self::reported(self::report(false, stateKey: ''), false)->lockOf(self::CAPS));
self::assertNull(self::reported(self::report(false, version: '9', stateKey: ''), false)->lockOf(self::CAPS));
}
public function testACaughtThrowableIsLogged(): void
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
active: static fn (): bool => throw new RuntimeException('geheim-xyz')
);
try {
self::assertFalse((new Compatibility([$profile]))->active($profile));
$logged = (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
self::assertStringContainsString('compat fake failed', $logged);
self::assertStringContainsString('geheim-xyz', $logged);
}
}
+49 -6
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Compatibility;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Tests\Support\FakeCapability;
@@ -17,10 +18,10 @@ final class ProfileTest extends TestCase
{
$matrix = static fn (): CompatibilityMatrix => CompatibilityMatrix::allReleased([FakeCapability::Read]);
$profile = new Profile(
'fake',
'Fake Plugin',
Tier::ReleasePerVersion,
[FakeCapability::Read],
id: 'fake',
label: 'Fake Plugin',
tier: Tier::ReleasePerVersion,
capabilities: [FakeCapability::Read],
matrix: $matrix,
minVersion: '2.0'
);
@@ -34,12 +35,54 @@ final class ProfileTest extends TestCase
self::assertSame([], $profile->manifests);
}
public function testTheStoredVersionKeyDefaultsToVersionAndIsKeptWhenGiven(): void
{
$plain = new Profile(id: 'fake', label: 'Fake', tier: Tier::ReportOnly, capabilities: [FakeCapability::Read]);
$divi = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::ReportOnly,
capabilities: [FakeCapability::Read],
reportVersionKey: 'divi_version'
);
self::assertSame('version', $plain->reportVersionKey);
self::assertSame('divi_version', $divi->reportVersionKey);
}
public function testTheReleaseFieldsAreOptionalAndKeptWhenGiven(): void
{
$bare = new Profile(id: 'fake', label: 'Fake', tier: Tier::ReportOnly, capabilities: [FakeCapability::Read]);
self::assertSame([], $bare->testedVersions);
self::assertNull($bare->stateKey);
self::assertNull($bare->extraSteps);
self::assertNull($bare->manifestCheck);
$stateKey = static fn (string $version): string => 'k' . $version;
$extraSteps = static fn (bool $manifestClean): array => [];
$manifestCheck = static fn (): ManifestCheck => new ManifestCheck([], signatures: false);
$full = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::ReportOnly,
capabilities: [FakeCapability::Read],
testedVersions: ['1.0'],
stateKey: $stateKey,
extraSteps: $extraSteps,
manifestCheck: $manifestCheck
);
self::assertSame(['1.0'], $full->testedVersions);
self::assertSame($stateKey, $full->stateKey);
self::assertSame($extraSteps, $full->extraSteps);
self::assertSame($manifestCheck, $full->manifestCheck);
}
public function testAReleasePerVersionProfileWithoutMatrixIsAProgrammingError(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('needs a matrix closure');
new Profile('fake', 'Fake', Tier::ReleasePerVersion, [FakeCapability::Read]);
new Profile(id: 'fake', label: 'Fake', tier: Tier::ReleasePerVersion, capabilities: [FakeCapability::Read]);
}
public function testCapabilityIdsMustBeUniqueWithinAProfile(): void
@@ -47,7 +90,7 @@ final class ProfileTest extends TestCase
$this->expectException(LogicException::class);
$this->expectExceptionMessage('fake-read');
new Profile('fake', 'Fake', Tier::ReportOnly, [FakeCapability::Read, FakeCapability::Read]);
new Profile(id: 'fake', label: 'Fake', tier: Tier::ReportOnly, capabilities: [FakeCapability::Read, FakeCapability::Read]);
}
public function testTheTiersHaveStableValues(): void
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Compatibility;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Tests\Unit\TestCase;
final class ReleaseStoreTest extends TestCase
{
/** @var array<string, mixed> */
private array $options = [];
protected function setUp(): void
{
parent::setUp();
$this->options = [];
Functions\when('get_option')->alias(
fn (string $name, mixed $default = false): mixed => $this->options[$name] ?? $default
);
Functions\when('update_option')->alias(function (string $name, mixed $value, mixed $autoload = null): bool {
$this->options[$name] = $value;
self::assertFalse($autoload, 'releases are read only by the gate – no autoload');
return true;
});
}
public function testAddsFindsAndWithdrawsPerVersionAndPluginVersion(): void
{
$store = new ReleaseStore('divi', 'divi_version');
$store->add('5.3.0', '0.6.0', 1, 'r1');
$store->add('5.3.0', '0.6.0', 2, 'r2');
self::assertCount(1, $store->entries(), 'one entry per version pair');
self::assertSame(2, $store->find('5.3.0', '0.6.0')['user_id'] ?? null);
self::assertNull($store->find('5.3.0', '0.7.0'), 'another plugin version needs its own check');
self::assertTrue($store->withdraw('5.3.0', '0.6.0'));
self::assertFalse($store->withdraw('5.3.0', '0.6.0'));
self::assertSame([], $store->entries());
}
public function testDiviKeepsOptionAndFormat(): void
{
$this->options['lit_mcp_divi_releases'] = [[
'divi_version' => '5.2.2', 'plugin_version' => '0.6.0', 'user_id' => 3,
'released_at' => '2026-10-01T08:00:00+00:00', 'report_id' => 'r',
]];
$store = new ReleaseStore('divi', 'divi_version');
self::assertSame('r', $store->find('5.2.2', '0.6.0')['report_id'] ?? null);
$store->add('5.3.0', '0.6.0', 1, 'r1');
$written = $this->options['lit_mcp_divi_releases'];
self::assertSame(['lit_mcp_divi_releases'], array_keys($this->options));
self::assertSame(
['divi_version', 'plugin_version', 'user_id', 'released_at', 'report_id'],
array_keys($written[1])
);
self::assertSame($this->options['lit_mcp_divi_releases'][0], [
'divi_version' => '5.2.2', 'plugin_version' => '0.6.0', 'user_id' => 3,
'released_at' => '2026-10-01T08:00:00+00:00', 'report_id' => 'r',
]);
}
public function testAnotherProfileGetsItsOwnOptionAndTheVersionKey(): void
{
$store = new ReleaseStore('pods');
$store->add('3.3.9.2', '0.6.0', 1, 'r1');
self::assertSame(['lit_mcp_pods_releases'], array_keys($this->options));
self::assertSame('3.3.9.2', $this->options['lit_mcp_pods_releases'][0]['version']);
self::assertSame('r1', $store->find('3.3.9.2', '0.6.0')['report_id'] ?? null);
self::assertSame([], (new ReleaseStore('divi', 'divi_version'))->entries());
}
public function testIgnoresBrokenRows(): void
{
$this->options['lit_mcp_divi_releases'] = ['x', ['divi_version' => '5.3.0'], [
'divi_version' => '5.2.2', 'plugin_version' => '0.6.0', 'user_id' => 3,
'released_at' => 'd', 'report_id' => 'r',
]];
self::assertSame(['5.2.2'], array_column((new ReleaseStore('divi', 'divi_version'))->entries(), 'divi_version'));
self::assertSame([], (new ReleaseStore('divi'))->entries(), 'rows hold divi_version, not version');
}
}
@@ -9,15 +9,16 @@ use Brain\Monkey\Functions;
use LIT\MCP\Admin\AdminPostAction;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeChangeLog;
@@ -63,7 +64,7 @@ final class DiviTabTest extends TestCase
{
$steps = array_map(
static fn (string $step): CheckStep => new CheckStep($step, $green ? [] : ['rot']),
[CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE]
[CheckStep::MANIFEST, CheckStep::MIRRORS, DiviCheckSteps::CATALOG, DiviCheckSteps::FORMAT, DiviCheckSteps::WRITE]
);
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps);
@@ -7,10 +7,11 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Cli;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviReference;
@@ -74,7 +75,7 @@ final class DiviCheckCommandTest extends TestCase
): DiviCheckCommand {
$steps = array_map(
static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []),
[CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE]
[CheckStep::MANIFEST, CheckStep::MIRRORS, DiviCheckSteps::CATALOG, DiviCheckSteps::FORMAT, DiviCheckSteps::WRITE]
);
$check = Mockery::mock(CompatibilityCheck::class);
$check->allows('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', $steps));
@@ -121,14 +122,14 @@ final class DiviCheckCommandTest extends TestCase
public function testFailsWhenAStepIsRed(): void
{
self::assertSame(1, $this->command([CheckStep::CATALOG => ['divi/text: unbekanntes Modul']])->run([]));
self::assertSame(1, $this->command([DiviCheckSteps::CATALOG => ['divi/text: unbekanntes Modul']])->run([]));
self::assertContains('✗ Modulkatalog (Kurzform-Pfade)', $this->lines);
self::assertContains(' divi/text: unbekanntes Modul', $this->lines);
}
public function testUpdateReferenceRefusesWhenMoreThanFingerprintsIsRed(): void
{
$code = $this->command([CheckStep::FORMAT => ['kaputt']])->run(['update-reference' => true]);
$code = $this->command([DiviCheckSteps::FORMAT => ['kaputt']])->run(['update-reference' => true]);
self::assertSame(1, $code);
self::assertFileDoesNotExist($this->expectedFile);
@@ -324,7 +325,7 @@ final class DiviCheckCommandTest extends TestCase
public function testARedReportKeepsExitOneWhateverTheMatrixSays(): void
{
$red = [CheckStep::CATALOG => ['divi/text: unbekanntes Modul']];
$red = [DiviCheckSteps::CATALOG => ['divi/text: unbekanntes Modul']];
$incompatible = MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Incompatible, 'auto', 'x');
self::assertSame(1, $this->command($red)->run([]));
@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use PHPUnit\Framework\TestCase;
final class CheckReportTest extends TestCase
{
private static function report(array $steps): CheckReport
{
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '5.3.0', '0.6.0', 'key', $steps);
}
private static function green(): array
{
return array_map(
static fn (string $key): CheckStep => new CheckStep($key, []),
[CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE]
);
}
public function testGreenOnlyWithEveryStepGreenAndNoneSkipped(): void
{
self::assertTrue(self::report(self::green())->green());
self::assertFalse(self::report([])->green());
$red = self::green();
$red[2] = new CheckStep(CheckStep::CATALOG, ['divi/text: unbekanntes Modul']);
self::assertFalse(self::report($red)->green());
$skipped = self::green();
$skipped[4] = new CheckStep(CheckStep::WRITE, [], true);
self::assertFalse(self::report($skipped)->green());
}
public function testFreshOnlyForTheSameVersionsAndStateKey(): void
{
$report = self::report(self::green());
self::assertTrue($report->freshFor('5.3.0', '0.6.0', 'key'));
self::assertFalse($report->freshFor('5.3.1', '0.6.0', 'key'), 'Divi updated');
self::assertFalse($report->freshFor('5.3.0', '0.7.0', 'key'), 'plugin updated');
self::assertFalse($report->freshFor('5.3.0', '0.6.0', 'other'), 'a Divi file changed');
}
public function testRoundTripsThroughAnArray(): void
{
$steps = self::green();
$steps[1] = new CheckStep(CheckStep::MIRRORS, ['Theme Builder: Nachbau von x() – y']);
$report = self::report($steps);
self::assertEquals($report, CheckReport::fromArray($report->toArray()));
self::assertNull(CheckReport::fromArray('broken'));
self::assertNull(CheckReport::fromArray(['id' => 'r1']));
self::assertSame('Fingerabdrücke nachgebauter Divi-Funktionen', $report->step(CheckStep::MIRRORS)?->label());
}
}
@@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Integrations\Divi\Release\FormatCheck;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
final class CompatibilityCheckTest extends TestCase
{
private function check(
array $code,
array $mirrors,
?WriteProbe $probe = null,
?\Closure $catalog = null
): CompatibilityCheck {
Functions\when('wp_generate_uuid4')->justReturn('uuid-1');
$manifests = Mockery::mock(ManifestCheck::class);
$manifests->allows('codeChecks')->andReturn($code);
$manifests->allows('registrationChecks')->andReturn([]);
$manifests->allows('mirrorChecks')->andReturn($mirrors);
$format = Mockery::mock(FormatCheck::class);
$format->allows('problems')->andReturn([]);
if ($probe === null) {
$probe = Mockery::mock(WriteProbe::class);
$probe->allows('run')->andReturn([]);
}
return new CompatibilityCheck(
$manifests,
$catalog ?? static fn (): array => [],
$format,
$probe,
static fn (): string => '5.3.0',
static fn (string $version): string => 'key-' . $version
);
}
public function testAGreenRunNamesVersionsAndStateKey(): void
{
$report = $this->check([], [])->run(7);
self::assertTrue($report->green());
self::assertSame(['uuid-1', 7, '5.3.0', Plugin::VERSION, 'key-5.3.0'], [
$report->id, $report->userId, $report->diviVersion, $report->pluginVersion, $report->stateKey,
]);
self::assertSame(
['manifest', 'mirrors', 'catalog', 'format', 'write'],
array_map(static fn (CheckStep $step): string => $step->key, $report->steps)
);
}
public function testNoWriteTestWhenDiviFunctionsAreMissing(): void
{
$probe = Mockery::mock(WriteProbe::class);
$probe->expects('run')->never();
$report = $this->check(['Seiten: et_x()' => static fn (): bool => false], [], $probe)->run(7);
self::assertSame(['Seiten: et_x()'], $report->step(CheckStep::MANIFEST)?->problems);
self::assertTrue($report->step(CheckStep::WRITE)?->skipped);
}
public function testChangedFingerprintsStillRunTheWriteTest(): void
{
$probe = Mockery::mock(WriteProbe::class);
$probe->expects('run')->once()->andReturn([]);
$report = $this->check([], ['TB: Nachbau von y()' => static fn (): bool => false], $probe)->run(7);
self::assertFalse($report->green());
self::assertTrue($report->step(CheckStep::WRITE)?->green());
}
public function testAThrowingStepIsARedStep(): void
{
$report = $this->check([], [], null, static function (): never {
throw new RuntimeException('Katalog weg');
})->run(7);
self::assertSame(['Prüfschritt abgebrochen: Katalog weg'], $report->step(CheckStep::CATALOG)?->problems);
}
}
@@ -5,9 +5,9 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Mirror;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FormatCheck;
use LIT\MCP\Tests\Unit\TestCase;
use Brain\Monkey\Functions;
use Closure;
use Mockery;
use RuntimeException;
final class DiviCheckStepsTest extends TestCase
{
public function testAStepCarriesItsLabel(): void
{
$step = DiviCheckSteps::step(DiviCheckSteps::CATALOG, ['x']);
self::assertSame('catalog', $step->key);
self::assertSame(['x'], $step->problems);
self::assertSame('Modulkatalog (Kurzform-Pfade)', $step->label());
self::assertTrue(DiviCheckSteps::step(DiviCheckSteps::WRITE, [], true)->skipped);
}
public function testTheLabelOfAnOldReportStepComesFromTheKey(): void
{
self::assertSame(
'Schreibtest auf einer Wegwerfseite',
DiviCheckSteps::label(new CheckStep(DiviCheckSteps::WRITE, []))
);
self::assertSame(
'Divi-Funktionen, Klassen und Registrierungen',
DiviCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
);
self::assertSame('stored', DiviCheckSteps::label(new CheckStep('catalog', [], false, 'stored')));
self::assertSame('other', DiviCheckSteps::label(new CheckStep('other', [])));
}
/** @param array<string, Closure(): bool> $code */
private function check(array $code, array $mirrors, ?Closure $probe = null, ?Closure $catalog = null): CompatibilityCheck
{
Functions\when('wp_generate_uuid4')->justReturn('uuid-1');
$manifests = Mockery::mock(ManifestCheck::class);
$manifests->allows('codeChecks')->andReturn($code);
$manifests->allows('registrationChecks')->andReturn([]);
$manifests->allows('mirrorChecks')->andReturn($mirrors);
$format = Mockery::mock(FormatCheck::class);
$format->allows('problems')->andReturn([]);
$profile = new Profile(
id: 'divi',
label: 'Divi',
tier: Tier::ReleasePerVersion,
capabilities: DiviFeature::cases(),
manifests: [new Manifest('Seiten', mirrors: [new Mirror('m', 'f.php', 'fn', null, 'x')])],
version: static fn (): ?string => '5.3.0',
matrix: static fn () => throw new RuntimeException('unused'),
stateKey: static fn (string $version): string => 'key-' . $version,
extraSteps: DiviCheckSteps::extra(
$catalog ?? static fn (): array => [],
$format,
$probe ?? static fn (): array => []
)
);
return new CompatibilityCheck($profile, $manifests);
}
public function testTheDiviStepOrderIsUnchanged(): void
{
$report = $this->check([], [])->run(7);
self::assertTrue($report->green());
self::assertSame(
['manifest', 'mirrors', 'catalog', 'format', 'write'],
array_map(static fn (CheckStep $step): string => $step->key, $report->steps)
);
self::assertSame('Schreibtest auf einer Wegwerfseite', $report->step('write')?->label());
self::assertSame('Divi-Funktionen, Klassen und Registrierungen', $report->step('manifest')?->label());
self::assertSame('key-5.3.0', $report->stateKey);
}
public function testNoWriteTestWhenDiviFunctionsAreMissing(): void
{
$ran = false;
$probe = static function () use (&$ran): array {
$ran = true;
return [];
};
$report = $this->check(['Seiten: et_x()' => static fn (): bool => false], [], $probe)->run(7);
self::assertSame(['Seiten: et_x()'], $report->step(CheckStep::MANIFEST)?->problems);
self::assertTrue($report->step(DiviCheckSteps::WRITE)?->skipped);
self::assertFalse($ran);
}
public function testChangedFingerprintsStillRunTheWriteTest(): void
{
$report = $this->check([], ['TB: Nachbau von y()' => static fn (): bool => false])->run(7);
self::assertFalse($report->green());
self::assertTrue($report->step(DiviCheckSteps::WRITE)?->green());
}
public function testAThrowingStepIsARedStep(): void
{
$report = $this->check([], [], null, static function (): never {
throw new RuntimeException('Katalog weg');
})->run(7);
self::assertSame(['Prüfschritt abgebrochen (Details im PHP-Error-Log).'], $report->step(DiviCheckSteps::CATALOG)?->problems);
}
}
@@ -5,12 +5,15 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\DiviReleases;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Tests\Support\ReleaseMatrices;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -32,6 +35,9 @@ final class DiviProfileTest extends TestCase
self::assertSame('divi', $profile->id);
self::assertSame('Divi', $profile->label);
self::assertSame('divi_version', $profile->reportVersionKey, 'stored Divi data keeps its key');
self::assertSame(DiviProfile::ID, $profile->id);
self::assertSame(DiviProfile::REPORT_VERSION_KEY, $profile->reportVersionKey);
self::assertSame(Tier::ReleasePerVersion, $profile->tier);
self::assertSame(DiviFeature::cases(), $profile->capabilities);
self::assertEquals(array_values(DiviAdapters::manifests()), $profile->manifests);
@@ -45,14 +51,25 @@ final class DiviProfileTest extends TestCase
self::assertSame($status->matrix, (DiviProfile::profile()->matrix)());
}
public function testTheVersionComesFromTheDiviTemplateAndThereIsNoActiveCheck(): void
public function testTheVersionComesFromTheDiviTemplateAndActiveMeansDiviIsInstalled(): void
{
$this->theme('Divi', '5.3.0');
$profile = DiviProfile::profile();
self::assertNull($profile->active, 'Divi has its own component; DiviIntegration decides availability');
self::assertTrue(($profile->active)());
self::assertSame('5.3.0', ($profile->version)());
$this->theme('twentytwentyfive');
self::assertFalse(($profile->active)());
self::assertNull(($profile->version)());
}
public function testCarriesTestedVersionsStateKeyAndManifestCheck(): void
{
$this->theme('twentytwentyfive');
$profile = DiviProfile::profile();
self::assertSame(DiviReleases::TESTED, $profile->testedVersions);
self::assertSame(ReleaseResolver::stateKey('5.3.0'), ($profile->stateKey)('5.3.0'));
self::assertInstanceOf(ManifestCheck::class, ($profile->manifestCheck)());
}
}
@@ -12,7 +12,7 @@ use LIT\MCP\Compatibility\ManifestCheck;
use LIT\MCP\Compatibility\Problem;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Tests\Unit\TestCase;
final class ReleaseStoreTest extends TestCase
{
/** @var array<string, mixed> */
private array $options = [];
protected function setUp(): void
{
parent::setUp();
$this->options = [];
Functions\when('get_option')->alias(
fn (string $name, mixed $default = false): mixed => $this->options[$name] ?? $default
);
Functions\when('update_option')->alias(function (string $name, mixed $value, mixed $autoload = null): bool {
$this->options[$name] = $value;
self::assertFalse($autoload, 'releases are read only by the gate – no autoload');
return true;
});
}
public function testAddsFindsAndWithdrawsPerDiviAndPluginVersion(): void
{
$store = new ReleaseStore();
$store->add('5.3.0', '0.6.0', 1, 'r1');
$store->add('5.3.0', '0.6.0', 2, 'r2');
self::assertCount(1, $store->entries(), 'one entry per version pair');
self::assertSame(2, $store->find('5.3.0', '0.6.0')['user_id'] ?? null);
self::assertNull($store->find('5.3.0', '0.7.0'), 'another plugin version needs its own check');
self::assertTrue($store->withdraw('5.3.0', '0.6.0'));
self::assertFalse($store->withdraw('5.3.0', '0.6.0'));
self::assertSame([], $store->entries());
}
public function testIgnoresBrokenRows(): void
{
$this->options[ReleaseStore::OPTION] = ['x', ['divi_version' => '5.3.0'], [
'divi_version' => '5.2.2', 'plugin_version' => '0.6.0', 'user_id' => 3,
'released_at' => 'd', 'report_id' => 'r',
]];
self::assertSame(['5.2.2'], array_column((new ReleaseStore())->entries(), 'divi_version'));
}
}
@@ -97,9 +97,18 @@ final class WriteProbeTest extends TestCase
throw new RuntimeException('Composer kaputt');
});
$problems = (new WriteProbe($composer, $this->renderer(''), '/nonexistent-divi'))->run();
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$problems = (new WriteProbe($composer, $this->renderer(''), '/nonexistent-divi'))->run();
$logged = (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
self::assertSame(['Schreibtest abgebrochen: Composer kaputt'], $problems);
self::assertSame(['Schreibtest abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('Composer kaputt', $logged);
self::assertSame([41], $this->deleted);
$this->expectException(\LIT\MCP\Integrations\Divi\Release\DiviLocked::class);
ReleaseGate::assertFeature(DiviFeature::PageWrite, 41);
@@ -112,8 +112,8 @@ final class IntegrationRegistryTest extends TestCase
public function testCompatibilitiesOfAllProvidersInRegistryOrderEvenUnavailable(): void
{
$first = new Profile('a', 'A', Tier::ReportOnly, []);
$second = new Profile('b', 'B', Tier::ReportOnly, []);
$first = new Profile(id: 'a', label: 'A', tier: Tier::ReportOnly, capabilities: []);
$second = new Profile(id: 'b', label: 'B', tier: Tier::ReportOnly, capabilities: []);
$provider = static function (string $id, bool $available, array $profiles): Integration {
$integration = Mockery::mock(Integration::class . ', ' . ProvidesCompatibility::class);
$integration->allows('id')->andReturn($id);
+1
View File
@@ -35,6 +35,7 @@ final class PodsCompatibilityTest extends TestCase
self::assertSame('Pods', $profile->label);
self::assertSame(Tier::LockOnFailure, $profile->tier);
self::assertSame('2.8', $profile->minVersion);
self::assertSame(['3.3.9.2'], $profile->testedVersions);
self::assertSame(PodsFeature::cases(), $profile->capabilities);
}
+4 -4
View File
@@ -145,10 +145,10 @@ final class PluginTest extends TestCase
private static function podsCompat(bool $usable): Compatibility
{
return new Compatibility([new Profile(
'pods',
'Pods',
Tier::LockOnFailure,
PodsFeature::cases(),
id: 'pods',
label: 'Pods',
tier: Tier::LockOnFailure,
capabilities: PodsFeature::cases(),
active: static fn (): bool => $usable
)]);
}
+2 -3
View File
@@ -11,10 +11,8 @@ use LIT\MCP\Access;
use LIT\MCP\Admin\ConnectionStatus;
use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Limits;
use LIT\MCP\Updater\RecheckOnInstall;
use Mockery;
@@ -56,7 +54,8 @@ final class UninstallTest extends TestCase
$own = [
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
ReleaseStore::OPTION, CheckReportStore::OPTION, DiviCheckRoute::LOCK,
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', DiviCheckRoute::LOCK,
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
+2 -3
View File
@@ -12,10 +12,8 @@ use LIT\MCP\Admin\ConnectionStatus;
use LIT\MCP\Admin\LogTab;
use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Limits;
use LIT\MCP\Uninstaller;
use LIT\MCP\Updater\RecheckOnInstall;
@@ -62,7 +60,8 @@ final class UninstallerTest extends TestCase
self::assertSame(
[
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
ReleaseStore::OPTION, CheckReportStore::OPTION, DiviCheckRoute::LOCK,
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', DiviCheckRoute::LOCK,
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),