From 9a3ef46dda449fcb513c20c0e84a102cfdbb0e9a Mon Sep 17 00:00:00 2001 From: Jignesh Bhavani Date: Sat, 1 Aug 2026 18:41:48 +0530 Subject: [PATCH 1/3] AI: Return false from support checks when the wrapped builder throws. WP_AI_Client_Prompt_Builder::__call() only special-cased generating methods when converting a caught exception into the error state, so is_supported() and its siblings fell through to the fluent return and handed back the builder instance. That instance is truthy, so a failed support check read as supported. Return false from support check methods instead, matching what they already return when the builder is in a pre-existing error state. --- .../class-wp-ai-client-prompt-builder.php | 3 +++ .../ai-client/wpAiClientPromptBuilder.php | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) 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..8e2c04ba05be3 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 @@ -364,6 +364,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..bfec952718396 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -2648,6 +2648,28 @@ 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 + */ + public function test_support_check_methods_return_false_when_builder_throws() { + $registry = AiClient::defaultRegistry(); + $prompt_builder = new WP_AI_Client_Prompt_Builder( $registry, 'Test text' ); + + /* + * The document modality has no capability to infer, so the wrapped builder + * throws while determining whether the prompt is supported. + */ + $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' ); + + // The error is still recorded, so a generating method returns it. + $this->assertWPError( $prompt_builder->generate_text(), 'The caught error should still be returned by generating methods' ); + } + /** * Tests that generating methods return WP_Error when in error state. * From 88c2bf38e1999f014b3362dc388e871ce53b4d26 Mon Sep 17 00:00:00 2001 From: Jignesh Bhavani Date: Wed, 12 Aug 2026 16:51:48 +0530 Subject: [PATCH 2/3] Tests: Cover every support check method with a mocked throw. The regression test relied on the document output modality throwing from the bundled client, which only reaches the bare is_supported() call. The seven is_supported_for_*() methods pass an explicit capability, so they never run the inference that throws and were not covered at all. Drive the throw from a mocked PromptBuilder instead and loop over all eight methods, and keep the document modality case as the route reported on the ticket, pinned to the error code so it fails if the bundled client stops throwing. See #65781. --- .../ai-client/wpAiClientPromptBuilder.php | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index bfec952718396..bec6f6fcad8e2 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -2652,22 +2652,82 @@ public function test_support_check_methods_return_false_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 function test_support_check_methods_return_false_when_builder_throws() { + 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' ); - /* - * The document modality has no capability to infer, so the wrapped builder - * throws while determining whether the prompt is supported. - */ $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' ); - // The error is still recorded, so a generating method returns it. - $this->assertWPError( $prompt_builder->generate_text(), 'The caught error should still be returned by generating methods' ); + $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' + ); } /** From 69fc18ce431e9d084dce0882a34a91b45e6613c8 Mon Sep 17 00:00:00 2001 From: Jignesh Bhavani Date: Thu, 13 Aug 2026 17:03:38 +0530 Subject: [PATCH 3/3] Docs: Correct the documented return of the support check methods. `is_supported()` is annotated `bool|WP_Error` but no path returns a `WP_Error`. Drop the `WP_Error`, and note in the `__call()` and class docblocks that support checks return false rather than the error state instance. See #65781. --- .../ai-client/class-wp-ai-client-prompt-builder.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 8e2c04ba05be3..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 *