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
@@ -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());
}
}