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:
co-authored by
Claude Opus 5.5
parent
c85131603b
commit
7d6a97a54e
@@ -46,10 +46,27 @@ mechanisch (Namespace + `use`), keine Verhaltensänderung. Keine Commits durch U
|
||||
| 1.12 | `PodsFeature` (`pods-schema-read`, `pods-field-read`, `pods-field-write`, `pods-field-permissions`), `PodsCompatibility::profile()` (LockOnFailure, PODS_VERSION, min 2.8, Calls mit Argumentzahlen gegen Pods 3.3.9.2), `PodsIntegration` ohne Abilities | `PodsCompatibilityTest`; Registry-Test: leere Integration nicht in `grouped()`/`labels()`/Instructions | keine leere Gruppe sichtbar | sonnet |
|
||||
| 1.13 | `CompatibilityComponent` (3 Zustände, Zeile je Fähigkeit); `EnvironmentProbe` ohne Pods (TEC bleibt); `Plugin`: Guards `[DiviFeatureGate, CapabilityGate]`, Komponenten per Closure aus der Registry; 0.3-Umzug (`unknownIds` entfällt, Test zieht in `CapabilityGateTest`); ID-Eindeutigkeit | `EnvironmentProbeTest`; Zwei-Gate-Test über `AbilityRegistrar::argsFor`; `PluginTest` | Reihenfolge Divi, Pods, TEC | sonnet |
|
||||
| 1.14 | `PodsFieldSource` + Schema-Wahl über `usable(PodsFeature::…)`; `isAvailable()` entfällt; Feldrechte über `usable(FieldPermissions)`, sonst fail-closed; 0.2-Bedingung = `active && ! usable(SchemaRead)` | `PodsFieldSourceTest` je Fähigkeit; `PluginTest` | alle Pods-Tests grün | sonnet |
|
||||
| 1.15 | `CompatibilityTab` + `CompatibilitySection` (Filter `lit_mcp_compat_sections`, lazy); Zustand (Wort + Icon) und Probleme je Fähigkeit für Nicht-`ReleasePerVersion`-Profile | `CompatibilityTabTest`: drei Zustände, Sektion eingebettet, escaped | `CoreIndependenceTest` grün | sonnet |
|
||||
| 1.16 | `DiviTab implements CompatibilitySection` (`title()` „Divi“); Slug/Redirects/`requested()`/Notice auf `compatibility`; `DiviIntegration` hängt über `lit_mcp_compat_sections` ein | `DiviTabTest`, `DiviIntegrationTest` | Release/Withdraw/Orphans/Render unverändert | sonnet |
|
||||
| 1.17 | Links/Texte: `DiviCheckRoute`, `ReleaseStatus:24`, `docs/guides/overview.md:96`, `divi-release-smoke.sh`, Docblock `DiviCheckCommand` | `DiviCheckRouteTest` `tab=compatibility`; `ReleaseStatusTest` „→ Compatibility“ | DDEV-Smoke grün | haiku |
|
||||
| 1.18 | Reiter in `Plugin::boot` registrieren (nach `LogTab`); Pods-Smoke erweitern | `PluginTest` Tab-Liste | DDEV: `compatibility ok`, Pods ohne Probleme; hohe minVersion → Pods-Felder schreibgeschützt | sonnet |
|
||||
**Erledigt 1.1–1.14:** 9beb55e, b511ef6, c851316.
|
||||
|
||||
**Neu-Zuschnitt 1.15–1.25 (Gate 1 vom 2026-11-06):** Prüfen und Freigeben sind für alle Profile im Reiter „Compatibility“
|
||||
gleich; die Wirkung regelt das Tier (ReleasePerVersion = Pflicht, LockOnFailure = dokumentiert, Nutzung automatisch solange grün,
|
||||
ReportOnly = nur Status). Optionen `lit_mcp_<id>_releases|_check_report|_check_lock` – für `divi` exakt die heutigen Namen.
|
||||
**Divi behält auch das Datenformat:** Store und Bericht bekommen den Versionsschlüssel als Parameter (Divi `divi_version`, neue
|
||||
Profile `version`) – keine Migration, Rollback-sicher. Profil-ID aus POST nur nach Allowlist (`Compatibility::profile()`) in
|
||||
Optionsnamen.
|
||||
|
||||
| 1.15 | `Profile`: `testedVersions`, `stateKey`, `extraSteps` (Closure(bool $manifestClean): list<CheckStep>), `manifestCheck`; `DiviProfile` + `active`, `testedVersions=DiviReleases::TESTED`, `stateKey=ReleaseResolver::stateKey`; Pods `testedVersions=['3.3.9.2']` | `ProfileTest`, `DiviProfileTest`, `PodsCompatibilityTest` | Gates grün | sonnet (mit 1.16–1.18) |
|
||||
| 1.16 | `CheckStep`, `CheckReport`, `CheckReportStore(profileId, versionKey)` nach `src/Compatibility`; Label mitgespeichert; Divi-Schrittkonstanten in `DiviCheckSteps` | `CheckReportTest` (Divi-Rundlauf unverändert, Option je ID) | Divi-Suite grün | sonnet |
|
||||
| 1.17 | `ReleaseStore(profileId, versionKey)` nach `src/Compatibility` | `ReleaseStoreTest` (Divi-Format unverändert, Option je ID) | `ReleaseResolverTest` grün | sonnet |
|
||||
| 1.18 | `CompatibilityCheck` generisch (Manifest, Fingerabdrücke nur mit Mirrors, dann `extraSteps`); `Compatibility::check(Profile)`; Divi liefert Katalog/Format/Schreibtest als `extraSteps` | `CompatibilityCheckTest`, `DiviCheckStepsTest` | `DiviCheckCommandTest` grün, Divi-Schrittfolge gleich | sonnet |
|
||||
| 1.18a | Review-Befunde (Architektur-Review 2026-11-06): roter gespeicherter Bericht für den aktuellen Stand sperrt LockOnFailure-Fähigkeiten bis zur nächsten grünen Prüfung; gefangene Ausnahmen in `Compatibility` per `Log::failure` protokollieren; `Profile` nur mit benannten Argumenten bauen | `CompatibilityTest` (roter/grüner/veralteter/kein Bericht) | Divi unverändert | sonnet |
|
||||
| 1.19 | `Admin\CheckRoute` je Profil (`/<id>-check`, Lock je Profil, Redirect `tab=compatibility`, `do_action('lit_mcp_compat_changed', $id)`; Divi invalidiert darauf `ReleaseGate`) | `CheckRouteTest`, `DiviIntegrationTest` | Lock-Tests portiert | sonnet |
|
||||
| 1.20 | `Admin\CompatibilityTab` read-only (Sektion je Profil, Zustand Wort+Icon, Tier-Hinweis, Fähigkeiten, Versionen, Bericht, Hook `lit_mcp_compat_profile_extra`; inaktiv ohne `matrix()`) | `CompatibilityTabTest` inkl. Escaping | `CoreIndependenceTest` grün | sonnet |
|
||||
| 1.21 | Formulare + admin-post `lit_mcp_compat_release|withdraw` (guard, Allowlist, Nonce je Profil) | `CompatibilityTabTest` (frischer Bericht, unbekanntes Profil, Nonce/403, Tier-Sichtbarkeit) | Release/Withdraw-Fälle aus `DiviTabTest` portiert | sonnet |
|
||||
| 1.22 | Reiter in `Plugin::boot` (nach `LimitsTab`), Hilfe-Tab | `PluginTest`, `SettingsPageTest` | grün | sonnet |
|
||||
| 1.23 | `DiviTab` → `DiviAdminExtras` (Notice, Orphans, Zusatzhinweise); `TABS_FILTER`-Eintrag entfällt | `DiviAdminExtrasTest`, `DiviIntegrationTest` | Boundary/Core grün | sonnet |
|
||||
| 1.24 | Texte/CSS (`lit-mcp-divi-*` → `lit-mcp-compat-*`), `ReleaseStatus:24`, `docs/guides/overview.md`, `Uninstaller` (`lit_mcp_pods_*`), `divi-release-smoke.sh` | `ReleaseStatusTest`, `UninstallerTest` | DDEV Divi-Smoke grün | sonnet |
|
||||
| 1.25 | Pods-Smoke: `/pods-check`, Freigabe schreibt `lit_mcp_pods_releases` ohne Sperrwirkung, hohe `minVersion` sperrt | Smoke rot ohne 1.19–1.21 | DDEV Pods- und Divi-Smoke grün | sonnet |
|
||||
|
||||
## Phase 2 – WP Super Cache 3.1.5
|
||||
|
||||
@@ -67,8 +84,9 @@ mechanisch (Namespace + `use`), keine Verhaltensänderung. Keine Commits durch U
|
||||
(1.2–1.4) · 6 `refactor(compat): CompatibilityMatrix generisch mit fromProblems` (1.5) · 7 `refactor(compat): Manifest und
|
||||
ManifestCheck generisch, DiviAreas, DiviManifestCheck` (1.6–1.7) · 8 `feat(compat): Tier, Profile, Compatibility,
|
||||
CapabilityLocked, CapabilityGate` (1.8–1.10) · 9 `feat(compat): Profile aus Integrationen, Divi- und Pods-Profil` (1.11–1.12) ·
|
||||
10 `feat(compat): Gates, Status und Pods über den Kern verdrahten` (1.13–1.14) · 11 `feat(admin): Reiter „Compatibility“ mit
|
||||
Divi als Abschnitt` (1.15–1.18) · 12 `feat(changelog): Hook lit_mcp_changed` (2.1) · 13 `feat(cache): WP Super Cache über den
|
||||
10 `feat(compat): Gates, Status und Pods über den Kern verdrahten` (1.13–1.14) · 11a `refactor(compat): Freigabe-Bausteine
|
||||
generisch` (1.15–1.18) · 11b `feat(compat): Prüfroute je Profil` (1.19) · 11c `feat(admin): Reiter „Compatibility“ für alle
|
||||
Profile` (1.20–1.22) · 11d `refactor(divi): DiviTab geht im Reiter auf` (1.23–1.24) · 11e `test(compat): Smokes` (1.25) · 12 `feat(changelog): Hook lit_mcp_changed` (2.1) · 13 `feat(cache): WP Super Cache über den
|
||||
Kern anbinden` (2.2–2.4) · 14 `test(cache): Integrations-Smoke` (2.5)
|
||||
|
||||
## Risiken
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -14,7 +14,7 @@ final class CheckReport
|
||||
public readonly string $id,
|
||||
public readonly string $createdAt,
|
||||
public readonly int $userId,
|
||||
public readonly string $diviVersion,
|
||||
public readonly string $version,
|
||||
public readonly string $pluginVersion,
|
||||
public readonly string $stateKey,
|
||||
public readonly array $steps
|
||||
@@ -35,11 +35,16 @@ final class CheckReport
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Green and made for exactly this Divi version, plugin version and Divi files (ReleaseResolver::stateKey()). */
|
||||
public function freshFor(string $diviVersion, string $pluginVersion, string $stateKey): bool
|
||||
/** Green and made for exactly this version, plugin version and state (Profile::$stateKey). */
|
||||
public function freshFor(string $version, string $pluginVersion, string $stateKey): bool
|
||||
{
|
||||
return $this->green()
|
||||
&& $this->diviVersion === $diviVersion
|
||||
return $this->green() && $this->currentFor($version, $pluginVersion, $stateKey);
|
||||
}
|
||||
|
||||
/** Made for exactly this state, green or not. */
|
||||
public function currentFor(string $version, string $pluginVersion, string $stateKey): bool
|
||||
{
|
||||
return $this->version === $version
|
||||
&& $this->pluginVersion === $pluginVersion
|
||||
&& $this->stateKey === $stateKey;
|
||||
}
|
||||
@@ -55,26 +60,29 @@ final class CheckReport
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(): array
|
||||
/**
|
||||
* @param string $versionKey Array key of the version; Divi keeps 'divi_version' so stored reports stay as they are.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(string $versionKey = 'version'): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'created_at' => $this->createdAt,
|
||||
'user_id' => $this->userId,
|
||||
'divi_version' => $this->diviVersion,
|
||||
$versionKey => $this->version,
|
||||
'plugin_version' => $this->pluginVersion,
|
||||
'state_key' => $this->stateKey,
|
||||
'steps' => array_map(static fn (CheckStep $step): array => $step->toArray(), $this->steps),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(mixed $data): ?self
|
||||
public static function fromArray(mixed $data, string $versionKey = 'version'): ?self
|
||||
{
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['id', 'created_at', 'divi_version', 'plugin_version', 'state_key'] as $key) {
|
||||
foreach (['id', 'created_at', $versionKey, 'plugin_version', 'state_key'] as $key) {
|
||||
if (! is_string($data[$key] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
@@ -96,7 +104,7 @@ final class CheckReport
|
||||
$data['id'],
|
||||
$data['created_at'],
|
||||
$data['user_id'],
|
||||
$data['divi_version'],
|
||||
$data[$versionKey],
|
||||
$data['plugin_version'],
|
||||
$data['state_key'],
|
||||
$steps
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The latest admin check report of one profile (spec §6.3), option lit_mcp_<profile>_check_report. Not final so the
|
||||
* tab can be tested with a mock.
|
||||
*/
|
||||
class CheckReportStore
|
||||
{
|
||||
/** @param string $versionKey Array key of the version in the stored report (Divi: 'divi_version'). */
|
||||
public function __construct(private string $profileId, private string $versionKey = 'version')
|
||||
{
|
||||
}
|
||||
|
||||
public static function forProfile(Profile $profile): self
|
||||
{
|
||||
return new self($profile->id, $profile->reportVersionKey);
|
||||
}
|
||||
|
||||
public function latest(): ?CheckReport
|
||||
{
|
||||
return CheckReport::fromArray(get_option($this->option(), null), $this->versionKey);
|
||||
}
|
||||
|
||||
public function save(CheckReport $report): void
|
||||
{
|
||||
update_option($this->option(), $report->toArray($this->versionKey), false);
|
||||
}
|
||||
|
||||
private function option(): string
|
||||
{
|
||||
return 'lit_mcp_' . $this->profileId . '_check_report';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* One step of the admin check (spec §6.2); skipped = not run. The label is stored with the step, so a report stays
|
||||
* readable without the profile that made it; an old report without one shows the raw key.
|
||||
*/
|
||||
final class CheckStep
|
||||
{
|
||||
public const MANIFEST = 'manifest';
|
||||
public const MIRRORS = 'mirrors';
|
||||
|
||||
/** The label of a generic step of the profile named $profileLabel; null for any other key. */
|
||||
public static function genericLabel(string $key, string $profileLabel): ?string
|
||||
{
|
||||
return match ($key) {
|
||||
self::MANIFEST => sprintf('%s-Funktionen, Klassen und Registrierungen', $profileLabel),
|
||||
self::MIRRORS => sprintf('Fingerabdrücke nachgebauter %s-Funktionen', $profileLabel),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param list<string> $problems */
|
||||
public function __construct(
|
||||
public readonly string $key,
|
||||
public readonly array $problems,
|
||||
public readonly bool $skipped = false,
|
||||
public readonly ?string $label = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function green(): bool
|
||||
{
|
||||
return ! $this->skipped && $this->problems === [];
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return $this->label ?? $this->key;
|
||||
}
|
||||
|
||||
/** @return array{key: string, problems: list<string>, skipped: bool, label?: string} */
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = ['key' => $this->key, 'problems' => $this->problems, 'skipped' => $this->skipped];
|
||||
|
||||
return $this->label === null ? $data : $data + ['label' => $this->label];
|
||||
}
|
||||
|
||||
public static function fromArray(mixed $data): ?self
|
||||
{
|
||||
if (! is_array($data) || ! is_string($data['key'] ?? null) || ! is_array($data['problems'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['key'],
|
||||
array_values(array_filter($data['problems'], 'is_string')),
|
||||
($data['skipped'] ?? false) === true,
|
||||
is_string($data['label'] ?? null) ? $data['label'] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Log;
|
||||
use LIT\MCP\Plugin;
|
||||
use LogicException;
|
||||
use Throwable;
|
||||
|
||||
@@ -21,25 +23,31 @@ final class Compatibility
|
||||
private array $byId = [];
|
||||
/** @var array<string, array{0: Capability, 1: Profile}> keyed by capability ID */
|
||||
private array $byCapability = [];
|
||||
/** @var array<string, bool> */
|
||||
private array $actives = [];
|
||||
/** @var array<string, string|null> */
|
||||
private array $versions = [];
|
||||
/** @var array<string, CompatibilityMatrix> */
|
||||
private array $matrices = [];
|
||||
/** @var array<string, ManifestCheck> */
|
||||
private array $checks = [];
|
||||
/** @var Closure(Profile): ManifestCheck */
|
||||
private Closure $check;
|
||||
/** @var Closure(Profile): ?CheckReport */
|
||||
private Closure $report;
|
||||
|
||||
/**
|
||||
* @param list<Profile> $profiles
|
||||
* @param (Closure(Profile): ManifestCheck)|null $check Builds the check of a profile; default: its manifests
|
||||
* without signature checks.
|
||||
* @param (Closure(Profile): ManifestCheck)|null $check Builds the check of a profile; default: Profile::checker().
|
||||
* @param (Closure(Profile): ?CheckReport)|null $report The latest check report of a profile; a red one for
|
||||
* the current state locks a LockOnFailure profile.
|
||||
* Default: none (the structure check is the base).
|
||||
* @throws LogicException For a profile ID or a capability ID that occurs twice.
|
||||
*/
|
||||
public function __construct(private array $profiles, ?Closure $check = null)
|
||||
public function __construct(private array $profiles, ?Closure $check = null, ?Closure $report = null)
|
||||
{
|
||||
$this->check = $check ?? static fn (Profile $profile): ManifestCheck => new ManifestCheck(
|
||||
$profile->manifests,
|
||||
signatures: false
|
||||
);
|
||||
$this->report = $report ?? static fn (Profile $profile): ?CheckReport => null;
|
||||
$this->check = $check ?? static fn (Profile $profile): ManifestCheck => $profile->checker();
|
||||
foreach ($profiles as $profile) {
|
||||
if (isset($this->byId[$profile->id])) {
|
||||
throw new LogicException('profile ID twice: ' . $profile->id);
|
||||
@@ -77,14 +85,18 @@ final class Compatibility
|
||||
|
||||
public function active(Profile $profile): bool
|
||||
{
|
||||
return $profile->active === null || self::attempt($profile->active) === true;
|
||||
return $this->actives[$profile->id] ??= $profile->active === null
|
||||
|| self::attempt($profile->active, $profile) === true;
|
||||
}
|
||||
|
||||
public function version(Profile $profile): ?string
|
||||
{
|
||||
$version = $profile->version === null ? null : self::attempt($profile->version);
|
||||
if (! array_key_exists($profile->id, $this->versions)) {
|
||||
$version = $profile->version === null ? null : self::attempt($profile->version, $profile);
|
||||
$this->versions[$profile->id] = is_string($version) ? $version : null;
|
||||
}
|
||||
|
||||
return is_string($version) ? $version : null;
|
||||
return $this->versions[$profile->id];
|
||||
}
|
||||
|
||||
public function matrix(Profile $profile): CompatibilityMatrix
|
||||
@@ -104,11 +116,19 @@ final class Compatibility
|
||||
static fn (Problem $problem): string => $problem->label,
|
||||
$this->checkOf($profile)->problems()
|
||||
));
|
||||
} catch (Throwable) {
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('compat ' . $profile->id, $e);
|
||||
|
||||
return [self::failed($profile)];
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the admin check of the profile (spec §6.2); a report for $userId. */
|
||||
public function check(Profile $profile, int $userId): CheckReport
|
||||
{
|
||||
return CompatibilityCheck::forProfile($profile, $this->checkOf($profile))->run($userId);
|
||||
}
|
||||
|
||||
public function usable(Capability $capability): bool
|
||||
{
|
||||
$owner = $this->owner($capability);
|
||||
@@ -161,17 +181,62 @@ final class Compatibility
|
||||
return CompatibilityMatrix::allIncompatible($capabilities, $tooOld);
|
||||
}
|
||||
|
||||
$red = $this->redReport($profile);
|
||||
if ($red !== null) {
|
||||
// shortcut: a step names no capability, so a red check locks all of them; refine with step features.
|
||||
return CompatibilityMatrix::allIncompatible($capabilities, $red);
|
||||
}
|
||||
|
||||
return CompatibilityMatrix::fromProblems(
|
||||
$capabilities,
|
||||
$this->checkOf($profile)->problems(),
|
||||
null,
|
||||
automatic: true
|
||||
);
|
||||
} catch (Throwable) {
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('compat ' . $profile->id, $e);
|
||||
|
||||
return CompatibilityMatrix::allIncompatible($capabilities, self::failed($profile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the latest report says when it is red and made for the current state; null = no reason to lock. A report
|
||||
* without steps is no check result. The reason names only the labels of the steps, never their problem texts
|
||||
* (they reach MCP clients in the 423). Only a new green check or a version change (plugin or third-party plugin)
|
||||
* lifts a lock by a red report.
|
||||
*/
|
||||
private function redReport(Profile $profile): ?string
|
||||
{
|
||||
$report = ($this->report)($profile);
|
||||
$version = $this->version($profile) ?? '';
|
||||
$stateKey = $profile->stateKey === null ? '' : ($profile->stateKey)($version);
|
||||
if (
|
||||
$report === null
|
||||
|| $report->steps === []
|
||||
|| $report->green()
|
||||
|| ! $report->currentFor($version, Plugin::VERSION, $stateKey)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$failed = array_map(
|
||||
static fn (CheckStep $step): string => $step->label(),
|
||||
array_values(array_filter($report->steps, static fn (CheckStep $step): bool => ! $step->green()))
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'Die letzte Prüfung von %s ist fehlgeschlagen (%s); Details im Admin-Bereich.',
|
||||
$profile->label,
|
||||
implode('; ', $failed)
|
||||
);
|
||||
}
|
||||
|
||||
/** The report stored by the admin check, for Compatibility's $report parameter. */
|
||||
public static function storedReport(Profile $profile): ?CheckReport
|
||||
{
|
||||
return CheckReportStore::forProfile($profile)->latest();
|
||||
}
|
||||
|
||||
private function versionProblem(Profile $profile): ?string
|
||||
{
|
||||
if ($profile->minVersion === null) {
|
||||
@@ -202,11 +267,13 @@ final class Compatibility
|
||||
}
|
||||
|
||||
/** The closure's result, null when it throws. */
|
||||
private static function attempt(Closure $closure): mixed
|
||||
private static function attempt(Closure $closure, Profile $profile): mixed
|
||||
{
|
||||
try {
|
||||
return $closure();
|
||||
} catch (Throwable) {
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('compat ' . $profile->id, $e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Log;
|
||||
use LIT\MCP\Plugin;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The admin check of one profile (spec §6.2) – also wp lit-mcp divi-check: manifest (code and registrations),
|
||||
* fingerprints (only when a manifest has mirrors), then the extra steps of the profile. A step that throws is a red
|
||||
* step. Not final so the route and the command can be tested with a mock.
|
||||
*/
|
||||
class CompatibilityCheck
|
||||
{
|
||||
public function __construct(private Profile $profile, private ManifestCheck $manifests)
|
||||
{
|
||||
}
|
||||
|
||||
public static function forProfile(Profile $profile, ?ManifestCheck $check = null): self
|
||||
{
|
||||
return new self($profile, $check ?? $profile->checker());
|
||||
}
|
||||
|
||||
public function run(int $userId): CheckReport
|
||||
{
|
||||
$label = $this->profile->label;
|
||||
$manifest = self::guarded(fn (): array => ManifestCheck::failed(
|
||||
array_merge($this->manifests->codeChecks(), $this->manifests->registrationChecks())
|
||||
));
|
||||
$steps = [new CheckStep(
|
||||
CheckStep::MANIFEST,
|
||||
$manifest,
|
||||
false,
|
||||
CheckStep::genericLabel(CheckStep::MANIFEST, $label)
|
||||
)];
|
||||
if (array_filter($this->profile->manifests, static fn (Manifest $m): bool => $m->mirrors !== []) !== []) {
|
||||
$steps[] = new CheckStep(
|
||||
CheckStep::MIRRORS,
|
||||
self::guarded(fn (): array => ManifestCheck::failed($this->manifests->mirrorChecks())),
|
||||
false,
|
||||
CheckStep::genericLabel(CheckStep::MIRRORS, $label)
|
||||
);
|
||||
}
|
||||
if ($this->profile->extraSteps !== null) {
|
||||
$steps = [...$steps, ...($this->profile->extraSteps)($manifest === [])];
|
||||
}
|
||||
$version = (string) ($this->profile->version === null ? '' : ($this->profile->version)());
|
||||
|
||||
return new CheckReport(
|
||||
wp_generate_uuid4(),
|
||||
gmdate('c'),
|
||||
$userId,
|
||||
$version,
|
||||
Plugin::VERSION,
|
||||
$this->profile->stateKey === null ? '' : ($this->profile->stateKey)($version),
|
||||
$steps
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<string> The step's problems; a throwing step is one problem. */
|
||||
public static function guarded(Closure $step): array
|
||||
{
|
||||
try {
|
||||
return array_values(array_filter((array) $step(), 'is_string'));
|
||||
} catch (Throwable $e) {
|
||||
// The report reaches MCP clients through the lock reason; the exception text stays in the log.
|
||||
Log::failure('compat check step', $e);
|
||||
|
||||
return ['Prüfschritt abgebrochen (Details im PHP-Error-Log).'];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ defined('ABSPATH') || exit;
|
||||
/**
|
||||
* Everything the compatibility layer needs to know about one third-party plugin: its capabilities, what its code
|
||||
* depends on, how to tell whether it is active and which version it has. Pure data; Compatibility evaluates it.
|
||||
* Mit benannten Argumenten bauen.
|
||||
*/
|
||||
final class Profile
|
||||
{
|
||||
@@ -22,6 +23,14 @@ final class Profile
|
||||
* @param (Closure(): ?string)|null $version The installed version, null result = unknown.
|
||||
* @param string|null $minVersion Lowest supported version (version_compare).
|
||||
* @param (Closure(): CompatibilityMatrix)|null $matrix Required for ReleasePerVersion, unused otherwise.
|
||||
* @param list<string> $testedVersions Versions the capabilities were verified against.
|
||||
* @param (Closure(string): string)|null $stateKey What a check report is valid for, given the version.
|
||||
* @param (Closure(bool): list<CheckStep>)|null $extraSteps Steps after manifest and fingerprints; gets
|
||||
* whether the manifest step was clean.
|
||||
* @param (Closure(): ManifestCheck)|null $manifestCheck The check of this profile; null = its manifests
|
||||
* without signature checks.
|
||||
* @param string $reportVersionKey Array key of the version in stored reports and
|
||||
* release rows (Divi: 'divi_version').
|
||||
* @throws LogicException For a ReleasePerVersion profile without matrix or a capability ID given twice.
|
||||
*/
|
||||
public function __construct(
|
||||
@@ -33,7 +42,12 @@ final class Profile
|
||||
public readonly ?Closure $active = null,
|
||||
public readonly ?Closure $version = null,
|
||||
public readonly ?string $minVersion = null,
|
||||
public readonly ?Closure $matrix = null
|
||||
public readonly ?Closure $matrix = null,
|
||||
public readonly array $testedVersions = [],
|
||||
public readonly ?Closure $stateKey = null,
|
||||
public readonly ?Closure $extraSteps = null,
|
||||
public readonly ?Closure $manifestCheck = null,
|
||||
public readonly string $reportVersionKey = 'version'
|
||||
) {
|
||||
if ($tier === Tier::ReleasePerVersion && $matrix === null) {
|
||||
throw new LogicException(sprintf('profile %s: a ReleasePerVersion profile needs a matrix closure', $id));
|
||||
@@ -45,4 +59,12 @@ final class Profile
|
||||
throw new LogicException(sprintf('profile %s: capability IDs twice: %s', $id, implode(', ', $duplicates)));
|
||||
}
|
||||
}
|
||||
|
||||
/** The check of this profile: its manifestCheck closure, else its manifests without signature checks. */
|
||||
public function checker(): ManifestCheck
|
||||
{
|
||||
return $this->manifestCheck !== null
|
||||
? ($this->manifestCheck)()
|
||||
: new ManifestCheck($this->manifests, signatures: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Versions of one profile released on this site after a green admin check (spec §5.3, source "database"), option
|
||||
* lit_mcp_<profile>_releases. An entry holds for the plugin version that ran the check only.
|
||||
*
|
||||
* Not final so the resolver can be tested with a mock.
|
||||
*/
|
||||
class ReleaseStore
|
||||
{
|
||||
/** @param string $versionKey Row key of the version (Divi: 'divi_version', so stored rows stay as they are). */
|
||||
public function __construct(private string $profileId, private string $versionKey = 'version')
|
||||
{
|
||||
}
|
||||
|
||||
public static function forProfile(Profile $profile): self
|
||||
{
|
||||
return new self($profile->id, $profile->reportVersionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, string|int>> Rows with the keys <versionKey>, plugin_version, user_id, released_at,
|
||||
* report_id.
|
||||
*/
|
||||
public function entries(): array
|
||||
{
|
||||
$raw = get_option($this->option(), []);
|
||||
$entries = [];
|
||||
foreach (is_array($raw) ? $raw : [] as $row) {
|
||||
if (
|
||||
is_array($row)
|
||||
&& is_string($row[$this->versionKey] ?? null)
|
||||
&& is_string($row['plugin_version'] ?? null)
|
||||
&& is_int($row['user_id'] ?? null)
|
||||
&& is_string($row['released_at'] ?? null)
|
||||
&& is_string($row['report_id'] ?? null)
|
||||
) {
|
||||
$entries[] = [
|
||||
$this->versionKey => $row[$this->versionKey],
|
||||
'plugin_version' => $row['plugin_version'],
|
||||
'user_id' => $row['user_id'],
|
||||
'released_at' => $row['released_at'],
|
||||
'report_id' => $row['report_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|int>|null
|
||||
*/
|
||||
public function find(string $version, string $pluginVersion): ?array
|
||||
{
|
||||
foreach ($this->entries() as $entry) {
|
||||
if ($entry[$this->versionKey] === $version && $entry['plugin_version'] === $pluginVersion) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(string $version, string $pluginVersion, int $userId, string $reportId): void
|
||||
{
|
||||
$entries = $this->without($version, $pluginVersion);
|
||||
$entries[] = [
|
||||
$this->versionKey => $version,
|
||||
'plugin_version' => $pluginVersion,
|
||||
'user_id' => $userId,
|
||||
'released_at' => gmdate('c'),
|
||||
'report_id' => $reportId,
|
||||
];
|
||||
update_option($this->option(), $entries, false);
|
||||
}
|
||||
|
||||
public function withdraw(string $version, string $pluginVersion): bool
|
||||
{
|
||||
if ($this->find($version, $pluginVersion) === null) {
|
||||
return false;
|
||||
}
|
||||
update_option($this->option(), $this->without($version, $pluginVersion), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, string|int>> Rows with the keys <versionKey>, plugin_version, user_id, released_at,
|
||||
* report_id.
|
||||
*/
|
||||
private function without(string $version, string $pluginVersion): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->entries(),
|
||||
fn (array $entry): bool => $entry[$this->versionKey] !== $version
|
||||
|| $entry['plugin_version'] !== $pluginVersion
|
||||
));
|
||||
}
|
||||
|
||||
private function option(): string
|
||||
{
|
||||
return 'lit_mcp_' . $this->profileId . '_releases';
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,18 @@ use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
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\ReleaseResolver;
|
||||
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;
|
||||
|
||||
@@ -159,7 +161,7 @@ final class DiviTab implements AdminTab
|
||||
return;
|
||||
}
|
||||
|
||||
($this->releases ?? new ReleaseStore())->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
||||
$this->releases()->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
||||
ReleaseGate::invalidate();
|
||||
$this->log?->record(
|
||||
'lit/divi-release',
|
||||
@@ -177,7 +179,7 @@ final class DiviTab implements AdminTab
|
||||
check_admin_referer(self::WITHDRAW_ACTION);
|
||||
|
||||
$version = $this->version();
|
||||
if (! ($this->releases ?? new ReleaseStore())->withdraw($version, Plugin::VERSION)) {
|
||||
if (! $this->releases()->withdraw($version, Plugin::VERSION)) {
|
||||
$action->redirect(self::SLUG, 'divi_withdraw_failed');
|
||||
|
||||
return;
|
||||
@@ -514,7 +516,7 @@ final class DiviTab implements AdminTab
|
||||
esc_html(sprintf(
|
||||
/* translators: 1: Divi version, 2: plugin version, 3: date */
|
||||
__('Divi %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
|
||||
$report->diviVersion,
|
||||
$report->version,
|
||||
$report->pluginVersion,
|
||||
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
|
||||
))
|
||||
@@ -530,7 +532,7 @@ final class DiviTab implements AdminTab
|
||||
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
|
||||
esc_attr($icon),
|
||||
esc_html($word),
|
||||
esc_html($step->label())
|
||||
esc_html(DiviCheckSteps::label($step))
|
||||
);
|
||||
if ($step->skipped) {
|
||||
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
|
||||
@@ -569,8 +571,13 @@ final class DiviTab implements AdminTab
|
||||
return ($this->stateKey ?? static fn (string $v): string => ReleaseResolver::stateKey($v))($version);
|
||||
}
|
||||
|
||||
private function releases(): ReleaseStore
|
||||
{
|
||||
return $this->releases ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
|
||||
private function reports(): CheckReportStore
|
||||
{
|
||||
return $this->reports ?? new CheckReportStore();
|
||||
return $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ namespace LIT\MCP\Integrations\Divi\Cli;
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\LineDiff;
|
||||
use LIT\MCP\Compatibility\SourceFingerprint;
|
||||
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\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
@@ -109,10 +111,10 @@ final class DiviCheckCommand
|
||||
}
|
||||
|
||||
$report = ($this->check)()->run(get_current_user_id());
|
||||
($this->reports ?? new CheckReportStore())->save($report);
|
||||
($this->out)(sprintf('Divi %s mit LIT MCP %s', $report->diviVersion ?: '?', $report->pluginVersion));
|
||||
($this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY))->save($report);
|
||||
($this->out)(sprintf('Divi %s mit LIT MCP %s', $report->version ?: '?', $report->pluginVersion));
|
||||
foreach ($report->steps as $step) {
|
||||
($this->out)(($step->skipped ? '–' : ($step->green() ? '✓' : '✗')) . ' ' . $step->label());
|
||||
($this->out)(($step->skipped ? '–' : ($step->green() ? '✓' : '✗')) . ' ' . DiviCheckSteps::label($step));
|
||||
foreach ($step->problems as $problem) {
|
||||
($this->out)(' ' . $problem);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ToolAvailability;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Bootable;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
@@ -73,7 +75,6 @@ use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviUpdateAbility;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
@@ -190,7 +191,7 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
|
||||
public function compatibilities(): array
|
||||
{
|
||||
return [DiviProfile::profile()];
|
||||
return [$this->profile()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +262,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
|
||||
// Spec §6: the admin check runs as a REST request (same context as MCP calls). Its short form has no
|
||||
// Tools-tab gate: the check tests Divi, not this site's tool switches.
|
||||
$check = fn (): CompatibilityCheck => CompatibilityCheck::create($this->shortForm(null));
|
||||
$profile = $this->profile();
|
||||
$check = static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile);
|
||||
(new DiviCheckRoute($check))->register();
|
||||
DiviCheckCommand::register($check);
|
||||
|
||||
@@ -429,6 +431,12 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
||||
}
|
||||
|
||||
/** @param (Closure(): bool)|null $writable */
|
||||
/** The Divi profile; the check's write test composes through the short form without a Tools-tab gate. */
|
||||
private function profile(): Profile
|
||||
{
|
||||
return DiviProfile::profile(fn (): DiviContentComposer => $this->shortForm(null));
|
||||
}
|
||||
|
||||
private function shortForm(?Closure $writable): DiviContentComposer
|
||||
{
|
||||
return new DiviContentComposer($this->schema, $this->composition(), new DiviSaver(), $this->design, $writable);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The latest admin check report of this site (spec §6.3). Not final so the Divi tab can be tested with a mock. */
|
||||
class CheckReportStore
|
||||
{
|
||||
public const OPTION = 'lit_mcp_divi_check_report';
|
||||
|
||||
public function latest(): ?CheckReport
|
||||
{
|
||||
return CheckReport::fromArray(get_option(self::OPTION, null));
|
||||
}
|
||||
|
||||
public function save(CheckReport $report): void
|
||||
{
|
||||
update_option(self::OPTION, $report->toArray(), false);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** One step of the admin check (spec §6.2); skipped = not run (the write test after missing Divi functions). */
|
||||
final class CheckStep
|
||||
{
|
||||
public const MANIFEST = 'manifest';
|
||||
public const MIRRORS = 'mirrors';
|
||||
public const CATALOG = 'catalog';
|
||||
public const FORMAT = 'format';
|
||||
public const WRITE = 'write';
|
||||
|
||||
private const LABELS = [
|
||||
self::MANIFEST => 'Divi-Funktionen, Klassen und Registrierungen',
|
||||
self::MIRRORS => 'Fingerabdrücke nachgebauter Divi-Funktionen',
|
||||
self::CATALOG => 'Modulkatalog (Kurzform-Pfade)',
|
||||
self::FORMAT => 'Format im Speicher (Parser und Serializer)',
|
||||
self::WRITE => 'Schreibtest auf einer Wegwerfseite',
|
||||
];
|
||||
|
||||
/** @param list<string> $problems */
|
||||
public function __construct(
|
||||
public readonly string $key,
|
||||
public readonly array $problems,
|
||||
public readonly bool $skipped = false
|
||||
) {
|
||||
}
|
||||
|
||||
public function green(): bool
|
||||
{
|
||||
return ! $this->skipped && $this->problems === [];
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return self::LABELS[$this->key] ?? $this->key;
|
||||
}
|
||||
|
||||
/** @return array{key: string, problems: list<string>, skipped: bool} */
|
||||
public function toArray(): array
|
||||
{
|
||||
return ['key' => $this->key, 'problems' => $this->problems, 'skipped' => $this->skipped];
|
||||
}
|
||||
|
||||
public static function fromArray(mixed $data): ?self
|
||||
{
|
||||
if (! is_array($data) || ! is_string($data['key'] ?? null) || ! is_array($data['problems'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['key'],
|
||||
array_values(array_filter($data['problems'], 'is_string')),
|
||||
($data['skipped'] ?? false) === true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformCheck;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Plugin;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The admin check (spec §6.2) – also wp lit-mcp divi-check: manifests (code and registrations), fingerprints,
|
||||
* module catalog, format round trip and the write test. A step that throws is a red step; the write test is skipped
|
||||
* only when Divi functions are missing (it would fail anyway), not when only fingerprints changed – so
|
||||
* --update-reference can see whether everything else still works. Not final so the route and the command can be
|
||||
* tested with a mock.
|
||||
*/
|
||||
class CompatibilityCheck
|
||||
{
|
||||
/**
|
||||
* @param Closure(): list<string> $catalog Short-form problems against Divi's live catalog.
|
||||
* @param Closure(): string $version Installed Divi version ('' = unknown).
|
||||
* @param Closure(string): string $stateKey ReleaseResolver::stateKey().
|
||||
*/
|
||||
public function __construct(
|
||||
private ManifestCheck $manifests,
|
||||
private Closure $catalog,
|
||||
private FormatCheck $format,
|
||||
private WriteProbe $probe,
|
||||
private Closure $version,
|
||||
private Closure $stateKey
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(ContentComposer $composer): self
|
||||
{
|
||||
return new self(
|
||||
DiviManifestCheck::create(),
|
||||
static fn (): array => DiviModules::isAvailable()
|
||||
? ShortformCheck::problems((new DiviModules())->catalog())
|
||||
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
|
||||
new FormatCheck(),
|
||||
new WriteProbe($composer),
|
||||
static fn (): string => DiviEnvironment::version() ?? '',
|
||||
static fn (string $version): string => ReleaseResolver::stateKey($version)
|
||||
);
|
||||
}
|
||||
|
||||
public function run(int $userId): CheckReport
|
||||
{
|
||||
$manifest = self::guarded(fn (): array => ManifestCheck::failed(
|
||||
array_merge($this->manifests->codeChecks(), $this->manifests->registrationChecks())
|
||||
));
|
||||
$steps = [
|
||||
new CheckStep(CheckStep::MANIFEST, $manifest),
|
||||
new CheckStep(CheckStep::MIRRORS, self::guarded(
|
||||
fn (): array => ManifestCheck::failed($this->manifests->mirrorChecks())
|
||||
)),
|
||||
new CheckStep(CheckStep::CATALOG, self::guarded($this->catalog)),
|
||||
new CheckStep(CheckStep::FORMAT, self::guarded(fn (): array => $this->format->problems())),
|
||||
$manifest === []
|
||||
? new CheckStep(CheckStep::WRITE, self::guarded(fn (): array => $this->probe->run()))
|
||||
: new CheckStep(CheckStep::WRITE, [], true),
|
||||
];
|
||||
$version = ($this->version)();
|
||||
|
||||
return new CheckReport(
|
||||
wp_generate_uuid4(),
|
||||
gmdate('c'),
|
||||
$userId,
|
||||
$version,
|
||||
Plugin::VERSION,
|
||||
($this->stateKey)($version),
|
||||
$steps
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function guarded(Closure $step): array
|
||||
{
|
||||
try {
|
||||
return array_values(array_filter((array) $step(), 'is_string'));
|
||||
} catch (Throwable $e) {
|
||||
return ['Prüfschritt abgebrochen: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
@@ -62,7 +64,8 @@ final class DiviCheckRoute
|
||||
if (self::lock()) {
|
||||
try {
|
||||
$report = ($this->check)()->run(get_current_user_id());
|
||||
($this->reports ?? new CheckReportStore())->save($report);
|
||||
$reports = $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
$reports->save($report);
|
||||
ReleaseGate::invalidate();
|
||||
$notice = 'divi_checked';
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The check steps only Divi has (module catalog, format round trip, write test) and their labels. */
|
||||
final class DiviCheckSteps
|
||||
{
|
||||
public const CATALOG = 'catalog';
|
||||
public const FORMAT = 'format';
|
||||
public const WRITE = 'write';
|
||||
|
||||
/** Labels of the Divi-only steps; manifest and mirrors come from CheckStep::genericLabel(). */
|
||||
private const LABELS = [
|
||||
self::CATALOG => 'Modulkatalog (Kurzform-Pfade)',
|
||||
self::FORMAT => 'Format im Speicher (Parser und Serializer)',
|
||||
self::WRITE => 'Schreibtest auf einer Wegwerfseite',
|
||||
];
|
||||
|
||||
/** @param list<string> $problems */
|
||||
public static function step(string $key, array $problems, bool $skipped = false): CheckStep
|
||||
{
|
||||
return new CheckStep($key, $problems, $skipped, self::LABELS[$key] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Profile::$extraSteps of Divi: module catalog, format round trip and the write test. The write test is
|
||||
* skipped only when Divi functions are missing (it would fail anyway), not when only fingerprints changed – so
|
||||
* --update-reference can see whether everything else still works.
|
||||
*
|
||||
* @param Closure(): list<string> $catalog Short-form problems against Divi's live catalog.
|
||||
* @param Closure(): list<string> $probe WriteProbe::run().
|
||||
* @return Closure(bool): list<CheckStep>
|
||||
*/
|
||||
public static function extra(Closure $catalog, FormatCheck $format, Closure $probe): Closure
|
||||
{
|
||||
return static fn (bool $manifestClean): array => [
|
||||
self::step(self::CATALOG, CompatibilityCheck::guarded($catalog)),
|
||||
self::step(self::FORMAT, CompatibilityCheck::guarded(static fn (): array => $format->problems())),
|
||||
$manifestClean
|
||||
? self::step(self::WRITE, CompatibilityCheck::guarded($probe))
|
||||
: self::step(self::WRITE, [], true),
|
||||
];
|
||||
}
|
||||
|
||||
public static function label(CheckStep $step): string
|
||||
{
|
||||
// Reports saved before labels were stored have none.
|
||||
return $step->label
|
||||
?? self::LABELS[$step->key]
|
||||
?? CheckStep::genericLabel($step->key, DiviProfile::LABEL)
|
||||
?? $step->key;
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformCheck;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LogicException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Divi as a compatibility profile: every version needs a release, the matrix comes from the release gate. */
|
||||
final class DiviProfile
|
||||
{
|
||||
public static function profile(): Profile
|
||||
public const ID = 'divi';
|
||||
public const LABEL = 'Divi';
|
||||
/** Stored reports and release rows keep this key, so data from before the generic layer stays readable. */
|
||||
public const REPORT_VERSION_KEY = 'divi_version';
|
||||
|
||||
/** @param (Closure(): ContentComposer)|null $composer The write test's composer; without it that step fails. */
|
||||
public static function profile(?Closure $composer = null): Profile
|
||||
{
|
||||
return new Profile(
|
||||
'divi',
|
||||
'Divi',
|
||||
Tier::ReleasePerVersion,
|
||||
DiviFeature::cases(),
|
||||
array_values(DiviAdapters::manifests()),
|
||||
id: self::ID,
|
||||
label: self::LABEL,
|
||||
tier: Tier::ReleasePerVersion,
|
||||
capabilities: DiviFeature::cases(),
|
||||
manifests: array_values(DiviAdapters::manifests()),
|
||||
active: static fn (): bool => DiviEnvironment::version() !== null,
|
||||
version: DiviEnvironment::version(...),
|
||||
matrix: static fn () => ReleaseGate::status()->matrix
|
||||
matrix: static fn () => ReleaseGate::status()->matrix,
|
||||
testedVersions: DiviReleases::TESTED,
|
||||
stateKey: ReleaseResolver::stateKey(...),
|
||||
extraSteps: DiviCheckSteps::extra(
|
||||
static fn (): array => DiviModules::isAvailable()
|
||||
? ShortformCheck::problems((new DiviModules())->catalog())
|
||||
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
|
||||
new FormatCheck(),
|
||||
static fn (): array => (new WriteProbe(
|
||||
$composer === null ? throw new LogicException('no composer for the write test') : $composer()
|
||||
))->run()
|
||||
),
|
||||
manifestCheck: DiviManifestCheck::create(...),
|
||||
reportVersionKey: self::REPORT_VERSION_KEY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\ReleaseStore;
|
||||
use LIT\MCP\Compatibility\Problem;
|
||||
use LIT\MCP\Plugin;
|
||||
|
||||
@@ -53,7 +54,8 @@ final class ReleaseResolver
|
||||
if (in_array($version, DiviReleases::TESTED, true)) {
|
||||
return ReleaseStatus::of($version, self::matrix($problems, CompatibilityMatrix::SOURCE_CODE, false));
|
||||
}
|
||||
$entry = $version === '' ? null : ($this->store ?? new ReleaseStore())->find($version, Plugin::VERSION);
|
||||
$store = $this->store ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
$entry = $version === '' ? null : $store->find($version, Plugin::VERSION);
|
||||
if ($entry !== null) {
|
||||
return ReleaseStatus::of(
|
||||
$version,
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Divi versions released on this site after a green admin check (spec §5.3, source "database"). An entry holds for
|
||||
* the plugin version that ran the check only.
|
||||
*
|
||||
* Not final so the resolver can be tested with a mock.
|
||||
*/
|
||||
class ReleaseStore
|
||||
{
|
||||
public const OPTION = 'lit_mcp_divi_releases';
|
||||
|
||||
/**
|
||||
* @return list<array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}>
|
||||
*/
|
||||
public function entries(): array
|
||||
{
|
||||
$raw = get_option(self::OPTION, []);
|
||||
$entries = [];
|
||||
foreach (is_array($raw) ? $raw : [] as $row) {
|
||||
if (
|
||||
is_array($row)
|
||||
&& is_string($row['divi_version'] ?? null)
|
||||
&& is_string($row['plugin_version'] ?? null)
|
||||
&& is_int($row['user_id'] ?? null)
|
||||
&& is_string($row['released_at'] ?? null)
|
||||
&& is_string($row['report_id'] ?? null)
|
||||
) {
|
||||
$entries[] = [
|
||||
'divi_version' => $row['divi_version'],
|
||||
'plugin_version' => $row['plugin_version'],
|
||||
'user_id' => $row['user_id'],
|
||||
'released_at' => $row['released_at'],
|
||||
'report_id' => $row['report_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}|null
|
||||
*/
|
||||
public function find(string $diviVersion, string $pluginVersion): ?array
|
||||
{
|
||||
foreach ($this->entries() as $entry) {
|
||||
if ($entry['divi_version'] === $diviVersion && $entry['plugin_version'] === $pluginVersion) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(string $diviVersion, string $pluginVersion, int $userId, string $reportId): void
|
||||
{
|
||||
$entries = $this->without($diviVersion, $pluginVersion);
|
||||
$entries[] = [
|
||||
'divi_version' => $diviVersion,
|
||||
'plugin_version' => $pluginVersion,
|
||||
'user_id' => $userId,
|
||||
'released_at' => gmdate('c'),
|
||||
'report_id' => $reportId,
|
||||
];
|
||||
update_option(self::OPTION, $entries, false);
|
||||
}
|
||||
|
||||
public function withdraw(string $diviVersion, string $pluginVersion): bool
|
||||
{
|
||||
if ($this->find($diviVersion, $pluginVersion) === null) {
|
||||
return false;
|
||||
}
|
||||
update_option(self::OPTION, $this->without($diviVersion, $pluginVersion), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{divi_version: string, plugin_version: string, user_id: int, released_at: string,
|
||||
* report_id: string}>
|
||||
*/
|
||||
private function without(string $diviVersion, string $pluginVersion): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->entries(),
|
||||
static fn (array $entry): bool => $entry['divi_version'] !== $diviVersion
|
||||
|| $entry['plugin_version'] !== $pluginVersion
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
@@ -101,7 +102,9 @@ class WriteProbe
|
||||
try {
|
||||
$problems = $this->exercise($id);
|
||||
} catch (Throwable $e) {
|
||||
$problems = ['Schreibtest abgebrochen: ' . $e->getMessage()];
|
||||
// The report reaches MCP clients through the lock reason; the exception text stays in the log.
|
||||
Log::failure('divi write test', $e);
|
||||
$problems = ['Schreibtest abgebrochen (Details im PHP-Error-Log).'];
|
||||
} finally {
|
||||
if (! self::delete($id)) {
|
||||
$problems[] = sprintf(
|
||||
|
||||
@@ -130,7 +130,10 @@ final class IntegrationRegistry
|
||||
*/
|
||||
public function compatibility(): Compatibility
|
||||
{
|
||||
return $this->compatibility ??= new Compatibility($this->compatibilities());
|
||||
return $this->compatibility ??= new Compatibility(
|
||||
$this->compatibilities(),
|
||||
report: Compatibility::storedReport(...)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<string> Ids of all known integrations, available or not. */
|
||||
|
||||
+9
-8
@@ -24,11 +24,11 @@ final class PodsCompatibility
|
||||
$all = PodsFeature::cases();
|
||||
|
||||
return new Profile(
|
||||
'pods',
|
||||
'Pods',
|
||||
Tier::LockOnFailure,
|
||||
$all,
|
||||
[new Manifest(
|
||||
id: 'pods',
|
||||
label: 'Pods',
|
||||
tier: Tier::LockOnFailure,
|
||||
capabilities: $all,
|
||||
manifests: [new Manifest(
|
||||
'Pods',
|
||||
[
|
||||
new Call('pods_api', 0, [PodsFeature::SchemaRead]),
|
||||
@@ -41,9 +41,10 @@ final class PodsCompatibility
|
||||
],
|
||||
[new ConstantRequirement('PODS_VERSION', $all)]
|
||||
)],
|
||||
static fn (): bool => defined('PODS_VERSION'),
|
||||
static fn (): ?string => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
||||
'2.8'
|
||||
active: static fn (): bool => defined('PODS_VERSION'),
|
||||
version: static fn (): ?string => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
||||
minVersion: '2.8',
|
||||
testedVersions: ['3.3.9.2']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ final class Plugin
|
||||
// The sources exist before the registry, so Pods gets its own Compatibility here – a deliberate, cheap
|
||||
// duplicate of the registry's (a few function_exists checks, memoised per request).
|
||||
$sources = self::schemaSources(
|
||||
new Compatibility([PodsCompatibility::profile()]),
|
||||
new Compatibility([PodsCompatibility::profile()], report: Compatibility::storedReport(...)),
|
||||
defined('PODS_VERSION')
|
||||
);
|
||||
|
||||
|
||||
+6
-3
@@ -17,9 +17,11 @@ defined('ABSPATH') || exit;
|
||||
* so the Composer/Jetpack autoloader is not guaranteed to have run. The option/hook/table
|
||||
* names below are therefore duplicated from Access::OPTION, Abilities\ExposurePolicy::OPTION,
|
||||
* Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Integrations\Divi\Release\
|
||||
* ReleaseStore::OPTION, Integrations\Divi\Release\CheckReportStore::OPTION, Integrations\
|
||||
* Divi\Release\DiviCheckRoute::LOCK, Integrations\Divi\Design\DesignLock::option(),
|
||||
* Admin\ConnectionStatus::ENDPOINT_TRANSIENT, Integrations\Divi\Release\ReleaseResolver::CACHE,
|
||||
* lit_mcp_<profile>_releases (Compatibility\ReleaseStore), lit_mcp_<profile>_check_report
|
||||
* (Compatibility\CheckReportStore) and lit_mcp_<profile>_check_lock (Divi\Release\DiviCheckRoute::LOCK) for the
|
||||
* profiles divi and pods (an explicit list: a LIKE pattern could hit foreign options),
|
||||
* Integrations\Divi\Design\DesignLock::option(), Admin\ConnectionStatus::ENDPOINT_TRANSIENT,
|
||||
* Integrations\Divi\Release\ReleaseResolver::CACHE,
|
||||
* ChangeLog\ChangeLogCleanup::HOOK, ChangeLog\ChangeLog::TABLE, Admin\LogTab::PER_PAGE_OPTION and
|
||||
* Updater\RecheckOnInstall::OPTION on purpose;
|
||||
* tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in sync with
|
||||
@@ -35,6 +37,7 @@ final class Uninstaller
|
||||
private const OPTIONS = [
|
||||
'lit_mcp_allowed_roles', 'lit_mcp_exposure', 'lit_mcp_limits', 'lit_mcp_db_version',
|
||||
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_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',
|
||||
'lit_mcp_lock_divi_options', 'lit_mcp_lock_variables', 'lit_mcp_lock_presets', 'lit_mcp_lock_custom_css',
|
||||
|
||||
@@ -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])
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
$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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)]);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user