allows('id')->andReturn('core'); $integration->allows('isAvailable')->andReturn(true); $integration->allows('abilities')->andReturn([$ability]); return new AbilityRegistrar(new IntegrationRegistry($integration)); } private function ability(): Mockery\MockInterface { $ability = Mockery::mock(Ability::class); $ability->allows('name')->andReturn('lit/test'); $ability->allows('label')->andReturn('Test'); $ability->allows('description')->andReturn('Test ability'); $ability->allows('inputSchema')->andReturn(['type' => 'object', 'properties' => []]); $ability->allows('outputSchema')->andReturn(['type' => 'object']); $ability->allows('annotations')->andReturn(['readonly' => true]); $ability->allows('isDirect')->andReturn(true); return $ability; } public function testArgsExposeAbilityPubliclyToMcpWithAnnotations(): void { $args = $this->registrarFor($this->ability())->argsFor($this->ability()); self::assertSame('Test', $args['label']); self::assertSame(AbilityRegistrar::CATEGORY, $args['category']); self::assertSame(['type' => 'object'], $args['output_schema']); self::assertTrue($args['meta']['mcp']['public']); self::assertSame(['readonly' => true], $args['meta']['annotations']); self::assertFalse($args['meta']['show_in_rest']); } public function testDirectOnlyAbilitiesAreNotPublicForTheMetaTools(): void { $ability = Mockery::mock(Ability::class); $ability->allows('name')->andReturn('lit/media-download'); $ability->allows('label')->andReturn('Download'); $ability->allows('description')->andReturn('Download'); $ability->allows('inputSchema')->andReturn(['type' => 'object', 'properties' => []]); $ability->allows('outputSchema')->andReturn(null); $ability->allows('annotations')->andReturn(['readonly' => true]); $ability->allows('isDirect')->andReturn(true); $args = $this->registrarFor($ability)->argsFor($ability); self::assertFalse($args['meta']['mcp']['public']); self::assertSame('tool', $args['meta']['mcp']['type']); self::assertIsCallable($args['execute_callback']); } public function testPermissionDeniedWithoutLitCapability(): void { Functions\when('current_user_can')->justReturn(false); $ability = $this->ability(); $ability->shouldNotReceive('permission'); $result = ($this->registrarFor($ability)->argsFor($ability)['permission_callback'])([]); self::assertInstanceOf(WP_Error::class, $result); self::assertSame('lit_forbidden', $result->get_error_code()); } public function testPermissionDeniedOutsideOAuthServerRequest(): void { Functions\when('current_user_can')->justReturn(true); RequestContext::reset(false); $ability = $this->ability(); $ability->shouldNotReceive('permission'); $result = ($this->registrarFor($ability)->argsFor($ability)['permission_callback'])([]); self::assertInstanceOf(WP_Error::class, $result); self::assertSame('lit_forbidden', $result->get_error_code()); } public function testPermissionDelegatesToAbilityWhenGatePasses(): void { Functions\when('current_user_can')->justReturn(true); $ability = $this->ability(); $ability->expects('permission')->with(['x' => 1])->andReturn(true); $result = ($this->registrarFor($ability)->argsFor($ability)['permission_callback'])(['x' => 1]); self::assertTrue($result); } public function testNullInputIsPassedAsEmptyArray(): void { $ability = $this->ability(); $ability->expects('execute')->with([])->andReturn(['ok' => true]); $result = ($this->registrarFor($ability)->argsFor($ability)['execute_callback'])(null); self::assertSame(['ok' => true], $result); } public function testPermissionExceptionBecomesNeutralInternalError(): void { Functions\when('current_user_can')->justReturn(true); // error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead. $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); $ability = $this->ability(); $ability->expects('permission')->andThrow(new RuntimeException('secret path /var/www/x.php')); try { $result = ($this->registrarFor($ability)->argsFor($ability)['permission_callback'])([]); } finally { ini_set('error_log', (string) $previous); } self::assertInstanceOf(WP_Error::class, $result); self::assertSame('lit_internal_error', $result->get_error_code()); self::assertStringNotContainsString('/var/www', $result->get_error_message()); self::assertStringContainsString('lit/test failed', (string) file_get_contents($log)); unlink($log); } public function testExceptionBecomesNeutralInternalError(): void { // error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead. $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); $ability = $this->ability(); $ability->expects('execute')->andThrow(new RuntimeException('secret path /var/www/x.php')); try { $result = ($this->registrarFor($ability)->argsFor($ability)['execute_callback'])([]); } finally { ini_set('error_log', (string) $previous); } self::assertInstanceOf(WP_Error::class, $result); self::assertSame('lit_internal_error', $result->get_error_code()); self::assertStringNotContainsString('/var/www', $result->get_error_message()); self::assertStringContainsString('lit/test failed', (string) file_get_contents($log)); unlink($log); } public function testWriteFailedBecomesItsOwnErrorAndTheDetailIsLogged(): void { $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); $ability = $this->ability(); $ability->expects('execute')->andThrow( new WriteFailed('Felder konnten nicht gespeichert werden.', false, 5, 811, new RuntimeException('meta /var/www')) ); try { $result = ($this->registrarFor($ability)->argsFor($ability)['execute_callback'])([]); } finally { ini_set('error_log', (string) $previous); } self::assertSame('lit_partial_write', $result->get_error_code()); self::assertStringNotContainsString('/var/www', $result->get_error_message()); self::assertStringContainsString('meta /var/www', (string) file_get_contents($log)); unlink($log); } public function testRegisterAbilitiesCallsWordPressForEachAbility(): void { $ability = $this->ability(); Functions\expect('wp_register_ability')->once()->with('lit/test', Mockery::type('array')); $this->registrarFor($ability)->registerAbilities(); } public function testDisabledAbilitiesAreNotRegistered(): void { $ability = $this->ability(); $integration = Mockery::mock(Integration::class); $integration->allows('id')->andReturn('core'); $integration->allows('isAvailable')->andReturn(true); $integration->allows('abilities')->andReturn([$ability]); Functions\expect('wp_register_ability')->never(); (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(); } }