Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php
T
Jan-Niclas LoosenandClaude Opus 5.5 7d6a97a54e 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>
2026-11-06 12:51:03 +01:00

321 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
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\Compatibility\ReleaseStore;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ReleaseResolverTest extends TestCase
{
private const UNKNOWN = '5.99.0';
/** @var array<string, mixed> */
private array $transients = [];
private int $mirrorRuns = 0;
private int $codeRuns = 0;
protected function setUp(): void
{
parent::setUp();
$this->transients = [];
$this->mirrorRuns = 0;
$this->codeRuns = 0;
Functions\when('get_transient')->alias(fn (string $key): mixed => $this->transients[$key] ?? false);
Functions\when('set_transient')->alias(function (string $key, mixed $value): bool {
$this->transients[$key] = $value;
return true;
});
}
/**
* @param list<Problem> $code
* @param list<Problem> $mirrors
* @param array<string, mixed>|null $entry
*/
private function resolver(
string $version,
array $code = [],
array $mirrors = [],
?array $entry = null
): ReleaseResolver {
$check = Mockery::mock(ManifestCheck::class);
$check->allows('codeProblems')->andReturnUsing(function () use ($code): array {
++$this->codeRuns;
return $code;
});
$check->allows('mirrorProblems')->andReturnUsing(function () use ($mirrors): array {
++$this->mirrorRuns;
return $mirrors;
});
$store = Mockery::mock(ReleaseStore::class);
$store->allows('find')->with($version, Plugin::VERSION)->andReturn($entry);
return new ReleaseResolver(
$check,
$store,
static fn (): ?string => $version,
static fn (string $v): string => 'key-' . $v
);
}
/** @param list<DiviFeature> $features */
private static function problem(string $kind, array $features, string $label = 'Seiten: et_x()'): Problem
{
return new Problem($label, $features, $kind);
}
/** @return array<string, string> Feature value => state, for every feature. */
private static function states(ReleaseStatus $status): array
{
$states = [];
foreach (DiviFeature::cases() as $feature) {
$states[$feature->value] = $status->matrix->of($feature)->state->value;
}
return $states;
}
/**
* @param array<string, string> $except Feature value => state.
* @return array<string, string>
*/
private static function allBut(string $state, array $except = []): array
{
$states = [];
foreach (DiviFeature::cases() as $feature) {
$states[$feature->value] = $except[$feature->value] ?? $state;
}
return $states;
}
public function testATestedVersionWithoutProblemsIsReleasedFromCode(): void
{
$status = $this->resolver('5.2.1')->resolve();
self::assertSame(ReleaseState::Released, $status->state);
self::assertSame('code', $status->source);
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame('code', $status->matrix->of(DiviFeature::PageWrite)->source);
}
public function testAReleaseCoversChangedSignaturesAndMissingReferences(): void
{
$status = $this->resolver('5.2.1', [
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
])->resolve();
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame([], $status->matrix->unusable());
self::assertSame([], $status->problems);
}
public function testAHardProblemLocksItsFeaturesEvenForATestedVersion(): void
{
$status = $this->resolver('5.2.1', [
self::problem(Problem::CALL, [DiviFeature::PageWrite], 'Seiten: et_x()'),
self::problem(Problem::CONSTANT, [DiviFeature::Catalog], 'Katalog: Konstante X'),
])->resolve();
self::assertSame(
self::allBut('released', ['page-write' => 'incompatible', 'catalog' => 'incompatible']),
self::states($status)
);
self::assertSame(['Seiten: et_x()'], $status->matrix->of(DiviFeature::PageWrite)->problems);
self::assertSame(ReleaseState::Incompatible, $status->state);
self::assertSame(['Seiten: et_x()', 'Katalog: Konstante X'], $status->problems);
self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable());
}
public function testAProblemWithoutFeaturesLocksEveryFeature(): void
{
$status = $this->resolver('5.2.1', [self::problem(Problem::CALL, [])])->resolve();
self::assertSame(self::allBut('incompatible'), self::states($status));
}
/** Review focus 2: a changed mirror locks exactly its features – released version or automatic release. */
public function testAChangedMirrorLocksExactlyItsFeatures(): void
{
$mirror = self::problem(
Problem::MIRROR,
[DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite],
'Theme Builder: Nachbau von y() – z'
);
$expected = ['tb-write' => 'incompatible', 'library-write' => 'incompatible'];
self::assertSame(
self::allBut('released', $expected),
self::states($this->resolver('5.2.1', mirrors: [$mirror])->resolve())
);
$automatic = $this->resolver(self::UNKNOWN, mirrors: [$mirror])->resolve();
self::assertSame(self::allBut('released', $expected), self::states($automatic));
self::assertSame(
['Theme Builder: Nachbau von y() – z'],
$automatic->matrix->of(DiviFeature::ThemeBuilderWrite)->problems
);
}
public function testAnUnknownVersionWithoutProblemsIsReleasedAutomatically(): void
{
$status = $this->resolver(self::UNKNOWN)->resolve();
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame('auto', $status->source);
self::assertSame('auto', $status->matrix->of(DiviFeature::PresetWrite)->source);
self::assertSame([], $status->matrix->unusable());
}
/** Review focus 3: the automatic release needs every signature unchanged. */
public function testAChangedSignatureMakesItsFeatureIncompatibleUnderAutomaticRelease(): void
{
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite], 'Designsystem: x() – Signatur geändert'),
])->resolve();
self::assertSame(self::allBut('released', ['preset-write' => 'incompatible']), self::states($status));
self::assertSame(
['Designsystem: x() – Signatur geändert'],
$status->matrix->of(DiviFeature::PresetWrite)->problems
);
self::assertFalse($status->matrix->of(DiviFeature::PresetWrite)->usable());
}
public function testAMissingReferenceMakesItsFeatureUntestedUnderAutomaticRelease(): void
{
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Roles], 'Seiten: r() – Referenz fehlt'),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::PageRead]),
self::problem(Problem::SIGNATURE, [DiviFeature::PageRead]),
])->resolve();
self::assertSame(
self::allBut('released', ['roles' => 'untested', 'page-read' => 'incompatible']),
self::states($status)
);
self::assertSame(
['Seiten: r() – Referenz fehlt'],
$status->matrix->of(DiviFeature::Roles)->problems
);
}
/** Review focus 1: unknown version with the automatic release switched off. */
public function testWithoutAutomaticReleaseAnUnknownVersionIsUntestedUnlessSomethingHardFailed(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->with(true)->andReturn(false);
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite]),
self::problem(Problem::CALL, [DiviFeature::Catalog]),
])->resolve();
self::assertSame(self::allBut('untested', ['catalog' => 'incompatible']), self::states($status));
self::assertNull($status->matrix->of(DiviFeature::PageRead)->source);
}
public function testOnlyALiteralTrueSwitchesTheAutomaticReleaseOn(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->andReturn(1);
self::assertSame(self::allBut('untested'), self::states($this->resolver(self::UNKNOWN)->resolve()));
}
public function testADatabaseReleaseReleasesEveryFeatureWithoutHardProblems(): void
{
$entry = ['user_id' => 1, 'released_at' => 'd'];
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
], [self::problem(Problem::MIRROR, [DiviFeature::LibraryWrite])], $entry)->resolve();
self::assertSame(self::allBut('released', ['library-write' => 'incompatible']), self::states($status));
self::assertSame('database', $status->matrix->of(DiviFeature::PageWrite)->source);
self::assertSame($entry, $status->release);
}
public function testWithoutDiviNothingIsReleasedAutomatically(): void
{
$status = $this->resolver('')->resolve();
self::assertSame(ReleaseState::Untested, $status->state);
self::assertSame(self::allBut('untested'), self::states($status));
}
public function testCodeProblemsRunOnEveryResolveMirrorProblemsOncePerStateKey(): void
{
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::ThemeBuilderWrite], 'Theme Builder: Nachbau von y()')];
$first = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
$second = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
self::assertSame(2, $this->codeRuns, 'code and signature checks are live');
self::assertSame(1, $this->mirrorRuns, 'the second resolve reads the transient');
self::assertSame(self::states($first), self::states($second));
self::assertSame($first->problems, $second->problems);
self::assertSame(
['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
['label' => 'Theme Builder: Nachbau von y()', 'kind' => 'mirror', 'features' => ['tb-write']],
]],
$this->transients[ReleaseResolver::CACHE]
);
$this->resolver('5.2.2', mirrors: $mirrors)->resolve();
self::assertSame(2, $this->mirrorRuns, 'another state key checks again');
}
/** Review focus 1: an old or broken transient is never trusted. */
public function testAnOldOrBrokenCacheEntryIsAMiss(): void
{
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::PageWrite])];
$entries = [
'format 1' => ['key' => 'key-5.2.1', 'problems' => []],
'other format' => ['format' => 3, 'key' => 'key-5.2.1', 'problems' => []],
'unknown feature' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
['label' => 'x', 'kind' => 'mirror', 'features' => ['page-delete']],
]],
'no list' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => 'none'],
'garbage' => 'x',
];
foreach ($entries as $case => $entry) {
$this->transients[ReleaseResolver::CACHE] = $entry;
$runs = $this->mirrorRuns;
$status = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
self::assertSame($runs + 1, $this->mirrorRuns, $case);
self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable(), $case);
}
}
public function testTheCacheHoldsProblemsSoAReleaseCountsAtOnce(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->andReturn(false);
$before = $this->resolver(self::UNKNOWN)->resolve();
$after = $this->resolver(self::UNKNOWN, entry: ['user_id' => 1, 'released_at' => 'd'])->resolve();
self::assertSame(ReleaseState::Untested, $before->state);
self::assertSame(ReleaseState::Released, $after->state);
self::assertSame(1, $this->mirrorRuns);
}
}