diff --git a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php index a64957fe73157..81297aced2571 100644 --- a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php +++ b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php @@ -43,7 +43,9 @@ * interface. As soon as any exception is caught in a chain of method calls, * the returned instance will be in an error state, and all subsequent method * calls will be no-ops that just return the same error state instance. Only - * when a generating method is called, the WP_Error will be returned. + * when a generating method is called, the WP_Error will be returned. The + * support check methods are the exception to the no-op behavior: they return + * false rather than the error state instance. * * @since 7.0.0 * @@ -79,7 +81,7 @@ * @method self as_output_media_aspect_ratio(string $aspectRatio) Sets the output media aspect ratio. * @method self as_output_speech_voice(string $voice) Sets the output speech voice. * @method self as_json_response(?array $schema = null) Configures the prompt for JSON response output. - * @method bool|WP_Error is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability. + * @method bool is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability. * @method bool is_supported_for_text_generation() Checks if the prompt is supported for text generation. * @method bool is_supported_for_image_generation() Checks if the prompt is supported for image generation. * @method bool is_supported_for_text_to_speech_conversion() Checks if the prompt is supported for text to speech conversion. @@ -282,7 +284,7 @@ public function using_abilities( ...$abilities ): self { * * This allows WordPress developers to use snake_case naming conventions. It catches * any exceptions thrown, stores them, and returns a WP_Error when a terminate method - * is called. + * is called, or false when a support check method is called. * * @since 7.0.0 * @@ -364,6 +366,9 @@ public function __call( string $name, array $arguments ) { if ( self::is_generating_method( $name ) ) { return $this->error; } + if ( self::is_support_check_method( $name ) ) { + return false; + } return $this; } } diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 0669be8cc7bb4..bec6f6fcad8e2 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -2648,6 +2648,88 @@ public function test_support_check_methods_return_false_in_error_state() { $this->assertFalse( $prompt_builder->is_supported_for_text_generation(), 'is_supported_for_text_generation should return false when in error state' ); } + /** + * Tests that support check methods return false when the wrapped builder throws. + * + * @ticket 65781 + * + * @dataProvider data_support_check_methods + * + * @param string $method The support check method on the wrapper. + * @param string $wrapped_method The matching method on the wrapped builder. + */ + public function test_support_check_methods_return_false_when_builder_throws( $method, $wrapped_method ) { + $registry = AiClient::defaultRegistry(); + $prompt_builder = new WP_AI_Client_Prompt_Builder( $registry, 'Test text' ); + + $wrapped_builder = $this->createMock( PromptBuilder::class ); + $wrapped_builder->method( $wrapped_method ) + ->willThrowException( new RuntimeException( 'Thrown by the wrapped builder.' ) ); + + $builder_property = new ReflectionProperty( WP_AI_Client_Prompt_Builder::class, 'builder' ); + self::set_accessible( $builder_property ); + $builder_property->setValue( $prompt_builder, $wrapped_builder ); + + $result = $prompt_builder->{$method}(); + + $this->assertIsBool( $result, $method . ' should return a boolean' ); + $this->assertFalse( $result, $method . ' should return false when the wrapped builder throws' ); + + // The error is still recorded, so a generating method returns it. + $error = $prompt_builder->generate_text(); + $this->assertWPError( $error, 'The caught error should still be returned by generating methods' ); + $this->assertSame( + 'prompt_builder_error', + $error->get_error_code(), + 'The recorded error should be the one thrown by the wrapped builder' + ); + } + + /** + * Data provider. + * + * @return array Support check method names, keyed by the method on the wrapper. + */ + public static function data_support_check_methods(): array { + return array( + 'is_supported' => array( 'is_supported', 'isSupported' ), + 'is_supported_for_text_generation' => array( 'is_supported_for_text_generation', 'isSupportedForTextGeneration' ), + 'is_supported_for_image_generation' => array( 'is_supported_for_image_generation', 'isSupportedForImageGeneration' ), + 'is_supported_for_text_to_speech_conversion' => array( 'is_supported_for_text_to_speech_conversion', 'isSupportedForTextToSpeechConversion' ), + 'is_supported_for_video_generation' => array( 'is_supported_for_video_generation', 'isSupportedForVideoGeneration' ), + 'is_supported_for_speech_generation' => array( 'is_supported_for_speech_generation', 'isSupportedForSpeechGeneration' ), + 'is_supported_for_music_generation' => array( 'is_supported_for_music_generation', 'isSupportedForMusicGeneration' ), + 'is_supported_for_embedding_generation' => array( 'is_supported_for_embedding_generation', 'isSupportedForEmbeddingGeneration' ), + ); + } + + /** + * Tests that a throw from the bundled client during a support check is handled. + * + * This covers the route reported on the ticket. The document modality has no + * capability to infer, so the wrapped builder throws while determining whether + * the prompt is supported. + * + * @ticket 65781 + */ + public function test_is_supported_returns_false_when_the_bundled_builder_throws() { + $registry = AiClient::defaultRegistry(); + $prompt_builder = new WP_AI_Client_Prompt_Builder( $registry, 'Test text' ); + + $result = $prompt_builder->as_output_modalities( ModalityEnum::document() )->is_supported(); + + $this->assertIsBool( $result, 'is_supported should return a boolean' ); + $this->assertFalse( $result, 'is_supported should return false when the wrapped builder throws' ); + + $error = $prompt_builder->generate_text(); + $this->assertWPError( $error, 'The caught error should still be returned by generating methods' ); + $this->assertSame( + 'prompt_builder_error', + $error->get_error_code(), + 'The document modality should still throw from the wrapped builder. If it no longer does, this test needs another route to a throw' + ); + } + /** * Tests that generating methods return WP_Error when in error state. *