feat: Divi release status, store, resolver and gate

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 01:46:05 +02:00
co-authored by Claude Opus 5.5
parent 6b93df94c3
commit cf3c5dfe76
14 changed files with 838 additions and 1 deletions
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ReleaseResolverTest extends TestCase
{
/** @var array<string, mixed> */
private array $transients = [];
protected function setUp(): void
{
parent::setUp();
$this->transients = [];
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 array<string, callable(): bool> $code
* @param array<string, callable(): bool> $mirrors
*/
private function resolver(
string $version,
array $code = [],
array $mirrors = [],
?array $entry = null
): ReleaseResolver {
$check = Mockery::mock(ManifestCheck::class);
$check->allows('codeChecks')->andReturn($code);
$check->allows('mirrorChecks')->andReturn($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
);
}
public function testATestedVersionWithGreenChecksIsReleasedFromCode(): void
{
$status = $this->resolver('5.2.1')->resolve();
self::assertSame(ReleaseState::Released, $status->state);
self::assertSame('code', $status->source);
}
public function testAnUnknownVersionIsUntestedUnlessTheDatabaseReleasedIt(): void
{
self::assertSame(ReleaseState::Untested, $this->resolver('5.3.0')->resolve()->state);
$released = $this->resolver('5.3.0', entry: ['user_id' => 1, 'released_at' => 'd'])->resolve();
self::assertSame(ReleaseState::Released, $released->state);
self::assertSame('database', $released->source);
}
public function testAFailedCheckMakesEvenATestedVersionIncompatible(): void
{
$status = $this->resolver('5.2.1', ['Seiten: et_x()' => static fn (): bool => false])->resolve();
self::assertSame(ReleaseState::Incompatible, $status->state);
self::assertSame(['Seiten: et_x()'], $status->problems);
}
public function testMirrorResultsAreCachedByStateKey(): void
{
$calls = 0;
$mirrors = ['Theme Builder: Nachbau von y()' => static function () use (&$calls): bool {
++$calls;
return false;
}];
$first = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
$second = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
self::assertSame(1, $calls, 'the second resolve reads the transient');
self::assertSame(ReleaseState::Incompatible, $second->state);
self::assertSame($first->problems, $second->problems);
self::assertSame('key-5.2.1', $this->transients[ReleaseResolver::CACHE]['key']);
$this->resolver('5.2.2', mirrors: $mirrors)->resolve();
self::assertSame(2, $calls, 'another state key checks again');
}
public function testWithoutDiviTheVersionIsEmptyAndUntested(): void
{
self::assertSame(ReleaseState::Untested, $this->resolver('')->resolve()->state);
}
}