Files
WP-LIT-MCP/tests/Unit/PluginTest.php
T
Jan-Niclas LoosenandClaude Opus 5.5 1a294b7881 feat(pods): Verhaltensprobe als Prüfschritt
Beim Prüfen schreibt und liest PodsProbe auf einem Wegwerf-Entwurf ein Textfeld genau so, wie LIT MCP es tut, vergleicht die Werte und löscht den Entwurf wieder. Bevorzugt von Pods angelegte Typen; ohne Pod mit Textfeld gilt der Schritt als grün mit Hinweis „nicht geprüft“. Ein roter Schritt sperrt Pods über die Berichtssperre. Liegengebliebene Probe-Einträge räumt der Load-Hook des Reiters auf. Pods-Smoke um Probe und Bereinigung erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:33:26 +01:00

210 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit;
use Brain\Monkey\Actions;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\ExposurePolicy;
use LIT\MCP\Abilities\ToolAvailability;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\Compatibility\CapabilityGate;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate;
use LIT\MCP\Integrations\Pods\PodsFeature;
use LIT\MCP\Integrations\Pods\PodsFieldSource;
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
use LIT\MCP\Integrations\WordPress\Schema\SampledMetaSource;
use LIT\MCP\Integrations\Core\SiteStatusAbility;
use LIT\MCP\Access;
use ReflectionProperty;
use WP_User;
use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Limits;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeChangeLog;
use Mockery;
/** The wiring of the composition root: Plugin::integrations() and the steps Plugin::boot() takes with it. */
final class PluginTest extends TestCase
{
private static function registry(ToolAvailability $tools = new ToolAvailability()): IntegrationRegistry
{
return Plugin::integrations(
Mockery::mock(ContentSchema::class),
FakeChangeLog::forbidden(),
new Limits(),
$tools
);
}
/** Registry order = order of the Tools tab, the guide and the server instructions. */
public function testIntegrationsInRegistryOrder(): void
{
self::assertSame(
['core', 'wordpress', 'media', 'divi', 'divi-library', 'divi-theme-builder', 'pods'],
self::registry()->ids()
);
}
/** Divi and Pods boot (hooks, orphan cleanup); a second boot registers nothing twice. */
public function testTheBootableIntegrationsBootExactlyOnce(): void
{
Actions\expectAdded('upgrader_process_complete')->once();
Actions\expectAdded('lit_mcp_compat_changed')->once();
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(2); // Divi and Pods
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
$registry = self::registry();
$registry->boot();
$registry->boot();
}
/**
* No temporal coupling: building and booting the integrations never asks the Tools-tab state (an unbound
* ToolAvailability throws when asked); bound afterwards, it answers from the bound policy.
*/
public function testTheToolsTabStateIsAskedOnlyAfterBind(): void
{
Functions\stubTranslationFunctions();
$tools = new ToolAvailability();
$registry = self::registry($tools);
$registry->boot();
$tools->bind($registry, new ExposurePolicy(['@group:divi-library' => ExposurePolicy::DISABLED]));
self::assertFalse($tools->groupEnabled('divi-library'));
self::assertTrue($tools->groupWritable('divi-theme-builder'));
self::assertFalse($tools->allows('lit/divi-insert'), 'no Divi in unit tests: lit/divi-insert not registered');
}
/** Pods has no tools: it is in the registry for its profile only and shows up as no group. */
public function testPodsIsRegisteredWithoutBecomingAToolGroup(): void
{
$registry = self::registry();
self::assertContains('pods', $registry->ids());
self::assertArrayNotHasKey('pods', $registry->grouped());
}
/** lit-site-status keeps its order: Divi, Pods, The Events Calendar. */
public function testSiteStatusListsDiviPodsAndTheEventsCalendarInThatOrder(): void
{
Functions\when('get_bloginfo')->justReturn('x');
Functions\when('home_url')->justReturn('https://example.test');
Functions\when('wp_get_current_user')->justReturn(new WP_User(1, ['administrator']));
Functions\when('get_option')->justReturn(['administrator']);
Functions\when('apply_filters')->returnArg(2);
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn('twentytwentyfive');
Functions\when('wp_get_theme')->justReturn($theme);
$status = array_values(array_filter(
self::registry()->abilities(),
static fn ($ability): bool => $ability instanceof SiteStatusAbility
))[0];
$components = $status->execute([])['components'];
self::assertSame(['divi', 'pods', 'the-events-calendar'], array_column($components, 'id'));
$pods = array_column($components, null, 'id')['pods'];
self::assertFalse($pods['active'], 'no Pods in unit tests');
self::assertSame([], $pods['problems']);
}
public function testTheCompatibilityCoversAllProfilesWithoutIdCollision(): void
{
$compat = self::registry()->compatibility();
self::assertSame(['divi', 'pods'], array_map(static fn (Profile $p): string => $p->id, $compat->profiles()));
self::assertNotNull($compat->capability('page-write'));
self::assertNotNull($compat->capability('pods-field-read'));
self::assertSame('divi', $compat->owner($compat->capability('page-write'))?->id);
self::assertSame('pods', $compat->owner($compat->capability('pods-field-write'))?->id);
self::assertNull($compat->capability('nobody-knows-me'));
}
/** The lock of unknown IDs lives in CapabilityGate: without it a core tool with an odd ID would run unlocked. */
public function testBothGuardsAreWiredDiviFirst(): void
{
$guards = Plugin::guards(self::registry(), FakeChangeLog::forbidden());
self::assertCount(2, $guards);
self::assertInstanceOf(DiviFeatureGate::class, $guards[0]);
self::assertInstanceOf(CapabilityGate::class, $guards[1]);
}
/** The tab "Compatibility" gets the registry's Compatibility: one section per profile, Divi and Pods. */
public function testTheCompatibilityTabShowsEveryProfileOfTheRegistry(): void
{
Functions\stubEscapeFunctions();
Functions\stubTranslationFunctions();
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn('twentytwentyfive');
Functions\when('wp_get_theme')->justReturn($theme);
$tab = Plugin::compatibilityTab(self::registry(), FakeChangeLog::forbidden());
ob_start();
$tab->render();
$html = (string) ob_get_clean();
self::assertInstanceOf(CompatibilityTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
self::assertStringContainsString('id="lit-mcp-compat-divi"', $html);
self::assertStringContainsString('id="lit-mcp-compat-pods"', $html);
}
private static function readOnly(object $source): bool
{
return (new ReflectionProperty($source, 'readOnly'))->getValue($source);
}
private static function podsCompat(bool $usable): Compatibility
{
return new Compatibility([new Profile(
id: 'pods',
label: 'Pods',
tier: Tier::LockOnFailure,
capabilities: PodsFeature::cases(),
active: static fn (): bool => $usable
)]);
}
public function testWithoutPodsTheMetaSourcesStayWritableAndPodsIsNotASource(): void
{
$sources = Plugin::schemaSources(self::podsCompat(false), false);
self::assertCount(2, $sources);
self::assertInstanceOf(RegisteredMetaSource::class, $sources[0]);
self::assertInstanceOf(SampledMetaSource::class, $sources[1]);
self::assertFalse(self::readOnly($sources[0]));
self::assertFalse(self::readOnly($sources[1]));
}
public function testWithUsablePodsItsSourceComesFirstAndTheMetaSourcesStayWritable(): void
{
$sources = Plugin::schemaSources(self::podsCompat(true), true);
self::assertInstanceOf(PodsFieldSource::class, $sources[0]);
self::assertFalse(self::readOnly($sources[1]));
self::assertFalse(self::readOnly($sources[2]));
}
/** Pods active but its schema locked: its meta keys must not be written past Pods. */
public function testWithActiveButLockedPodsTheMetaSourcesAreReadOnly(): void
{
$sources = Plugin::schemaSources(self::podsCompat(false), true);
self::assertInstanceOf(PodsFieldSource::class, $sources[0]);
self::assertTrue(self::readOnly($sources[1]));
self::assertTrue(self::readOnly($sources[2]));
}
}