feat: DiviFeatureGate ersetzt DiviToolGate – Ablehnung je Fähigkeit für Lese- und Schreib-Tools, Details nach D6, kein Hinweis mehr in warnings; DiviContentComposer::refusal prüft seine Fähigkeiten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fdbda8012c
commit
74cf0c54a8
@@ -12,14 +12,17 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\ExposurePolicy;
|
||||
use LIT\MCP\Abilities\ToolRefusal;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviToolGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Server\RequestContext;
|
||||
use LIT\MCP\Tests\Support\DiviFeatureFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
@@ -60,12 +63,18 @@ final class AbilityRegistrarTest extends TestCase
|
||||
$integration->allows('abilities')->andReturn([$ability]);
|
||||
$registry = new IntegrationRegistry($integration);
|
||||
|
||||
return new AbilityRegistrar($registry, null, [new DiviToolGate($registry->groupMap(...))]);
|
||||
return new AbilityRegistrar($registry, null, [new DiviFeatureGate()]);
|
||||
}
|
||||
|
||||
private function ability(bool $readonly = true): Mockery\MockInterface
|
||||
/** @param list<DiviFeature>|null $features null = a tool that declares no Divi features */
|
||||
private function ability(bool $readonly = true, ?array $features = null): Mockery\MockInterface
|
||||
{
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
$ability = $features === null
|
||||
? Mockery::mock(Ability::class)
|
||||
: Mockery::mock(Ability::class, RequiresDiviFeatures::class);
|
||||
if ($features !== null) {
|
||||
$ability->allows('diviFeatures')->andReturn($features);
|
||||
}
|
||||
$ability->allows('name')->andReturn('lit/test');
|
||||
$ability->allows('label')->andReturn('Test');
|
||||
$ability->allows('description')->andReturn('Test ability');
|
||||
@@ -222,15 +231,52 @@ final class AbilityRegistrarTest extends TestCase
|
||||
unlink($log);
|
||||
}
|
||||
|
||||
public function testUntestedDiviRefusesAWritingDiviToolBeforeItRuns(): void
|
||||
public function testALockedFeatureRefusesAWritingDiviToolBeforeItRuns(): void
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0'));
|
||||
$ability = $this->ability(false);
|
||||
DiviFeatureFixtures::lock(DiviFeature::PageWrite);
|
||||
$ability = $this->ability(false, [DiviFeature::PageRead, DiviFeature::PageWrite]);
|
||||
$ability->expects('execute')->never();
|
||||
|
||||
$args = $this->diviRegistrar($ability)->argsFor($ability);
|
||||
|
||||
self::assertToolError(($args['execute_callback'])([]), 'divi_untested');
|
||||
$payload = self::assertToolError(($args['execute_callback'])([]), 'divi_untested');
|
||||
self::assertSame(['page-write'], array_column($payload['features'], 'id'));
|
||||
}
|
||||
|
||||
public function testALockedFeatureRefusesAReadingDiviToolToo(): void
|
||||
{
|
||||
DiviFeatureFixtures::lock(DiviFeature::DesignRead, ReleaseState::Incompatible);
|
||||
$ability = $this->ability(true, [DiviFeature::PageRead, DiviFeature::DesignRead]);
|
||||
$ability->expects('execute')->never();
|
||||
|
||||
$args = $this->diviRegistrar($ability)->argsFor($ability);
|
||||
|
||||
self::assertToolError(($args['execute_callback'])([]), 'divi_incompatible');
|
||||
}
|
||||
|
||||
public function testAToolRunsAndKeepsItsResultWhileOnlyOtherFeaturesAreLocked(): void
|
||||
{
|
||||
DiviFeatureFixtures::lock(DiviFeature::PresetWrite);
|
||||
$ability = $this->ability(false, [DiviFeature::PageRead, DiviFeature::PageWrite]);
|
||||
$ability->expects('execute')->once()->with(['post_id' => 3])->andReturn(['ok' => true]);
|
||||
|
||||
$args = $this->diviRegistrar($ability)->argsFor($ability);
|
||||
|
||||
self::assertSame(['ok' => true], ($args['execute_callback'])(['post_id' => 3]), 'no notice is added (E2)');
|
||||
}
|
||||
|
||||
public function testAToolThatDeclaresNoFeaturesIsNotGated(): void
|
||||
{
|
||||
DiviFeatureFixtures::useStates(array_fill_keys(
|
||||
array_map(static fn (DiviFeature $feature): string => $feature->value, DiviFeature::cases()),
|
||||
ReleaseState::Incompatible
|
||||
));
|
||||
$ability = $this->ability(false);
|
||||
$ability->expects('execute')->once()->andReturn(['ok' => true]);
|
||||
|
||||
$args = $this->diviRegistrar($ability)->argsFor($ability);
|
||||
|
||||
self::assertSame(['ok' => true], ($args['execute_callback'])([]));
|
||||
}
|
||||
|
||||
public function testALockThrownInsideAWriteBecomesTheLockError(): void
|
||||
@@ -242,6 +288,18 @@ final class AbilityRegistrarTest extends TestCase
|
||||
self::assertToolError($this->executeLogged($ability), 'divi_incompatible');
|
||||
}
|
||||
|
||||
public function testAFeatureLockThrownInsideAWriteBecomesTheFeatureError(): void
|
||||
{
|
||||
DiviFeatureFixtures::lock(DiviFeature::LibraryWrite);
|
||||
$locked = new DiviLocked(ReleaseGate::status(), [DiviFeature::LibraryWrite]);
|
||||
$ability = $this->ability();
|
||||
$ability->allows('execute')->andThrow(WriteFailed::rolledBack('Nicht gespeichert.', $locked));
|
||||
|
||||
$payload = self::assertToolError($this->executeLogged($ability), 'divi_untested');
|
||||
|
||||
self::assertSame(['library-write'], array_column($payload['features'], 'id'));
|
||||
}
|
||||
|
||||
public function testAPartialWriteStaysPartialEvenWithALockInside(): void
|
||||
{
|
||||
$locked = new DiviLocked(new ReleaseStatus(ReleaseState::Untested, '5.3.0'));
|
||||
@@ -408,13 +466,16 @@ final class AbilityRegistrarTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testDiviReadToolsGetWarningsInTheirSchema(): void
|
||||
public function testDiviToolsGetWarningsInTheirSchemaOthersDoNot(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
|
||||
$args = $this->diviRegistrar($ability)->argsFor($ability);
|
||||
$divi = $this->ability(true, [DiviFeature::PageRead]);
|
||||
$core = $this->ability();
|
||||
|
||||
$args = $this->diviRegistrar($divi)->argsFor($divi);
|
||||
self::assertArrayHasKey('warnings', $args['output_schema']['properties']);
|
||||
|
||||
$coreArgs = $this->diviRegistrar($core)->argsFor($core);
|
||||
self::assertSame(['type' => 'object'], $coreArgs['output_schema']);
|
||||
}
|
||||
|
||||
public function testRegisterAbilitiesCallsWordPressForEachAbility(): void
|
||||
|
||||
Reference in New Issue
Block a user