merge: tool groups with inherited mode and 'nur lesen' per group
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -217,4 +217,18 @@ final class AbilityRegistrarTest extends TestCase
|
||||
(new AbilityRegistrar(new IntegrationRegistry($integration), new ExposurePolicy(['lit/test' => 'disabled'])))
|
||||
->registerAbilities();
|
||||
}
|
||||
|
||||
public function testGroupDisabledAbilitiesAreNotRegistered(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
$integration = Mockery::mock(Integration::class);
|
||||
$integration->allows('id')->andReturn('divi');
|
||||
$integration->allows('isAvailable')->andReturn(true);
|
||||
$integration->allows('abilities')->andReturn([$ability]);
|
||||
$registry = new IntegrationRegistry($integration);
|
||||
Functions\expect('wp_register_ability')->never();
|
||||
|
||||
(new AbilityRegistrar($registry, (new ExposurePolicy(['@group:divi' => 'disabled']))->withGroups($registry->groupMap())))
|
||||
->registerAbilities();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ use Mockery;
|
||||
|
||||
final class ExposurePolicyTest extends TestCase
|
||||
{
|
||||
private function ability(string $name, bool $direct): Ability
|
||||
/** @param array<string, bool>|null $annotations */
|
||||
private function ability(string $name, bool $direct, ?array $annotations = null): Ability
|
||||
{
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
$ability->allows('name')->andReturn($name);
|
||||
$ability->allows('isDirect')->andReturn($direct);
|
||||
if ($annotations !== null) {
|
||||
$ability->allows('annotations')->andReturn($annotations);
|
||||
}
|
||||
|
||||
return $ability;
|
||||
}
|
||||
@@ -113,4 +117,155 @@ final class ExposurePolicyTest extends TestCase
|
||||
|
||||
self::assertSame('direct', ExposurePolicy::fromOption()->mode($this->ability('lit/a', true)));
|
||||
}
|
||||
|
||||
private const GROUPS = [
|
||||
'lit/guide' => 'core',
|
||||
'lit/site-status' => 'core',
|
||||
'lit/divi-outline' => 'divi',
|
||||
'lit/divi-update' => 'divi',
|
||||
'lit/divi-delete' => 'divi',
|
||||
'lit/media-preview' => 'media',
|
||||
'lit/media-download' => 'media',
|
||||
'lit/media-get' => 'media',
|
||||
];
|
||||
|
||||
public function testToolOverrideBeatsGroupOverrideBeatsToolDefault(): void
|
||||
{
|
||||
$policy = (new ExposurePolicy([
|
||||
'@group:divi' => 'indirect',
|
||||
'lit/divi-update' => 'direct',
|
||||
]))->withGroups(self::GROUPS);
|
||||
|
||||
self::assertSame('direct', $policy->mode($this->ability('lit/divi-update', false)));
|
||||
self::assertSame('indirect', $policy->mode($this->ability('lit/divi-outline', true)));
|
||||
self::assertSame('direct', $policy->mode($this->ability('lit/site-status', true)));
|
||||
self::assertTrue($policy->hasToolOverride('lit/divi-update'));
|
||||
self::assertFalse($policy->hasToolOverride('lit/divi-outline'));
|
||||
self::assertSame('indirect', $policy->groupMode('divi'));
|
||||
self::assertNull($policy->groupMode('core'));
|
||||
self::assertSame('indirect', $policy->inheritedMode($this->ability('lit/divi-update', false)));
|
||||
}
|
||||
|
||||
public function testGroupSettingsAreIgnoredWithoutAGroupMap(): void
|
||||
{
|
||||
$policy = new ExposurePolicy(['@group:divi' => 'disabled', '@readonly:divi' => true]);
|
||||
|
||||
self::assertSame('direct', $policy->mode($this->ability('lit/divi-outline', true)));
|
||||
}
|
||||
|
||||
public function testGuideKeepsItsDefaultWhateverTheGroupSays(): void
|
||||
{
|
||||
$policy = (new ExposurePolicy(['@group:core' => 'disabled', '@readonly:core' => true]))
|
||||
->withGroups(self::GROUPS);
|
||||
|
||||
self::assertSame('direct', $policy->mode($this->ability('lit/guide', true, [])));
|
||||
self::assertSame('disabled', $policy->mode($this->ability('lit/site-status', true, ['readonly' => true])));
|
||||
}
|
||||
|
||||
public function testDirectOnlyToolsTakeTheNearestAllowedModeWhenTheGroupSaysIndirect(): void
|
||||
{
|
||||
$policy = (new ExposurePolicy(['@group:media' => 'indirect', 'lit/media-download' => 'disabled']))
|
||||
->withGroups(self::GROUPS);
|
||||
|
||||
$preview = $this->ability('lit/media-preview', true);
|
||||
self::assertSame('direct', $policy->mode($preview));
|
||||
self::assertTrue($policy->groupModeAdjusted($preview));
|
||||
self::assertSame('disabled', $policy->mode($this->ability('lit/media-download', true)));
|
||||
self::assertSame('indirect', $policy->mode($this->ability('lit/media-get', true)));
|
||||
self::assertFalse($policy->groupModeAdjusted($this->ability('lit/media-get', true)));
|
||||
}
|
||||
|
||||
public function testReadonlyGroupDisablesWriteToolsEvenAgainstAToolOverride(): void
|
||||
{
|
||||
$policy = (new ExposurePolicy([
|
||||
'@readonly:divi' => true,
|
||||
'@group:divi' => 'direct',
|
||||
'lit/divi-update' => 'direct',
|
||||
'lit/divi-outline' => 'indirect',
|
||||
]))->withGroups(self::GROUPS);
|
||||
|
||||
$update = $this->ability('lit/divi-update', false, ['readonly' => false, 'destructive' => false]);
|
||||
$delete = $this->ability('lit/divi-delete', false, ['destructive' => true]);
|
||||
$outline = $this->ability('lit/divi-outline', true, ['readonly' => true]);
|
||||
|
||||
self::assertSame('disabled', $policy->mode($update));
|
||||
self::assertSame('disabled', $policy->mode($delete));
|
||||
self::assertTrue($policy->disabledByReadonly($update));
|
||||
self::assertSame('indirect', $policy->mode($outline));
|
||||
self::assertFalse($policy->disabledByReadonly($outline));
|
||||
self::assertTrue($policy->isReadonly('divi'));
|
||||
self::assertFalse($policy->isReadonly('media'));
|
||||
self::assertSame([$outline], $policy->enabled([$update, $delete, $outline]));
|
||||
self::assertSame([], $policy->directNames([$update, $delete, $outline]));
|
||||
}
|
||||
|
||||
public function testSanitizeValidatesGroupEntries(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'@group:divi' => 'indirect',
|
||||
'@readonly:divi' => true,
|
||||
'@readonly:media' => true,
|
||||
'lit/menu-list' => 'disabled',
|
||||
],
|
||||
ExposurePolicy::sanitize([
|
||||
'lit/menu-list' => 'disabled',
|
||||
'@group:divi' => 'indirect',
|
||||
'@group:media' => 'sometimes',
|
||||
'@group:core' => '',
|
||||
'@group:unknown' => 'direct',
|
||||
'@group:' => 'direct',
|
||||
'@group:<b>' => 'direct',
|
||||
'@readonly:divi' => '1',
|
||||
'@readonly:media' => true,
|
||||
'@readonly:core' => 'yes',
|
||||
'@readonly:pods' => ['1'],
|
||||
'@readonly:unknown' => '1',
|
||||
'@other:divi' => 'direct',
|
||||
'@group:divi/x' => 'direct',
|
||||
0 => 'direct',
|
||||
], ['core', 'divi', 'media', 'pods'])
|
||||
);
|
||||
}
|
||||
|
||||
public function testSanitizeWithoutKnownGroupsStillChecksTheIdSyntax(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['@group:divi' => 'disabled', '@readonly:divi' => true],
|
||||
ExposurePolicy::sanitize(['@group:divi' => 'disabled', '@readonly:divi' => true, '@group:a b' => 'direct'])
|
||||
);
|
||||
}
|
||||
|
||||
public function testOldFlatOptionStaysValid(): void
|
||||
{
|
||||
$old = ['lit/content-get' => 'indirect', 'lit/menu-list' => 'direct'];
|
||||
|
||||
self::assertSame($old, ExposurePolicy::sanitize($old, ['core', 'wordpress']));
|
||||
}
|
||||
|
||||
public function testFromOptionReadsGroupEntries(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn(['@group:divi' => 'disabled', 'lit/divi-update' => 'indirect']);
|
||||
|
||||
$policy = ExposurePolicy::fromOption()->withGroups(self::GROUPS);
|
||||
|
||||
self::assertSame('disabled', $policy->mode($this->ability('lit/divi-outline', true)));
|
||||
self::assertSame('indirect', $policy->mode($this->ability('lit/divi-update', false)));
|
||||
}
|
||||
|
||||
public function testGroupMapCanBeResolvedLazily(): void
|
||||
{
|
||||
$calls = 0;
|
||||
$policy = (new ExposurePolicy(['@group:divi' => 'disabled']))->withGroups(
|
||||
static function () use (&$calls): array {
|
||||
$calls++;
|
||||
return self::GROUPS;
|
||||
}
|
||||
);
|
||||
|
||||
self::assertSame(0, $calls, 'The map must not be built while booting (Divi is not loaded yet).');
|
||||
self::assertSame('disabled', $policy->mode($this->ability('lit/divi-outline', true)));
|
||||
self::assertSame('direct', $policy->mode($this->ability('lit/site-status', true)));
|
||||
self::assertSame(1, $calls);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,53 +15,219 @@ use Mockery;
|
||||
|
||||
final class ToolsTabTest extends TestCase
|
||||
{
|
||||
private function registry(): IntegrationRegistry
|
||||
/** @param list<array{string, bool, bool}> $abilities name, isDirect, readonly */
|
||||
private function integration(string $id, array $abilities, bool $available = true): Integration
|
||||
{
|
||||
$abilities = [];
|
||||
foreach ([['lit/guide', true], ['lit/media-preview', true], ['lit/menu-list', false]] as [$name, $direct]) {
|
||||
$mocks = [];
|
||||
foreach ($abilities as [$name, $direct, $readonly]) {
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
$ability->allows('name')->andReturn($name);
|
||||
$ability->allows('label')->andReturn('Label ' . $name);
|
||||
$ability->allows('isDirect')->andReturn($direct);
|
||||
$abilities[] = $ability;
|
||||
$ability->allows('annotations')->andReturn(['readonly' => $readonly]);
|
||||
$mocks[] = $ability;
|
||||
}
|
||||
$integration = Mockery::mock(Integration::class);
|
||||
$integration->allows('isAvailable')->andReturn(true);
|
||||
$integration->allows('abilities')->andReturn($abilities);
|
||||
$integration->allows('id')->andReturn($id);
|
||||
$integration->allows('isAvailable')->andReturn($available);
|
||||
$integration->allows('abilities')->andReturn($mocks);
|
||||
|
||||
return new IntegrationRegistry($integration);
|
||||
return $integration;
|
||||
}
|
||||
|
||||
public function testRegistersTheExposureOptionWithThePolicySanitizer(): void
|
||||
private function registry(): IntegrationRegistry
|
||||
{
|
||||
Functions\expect('register_setting')->once()->with(
|
||||
'lit_mcp_tools',
|
||||
'lit_mcp_exposure',
|
||||
Mockery::on(static fn (array $args): bool => $args['sanitize_callback'] === [ExposurePolicy::class, 'sanitize'])
|
||||
return new IntegrationRegistry(
|
||||
$this->integration('core', [['lit/guide', true, true], ['lit/site-status', true, true]]),
|
||||
$this->integration('media', [['lit/media-preview', true, true], ['lit/media-upload', false, false]]),
|
||||
$this->integration('divi', [['lit/divi-outline', true, true], ['lit/divi-update', true, false]]),
|
||||
$this->integration('pods', [['lit/pods-x', true, true]], false)
|
||||
);
|
||||
|
||||
(new ToolsTab($this->registry(), new ExposurePolicy()))->registerSettings();
|
||||
}
|
||||
|
||||
public function testRendersOneSelectPerAbilityWithOnlyAllowedModes(): void
|
||||
/** @param array<string, string|bool> $option */
|
||||
private function render(array $option, ?IntegrationRegistry $registry = null): string
|
||||
{
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('settings_fields')->justReturn(null);
|
||||
Functions\when('submit_button')->justReturn(null);
|
||||
Functions\when('selected')->alias(static fn ($a, $b = true, $echo = true): string => (string) $a === (string) $b ? ' selected="selected"' : '');
|
||||
Functions\when('selected')->alias(
|
||||
static fn ($a, $b = true, $echo = true): string => (string) $a === (string) $b ? ' selected="selected"' : ''
|
||||
);
|
||||
Functions\when('checked')->alias(
|
||||
static fn ($a, $b = true, $echo = true): string => (string) $a === (string) $b ? ' checked="checked"' : ''
|
||||
);
|
||||
|
||||
ob_start();
|
||||
(new ToolsTab($this->registry(), new ExposurePolicy(['lit/menu-list' => 'disabled'])))->render();
|
||||
$html = (string) ob_get_clean();
|
||||
(new ToolsTab($registry ?? $this->registry(), new ExposurePolicy($option)))->render();
|
||||
|
||||
$selects = array_slice(explode('<select', $html), 1);
|
||||
self::assertCount(3, $selects);
|
||||
[$guide, $preview, $menu] = $selects;
|
||||
self::assertStringContainsString('lit_mcp_exposure[lit/guide]', $guide);
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
private function select(string $html, string $key): string
|
||||
{
|
||||
$start = strpos($html, '<select name="lit_mcp_exposure[' . $key . ']"');
|
||||
self::assertNotFalse($start, 'No select for ' . $key);
|
||||
|
||||
return substr($html, $start, (int) strpos($html, '</select>', $start) - $start);
|
||||
}
|
||||
|
||||
/** The table row of one tool (from its <tr> to </tr>). */
|
||||
private function row(string $html, string $name): string
|
||||
{
|
||||
$at = strpos($html, '<code>' . $name . '</code>');
|
||||
self::assertNotFalse($at, 'No row for ' . $name);
|
||||
$start = (int) strrpos(substr($html, 0, $at), '<tr');
|
||||
|
||||
return substr($html, $start, (int) strpos($html, '</tr>', $at) - $start);
|
||||
}
|
||||
|
||||
/** The <details> block of one group. */
|
||||
private function group(string $html, string $label): string
|
||||
{
|
||||
$at = strpos($html, '<summary>' . $label);
|
||||
self::assertNotFalse($at, 'No group ' . $label);
|
||||
$start = (int) strrpos(substr($html, 0, $at), '<details');
|
||||
|
||||
return substr($html, $start, (int) strpos($html, '</details>', $at) - $start);
|
||||
}
|
||||
|
||||
public function testRegistersTheExposureOptionWithAGroupAwareSanitizer(): void
|
||||
{
|
||||
$tab = new ToolsTab($this->registry(), new ExposurePolicy());
|
||||
$callback = null;
|
||||
Functions\expect('register_setting')->once()->with(
|
||||
'lit_mcp_tools',
|
||||
'lit_mcp_exposure',
|
||||
Mockery::on(static function (array $args) use (&$callback): bool {
|
||||
$callback = $args['sanitize_callback'];
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
$tab->registerSettings();
|
||||
|
||||
self::assertSame([$tab, 'sanitize'], $callback);
|
||||
self::assertSame(
|
||||
['@group:pods' => 'disabled', '@readonly:divi' => true, 'lit/menu-list' => 'disabled'],
|
||||
$tab->sanitize([
|
||||
'@readonly:divi' => '1',
|
||||
'@group:pods' => 'disabled',
|
||||
'@group:shop' => 'direct',
|
||||
'lit/menu-list' => 'disabled',
|
||||
'lit/guide' => 'disabled',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function testGroupsToolsByAvailableIntegrationWithOnlyAllowedModes(): void
|
||||
{
|
||||
$html = $this->render(['lit/divi-update' => 'disabled']);
|
||||
|
||||
self::assertSame(3, substr_count($html, '<details open'));
|
||||
self::assertStringContainsString('<summary>Grundlagen (2 Tools)', $html);
|
||||
self::assertStringContainsString('<summary>Medien (2 Tools)', $html);
|
||||
self::assertStringContainsString('<summary>Divi (2 Tools)', $html);
|
||||
self::assertStringNotContainsString('pods', $html);
|
||||
self::assertStringContainsString('lit/divi-outline', $this->group($html, 'Divi'));
|
||||
self::assertStringNotContainsString('lit/guide', $this->group($html, 'Divi'));
|
||||
|
||||
$groupSelect = $this->select($html, '@group:divi');
|
||||
self::assertStringContainsString('<option value="" selected="selected">Standard der Tools</option>', $groupSelect);
|
||||
foreach (['direct', 'indirect', 'disabled'] as $mode) {
|
||||
self::assertStringContainsString('value="' . $mode . '"', $groupSelect);
|
||||
}
|
||||
self::assertStringContainsString('name="lit_mcp_exposure[@readonly:divi]" value="1"', $html);
|
||||
self::assertStringContainsString('nur lesen (schreibende Tools dieser Gruppe aus)', $html);
|
||||
|
||||
$guide = $this->select($html, 'lit/guide');
|
||||
self::assertStringNotContainsString('value="disabled"', $guide);
|
||||
self::assertStringNotContainsString('value="indirect"', $preview);
|
||||
self::assertStringContainsString('value="disabled" selected="selected"', $menu);
|
||||
self::assertStringContainsString('value="" selected="selected"', $guide);
|
||||
self::assertStringNotContainsString('value="indirect"', $guide);
|
||||
self::assertStringContainsString('value="" selected="selected">Standard (direkt)', $guide);
|
||||
self::assertStringNotContainsString('value="indirect"', $this->select($html, 'lit/media-preview'));
|
||||
self::assertStringContainsString('value="disabled" selected="selected"', $this->select($html, 'lit/divi-update'));
|
||||
}
|
||||
|
||||
public function testToolsOfAGroupWithOverrideOfferWieGruppeAndTheGroupSaysGemischt(): void
|
||||
{
|
||||
$html = $this->render(['@group:divi' => 'indirect', 'lit/divi-update' => 'direct']);
|
||||
|
||||
self::assertStringContainsString('value="indirect" selected="selected"', $this->select($html, '@group:divi'));
|
||||
self::assertStringContainsString(
|
||||
'<option value="" selected="selected">wie Gruppe (indirekt)</option>',
|
||||
$this->select($html, 'lit/divi-outline')
|
||||
);
|
||||
$update = $this->select($html, 'lit/divi-update');
|
||||
self::assertStringContainsString('<option value="">wie Gruppe (indirekt)</option>', $update);
|
||||
self::assertStringContainsString('value="direct" selected="selected"', $update);
|
||||
self::assertStringContainsString('gemischt', $this->group($html, 'Divi'));
|
||||
self::assertStringNotContainsString('gemischt', $this->group($html, 'Medien'));
|
||||
self::assertStringContainsString('Standard (direkt)', $this->select($html, 'lit/site-status'));
|
||||
}
|
||||
|
||||
public function testDirectOnlyToolsExplainThatIndirectDoesNotApply(): void
|
||||
{
|
||||
$html = $this->render(['@group:media' => 'indirect']);
|
||||
|
||||
self::assertStringContainsString('wie Gruppe (direkt)', $this->select($html, 'lit/media-preview'));
|
||||
self::assertStringContainsString('„indirekt“ geht hier nicht', $this->row($html, 'lit/media-preview'));
|
||||
self::assertStringNotContainsString('geht hier nicht', $this->row($html, 'lit/media-upload'));
|
||||
}
|
||||
|
||||
public function testReadonlyDisablesWriteToolSelectsButKeepsTheirOverride(): void
|
||||
{
|
||||
$html = $this->render(['@readonly:divi' => true, 'lit/divi-update' => 'indirect']);
|
||||
|
||||
self::assertStringContainsString('name="lit_mcp_exposure[@readonly:divi]" value="1" checked="checked"', $html);
|
||||
$update = $this->row($html, 'lit/divi-update');
|
||||
self::assertStringContainsString('<select name="lit_mcp_exposure[lit/divi-update]" disabled', $update);
|
||||
self::assertStringContainsString('durch ‚nur lesen‘ aus', $update);
|
||||
self::assertStringContainsString(
|
||||
'<input type="hidden" name="lit_mcp_exposure[lit/divi-update]" value="indirect">',
|
||||
$update
|
||||
);
|
||||
$outline = $this->row($html, 'lit/divi-outline');
|
||||
self::assertStringNotContainsString('disabled>', $outline);
|
||||
self::assertStringNotContainsString('nur lesen', $outline);
|
||||
}
|
||||
|
||||
public function testCountsDirectToolsUnderTheSavedSettings(): void
|
||||
{
|
||||
$html = $this->render(['@group:divi' => 'disabled']);
|
||||
|
||||
// guide, site-status, media-preview (divi disabled, media-upload indirect by default).
|
||||
self::assertStringContainsString('Direkt sichtbar: 3 Tools', $html);
|
||||
self::assertStringNotContainsString('Kontext', $html);
|
||||
self::assertLessThan(strpos($html, '<details'), strpos($html, 'Direkt sichtbar'));
|
||||
}
|
||||
|
||||
public function testWarnsWhenMoreThan25ToolsAreDirect(): void
|
||||
{
|
||||
$many = [];
|
||||
for ($i = 1; $i <= 26; $i++) {
|
||||
$many[] = ['lit/tool-' . $i, true, true];
|
||||
}
|
||||
$registry = new IntegrationRegistry($this->integration('wordpress', $many));
|
||||
|
||||
$warned = $this->render([], $registry);
|
||||
$notWarned = $this->render(['lit/tool-1' => 'indirect'], $registry);
|
||||
|
||||
self::assertStringContainsString('Direkt sichtbar: 26 Tools', $warned);
|
||||
self::assertStringContainsString('Kontext', $warned);
|
||||
self::assertStringContainsString('<summary>WordPress (26 Tools)', $warned);
|
||||
self::assertStringContainsString('Direkt sichtbar: 25 Tools', $notWarned);
|
||||
self::assertStringNotContainsString('Kontext', $notWarned);
|
||||
}
|
||||
|
||||
public function testUnknownGroupsUseTheirIdAndEverythingIsEscaped(): void
|
||||
{
|
||||
$registry = new IntegrationRegistry($this->integration('shop', [['lit/shop-<b>', true, true]]));
|
||||
|
||||
$html = $this->render([], $registry);
|
||||
|
||||
self::assertStringContainsString('<summary>shop (1 Tool)', $html);
|
||||
self::assertStringNotContainsString('<b>', $html);
|
||||
self::assertStringContainsString('lit/shop-<b>', $html);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,24 @@ final class InstructionsBuilderTest extends TestCase
|
||||
|
||||
self::assertStringContainsString('lit-menu-list', $text);
|
||||
}
|
||||
|
||||
public function testGroupDisabledAndReadonlyToolsAreNotListedAsDirect(): void
|
||||
{
|
||||
$write = $this->ability('lit/divi-update', true);
|
||||
$write->allows('annotations')->andReturn(['readonly' => false]);
|
||||
$read = $this->ability('lit/divi-outline', true);
|
||||
$read->allows('annotations')->andReturn(['readonly' => true]);
|
||||
$divi = Mockery::mock(Integration::class);
|
||||
$divi->allows('id')->andReturn('divi');
|
||||
$divi->allows('isAvailable')->andReturn(true);
|
||||
$divi->allows('abilities')->andReturn([$write, $read]);
|
||||
$core = $this->coreWith($this->ability('lit/guide', true), $this->ability('lit/menu-list', true));
|
||||
$registry = new IntegrationRegistry($core, $divi);
|
||||
$policy = (new ExposurePolicy(['@group:core' => 'disabled', '@readonly:divi' => true]))
|
||||
->withGroups($registry->groupMap());
|
||||
|
||||
$text = (new InstructionsBuilder($registry, null, $policy))->build();
|
||||
|
||||
self::assertStringContainsString('Direkt aufrufbare Tools: lit-guide, lit-divi-outline.', $text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,28 @@ final class IntegrationRegistryTest extends TestCase
|
||||
self::assertSame([$direct, $indirect], $registry->abilities());
|
||||
}
|
||||
|
||||
public function testGroupsAbilitiesByIntegrationIdAndMapsNamesToGroups(): void
|
||||
{
|
||||
$guide = $this->ability('lit/guide', true);
|
||||
$outline = $this->ability('lit/divi-outline', true);
|
||||
$hidden = $this->ability('lit/pods-x', true);
|
||||
$divi = Mockery::mock(Integration::class);
|
||||
$divi->allows('id')->andReturn('divi');
|
||||
$divi->allows('isAvailable')->andReturn(true);
|
||||
$divi->expects('abilities')->once()->andReturn([$outline]);
|
||||
|
||||
$registry = new IntegrationRegistry(
|
||||
$this->integration('core', true, [$guide]),
|
||||
$divi,
|
||||
$this->integration('pods', false, [$hidden])
|
||||
);
|
||||
|
||||
self::assertSame([$guide, $outline], $registry->abilities());
|
||||
self::assertSame(['core' => [$guide], 'divi' => [$outline]], $registry->grouped());
|
||||
self::assertSame(['lit/guide' => 'core', 'lit/divi-outline' => 'divi'], $registry->groupMap());
|
||||
self::assertSame(['core', 'divi', 'pods'], $registry->ids());
|
||||
}
|
||||
|
||||
private function ability(string $name, bool $direct): Ability
|
||||
{
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
|
||||
@@ -32,6 +32,7 @@ final class ServerRegistrarTest extends TestCase
|
||||
$indirect = Mockery::mock(Ability::class);
|
||||
$indirect->allows('name')->andReturn('lit/rare-tool');
|
||||
$indirect->allows('isDirect')->andReturn(false);
|
||||
$indirect->allows('annotations')->andReturn([]);
|
||||
|
||||
$core = Mockery::mock(Integration::class);
|
||||
$core->allows('id')->andReturn('core');
|
||||
@@ -89,6 +90,24 @@ final class ServerRegistrarTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testGroupSettingsReachTheToolListAndTheInstructions(): void
|
||||
{
|
||||
$registry = $this->registry();
|
||||
$policy = (new ExposurePolicy(['@group:core' => 'disabled', '@readonly:core' => true]))
|
||||
->withGroups($registry->groupMap());
|
||||
$instructions = new InstructionsBuilder($registry, null, $policy);
|
||||
|
||||
$args = (new ServerRegistrar($registry, $instructions, static fn () => true, null, null, $policy))
|
||||
->serverArguments();
|
||||
|
||||
// lit/guide is REQUIRED and stays direct; lit/rare-tool is disabled by the group.
|
||||
self::assertSame(
|
||||
['lit/guide', 'mcp-adapter/discover-abilities', 'mcp-adapter/get-ability-info', 'mcp-adapter/execute-ability'],
|
||||
$args['tools']
|
||||
);
|
||||
self::assertStringContainsString('Direkt aufrufbare Tools: lit-guide.', $args['description']);
|
||||
}
|
||||
|
||||
public function testNoServerWhenOauthIsDisabled(): void
|
||||
{
|
||||
$registrar = new ServerRegistrar($this->registry(), $this->instructions(), static fn () => false);
|
||||
|
||||
Reference in New Issue
Block a user