fix: Guards laufen auch im permission_callback vor permission() und im try des execute_callback – Lese-Tools antworten bei gesperrter Fähigkeit mit der Ablehnung statt internal_error, werfende Guards ergeben internal_error

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:21:48 +02:00
co-authored by Claude Sonnet 5.5
parent 74cf0c54a8
commit b92545a8fe
2 changed files with 141 additions and 6 deletions
@@ -393,6 +393,119 @@ final class AbilityRegistrarTest extends TestCase
self::assertSame([['x' => 1]], $seen);
}
private function throwingGuard(): AbilityGuard
{
return new class implements AbilityGuard {
public function refusal(Ability $ability, array $input): ?WP_Error
{
throw new RuntimeException('secret path /var/www/x.php');
}
public function annotate(Ability $ability, mixed $result): mixed
{
return $result;
}
public function outputSchema(Ability $ability, ?array $schema): ?array
{
return $schema;
}
};
}
/** @param callable(): mixed $call */
private function withErrorLog(callable $call): mixed
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
return $call();
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
}
public function testAGuardRefusalInThePermissionCallbackSkipsPermissionAndExecute(): void
{
Functions\when('current_user_can')->justReturn(true);
$seen = [];
$ability = $this->ability(false);
$ability->shouldNotReceive('permission');
$ability->expects('execute')->never();
$registrar = $this->guardedRegistrar($ability, [$this->guard('a', Errors::forbidden('Nein.'), $seen)]);
$result = ($registrar->argsFor($ability)['permission_callback'])(['x' => 1]);
self::assertToolError($result, 'forbidden');
self::assertSame([['x' => 1]], $seen);
}
public function testAReadToolWhosePermissionWouldThrowGetsTheGuardRefusalInstead(): void
{
Functions\when('current_user_can')->justReturn(true);
$ability = $this->ability(true);
$ability->shouldNotReceive('permission');
$registrar = $this->guardedRegistrar($ability, [$this->guard('a', Errors::forbidden('Gesperrt.'))]);
self::assertToolError(($registrar->argsFor($ability)['permission_callback'])([]), 'forbidden');
}
public function testAPassingGuardLeavesThePermissionToTheAbility(): void
{
Functions\when('current_user_can')->justReturn(true);
$ability = $this->ability();
$ability->expects('permission')->with(['x' => 1])->andReturn(true);
$registrar = $this->guardedRegistrar($ability, [$this->guard('a')]);
self::assertTrue(($registrar->argsFor($ability)['permission_callback'])(['x' => 1]));
}
public function testAThrowingGuardIsAnInternalErrorInThePermissionCallback(): void
{
Functions\when('current_user_can')->justReturn(true);
$ability = $this->ability(false);
$ability->shouldNotReceive('permission');
$ability->expects('execute')->never();
$registrar = $this->guardedRegistrar($ability, [$this->throwingGuard()]);
$result = $this->withErrorLog(
static fn (): mixed => ($registrar->argsFor($ability)['permission_callback'])([])
);
$payload = self::assertToolError($result, 'internal_error');
self::assertStringNotContainsString('/var/www', $result->get_error_message());
self::assertIsArray($payload);
}
public function testAThrowingGuardIsAnInternalErrorInTheExecuteCallbackAndNothingRuns(): void
{
$ability = $this->ability(false);
$ability->expects('execute')->never();
$registrar = $this->guardedRegistrar($ability, [$this->throwingGuard()]);
$result = $this->withErrorLog(
static fn (): mixed => ($registrar->argsFor($ability)['execute_callback'])([])
);
self::assertToolError($result, 'internal_error');
}
public function testARealFeatureGateRefusesAReadToolInThePermissionCallback(): void
{
Functions\when('current_user_can')->justReturn(true);
DiviFeatureFixtures::lock(DiviFeature::PageRead);
$ability = $this->ability(true, [DiviFeature::PageRead]);
$ability->shouldNotReceive('permission');
$ability->expects('execute')->never();
$result = ($this->diviRegistrar($ability)->argsFor($ability)['permission_callback'])(['post_id' => 7]);
$payload = self::assertToolError($result, 'divi_untested');
self::assertSame(['page-read'], array_column($payload['features'], 'id'));
}
public function testTheFirstRefusalWinsAndLaterGuardsAreNotAsked(): void
{
$seenSecond = [];