Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.9 KiB
PHP
105 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Abilities;
|
|
|
|
use LIT\MCP\Abilities\Ability;
|
|
use LIT\MCP\Abilities\ExposurePolicy;
|
|
use LIT\MCP\Abilities\ToolAvailability;
|
|
use LIT\MCP\Integrations\Integration;
|
|
use LIT\MCP\Integrations\IntegrationRegistry;
|
|
use LogicException;
|
|
use Mockery;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ToolAvailabilityTest extends TestCase
|
|
{
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private static function registry(string ...$names): IntegrationRegistry
|
|
{
|
|
$abilities = array_map(static function (string $name): Ability {
|
|
$ability = Mockery::mock(Ability::class);
|
|
$ability->allows('name')->andReturn($name);
|
|
$ability->allows('isDirect')->andReturn(false);
|
|
|
|
return $ability;
|
|
}, $names);
|
|
$integration = Mockery::mock(Integration::class);
|
|
$integration->allows('id')->andReturn('divi');
|
|
$integration->allows('isAvailable')->andReturn(true);
|
|
$integration->allows('abilities')->andReturn($abilities);
|
|
|
|
return new IntegrationRegistry($integration);
|
|
}
|
|
|
|
/** The temporal coupling of the former use-by-reference closures: they answered false until Plugin set $policy. */
|
|
public function testAQuestionBeforeBindIsAWiringErrorNotFalse(): void
|
|
{
|
|
$tools = new ToolAvailability();
|
|
|
|
foreach ([
|
|
static fn (): bool => $tools->groupEnabled('divi-library'),
|
|
static fn (): bool => $tools->groupWritable('divi-library'),
|
|
static fn (): bool => $tools->allows('lit/divi-insert'),
|
|
] as $ask) {
|
|
try {
|
|
$ask();
|
|
self::fail('answered before bind()');
|
|
} catch (LogicException $e) {
|
|
self::assertSame('ToolAvailability asked before Plugin bound it.', $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/** A gate built before bind() (as DiviIntegration builds its gates) answers from the policy bound later. */
|
|
public function testAGateBuiltBeforeBindResolvesLazily(): void
|
|
{
|
|
$tools = new ToolAvailability();
|
|
$libraryVisible = static fn (): bool => $tools->groupEnabled('divi-library');
|
|
$libraryWritable = static fn (): bool => $tools->groupWritable('divi-library');
|
|
$diviWritable = static fn (): bool => $tools->allows('lit/divi-insert');
|
|
|
|
$tools->bind(self::registry('lit/divi-insert'), new ExposurePolicy(['@readonly:divi-library' => true]));
|
|
|
|
self::assertTrue($libraryVisible());
|
|
self::assertFalse($libraryWritable());
|
|
self::assertTrue($diviWritable());
|
|
}
|
|
|
|
public function testFollowsTheBoundPolicy(): void
|
|
{
|
|
$tools = new ToolAvailability();
|
|
$tools->bind(
|
|
self::registry('lit/divi-insert', 'lit/library-insert'),
|
|
new ExposurePolicy([
|
|
'@group:divi-theme-builder' => ExposurePolicy::DISABLED,
|
|
'lit/library-insert' => ExposurePolicy::DISABLED,
|
|
])
|
|
);
|
|
|
|
self::assertFalse($tools->groupEnabled('divi-theme-builder'));
|
|
self::assertFalse($tools->groupWritable('divi-theme-builder'));
|
|
self::assertTrue($tools->groupEnabled('divi-library'));
|
|
self::assertTrue($tools->groupWritable('divi-library'));
|
|
self::assertTrue($tools->allows('lit/divi-insert'));
|
|
self::assertFalse($tools->allows('lit/library-insert'));
|
|
self::assertFalse($tools->allows('lit/unknown'), 'not registered = not allowed (ExposurePolicy::allows)');
|
|
}
|
|
|
|
public function testBindsOnlyOnce(): void
|
|
{
|
|
$tools = new ToolAvailability();
|
|
$tools->bind(self::registry(), new ExposurePolicy());
|
|
|
|
$this->expectException(LogicException::class);
|
|
$this->expectExceptionMessage('ToolAvailability is already bound.');
|
|
$tools->bind(self::registry(), new ExposurePolicy());
|
|
}
|
|
}
|