From 73b28bf609e6514a945878a616aa41e82a55804c Mon Sep 17 00:00:00 2001 From: Diarmuid Mac Namara Date: Fri, 19 Jun 2026 11:57:57 +0100 Subject: [PATCH 1/5] Fix image generation support detection for non-API-key connectors has_image_generation_support() skipped connectors that authenticate without an API key (e.g. OAuth) because it only checked for API-key credentials. Also honor connectors the registry reports as configured, consistent with text generation credential detection. Add integration tests covering the API-key path, the non-API-key (configured) path, the missing-capability case, and the unauthenticated/unconfigured skip path. --- includes/helpers.php | 18 +- tests/Integration/Includes/HelpersTest.php | 282 ++++++++++++++++++++- 2 files changed, 297 insertions(+), 3 deletions(-) diff --git a/includes/helpers.php b/includes/helpers.php index 0f8314dac..fbdf388f2 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -505,13 +505,22 @@ function has_ai_credentials(): bool { /** * Checks whether any configured connector exposes an image-generation-capable model. * + * The result is memoized for the duration of the request. Pass true to recompute, + * which is useful when connector configuration changes within a single request. + * * @since 1.0.2 + * @since 1.0.3 Added the `$reset_cache` parameter. * + * @param bool $reset_cache Optional. Whether to bypass the memoized result and recompute it. Default false. * @return bool True if at least one configured connector has an image-generation-capable model. */ -function has_image_generation_support(): bool { +function has_image_generation_support( bool $reset_cache = false ): bool { static $result = null; + if ( $reset_cache ) { + $result = null; + } + if ( null !== $result ) { return $result; } @@ -525,7 +534,12 @@ function has_image_generation_support(): bool { $connectors = get_ai_connectors(); foreach ( array_keys( $connectors ) as $connector_id ) { - if ( ! has_connector_authentication( $connector_id ) ) { + // A connector qualifies when it has API-key credentials, or when its + // provider reports itself configured through the registry. The latter + // covers connectors that authenticate without an API key (e.g. OAuth), + // consistent with how text generation honors connectors that do not + // rely on API key settings. + if ( ! has_connector_authentication( $connector_id ) && ! is_connector_configured( $connector_id ) ) { continue; } diff --git a/tests/Integration/Includes/HelpersTest.php b/tests/Integration/Includes/HelpersTest.php index 14c9ccb39..3461ba6cc 100644 --- a/tests/Integration/Includes/HelpersTest.php +++ b/tests/Integration/Includes/HelpersTest.php @@ -20,6 +20,7 @@ use WordPress\AiClient\Providers\DTO\ProviderMetadata; use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; +use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; /** * Stub provider availability used by helper tests. @@ -93,6 +94,95 @@ public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface } } +/** + * Stub model metadata used by image generation support tests. + * + * @since 1.0.3 + */ +final class Image_Generation_Test_Model_Metadata { + + /** + * Whether the stub model advertises image-generation support. + * + * @since 1.0.3 + * + * @var bool + */ + public static bool $supports_image_generation = true; + + /** + * Returns the stub model's supported capabilities. + * + * @since 1.0.3 + * + * @return list Supported capabilities. + */ + // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Matches the AI client model metadata API. + public function getSupportedCapabilities(): array { + return array( + (object) array( + 'value' => self::$supports_image_generation + ? CapabilityEnum::IMAGE_GENERATION + : CapabilityEnum::TEXT_GENERATION, + ), + ); + } +} + +/** + * Stub model metadata directory used by image generation support tests. + * + * @since 1.0.3 + */ +final class Image_Generation_Test_Model_Metadata_Directory { + + /** + * Lists the stub model metadata. + * + * @since 1.0.3 + * + * @return list<\WordPress\AI\Tests\Integration\Includes\Image_Generation_Test_Model_Metadata> Stub model metadata. + */ + // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Matches the AI client model metadata directory API. + public function listModelMetadata(): array { + return array( new Image_Generation_Test_Model_Metadata() ); + } +} + +/** + * Stub provider exposing image-generation model metadata for support tests. + * + * Mirrors only the static methods that has_image_generation_support() and the AI + * client registry invoke, so it intentionally does not implement ProviderInterface. + * + * @since 1.0.3 + */ +final class Image_Generation_Test_Provider { + + /** + * Returns the stub provider availability. + * + * @since 1.0.3 + * + * @return \WordPress\AI\Tests\Integration\Includes\Helper_Test_Provider_Availability Stub availability reporting configured state. + */ + public static function availability(): Helper_Test_Provider_Availability { + return new Helper_Test_Provider_Availability(); + } + + /** + * Returns the stub model metadata directory. + * + * @since 1.0.3 + * + * @return \WordPress\AI\Tests\Integration\Includes\Image_Generation_Test_Model_Metadata_Directory Stub model metadata directory. + */ + // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Matches the AI client provider API. + public static function modelMetadataDirectory(): Image_Generation_Test_Model_Metadata_Directory { + return new Image_Generation_Test_Model_Metadata_Directory(); + } +} + /** * Helper functions test case. * @@ -111,6 +201,15 @@ class HelpersTest extends WP_UnitTestCase { */ private const TEST_AI_PROVIDER_ID = 'wpai_helper_test_provider'; + /** + * Stub provider ID used for image generation support tests. + * + * @since 1.0.3 + * + * @var string + */ + private const TEST_IMAGE_PROVIDER_ID = 'wpai_helper_test_image_provider'; + /** * Registered test connector IDs. * @@ -164,8 +263,16 @@ public function tearDown(): void { Guidelines::reset_cache(); wp_set_current_user( 0 ); delete_option( 'wpai_feature_test-feature_field_developer' ); - Helper_Test_Provider_Availability::$is_configured = false; + Helper_Test_Provider_Availability::$is_configured = false; + Image_Generation_Test_Model_Metadata::$supports_image_generation = true; $this->unregister_test_ai_provider(); + $this->unregister_test_image_provider(); + + // Recompute against the cleaned-up environment so the memoized result does + // not leak the stub state into other test cases. + if ( class_exists( AiClient::class ) ) { + \WordPress\AI\has_image_generation_support( true ); + } parent::tearDown(); } @@ -1066,6 +1173,133 @@ public function test_has_connector_authentication_detects_environment_variable() } } + /** + * Test that has_image_generation_support() detects connectors that authenticate without an API key. + * + * Regression test: connectors that authenticate without an API key (e.g. OAuth) are + * not picked up by has_connector_authentication(), so support detection must also + * honor connectors the registry reports as configured. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_detects_configured_connector_without_api_key(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'none', + ), + ) + ); + Helper_Test_Provider_Availability::$is_configured = true; + Image_Generation_Test_Model_Metadata::$supports_image_generation = true; + + $this->assertFalse( + \WordPress\AI\has_connector_authentication( self::TEST_IMAGE_PROVIDER_ID ), + 'A non-API-key connector should not report API-key authentication.' + ); + $this->assertTrue( + \WordPress\AI\has_image_generation_support( true ), + 'A configured connector with an image-generation model should be detected.' + ); + } + + /** + * Test that has_image_generation_support() still detects API-key connectors. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_detects_api_key_connector(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'api_key', + ), + ) + ); + $setting_name = 'connectors_ai_provider_' . self::TEST_IMAGE_PROVIDER_ID . '_api_key'; + update_option( $setting_name, 'test-api-key' ); + + // Force the configured state off to prove detection happens via API-key auth. + Helper_Test_Provider_Availability::$is_configured = false; + Image_Generation_Test_Model_Metadata::$supports_image_generation = true; + + try { + $this->assertTrue( \WordPress\AI\has_image_generation_support( true ) ); + } finally { + delete_option( $setting_name ); + } + } + + /** + * Test that has_image_generation_support() returns false when configured models lack the capability. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_returns_false_when_models_lack_capability(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'none', + ), + ) + ); + Helper_Test_Provider_Availability::$is_configured = true; + Image_Generation_Test_Model_Metadata::$supports_image_generation = false; + + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + } + + /** + * Test that has_image_generation_support() skips connectors without authentication or configuration. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_skips_unauthenticated_unconfigured_connector(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'none', + ), + ) + ); + Helper_Test_Provider_Availability::$is_configured = false; + Image_Generation_Test_Model_Metadata::$supports_image_generation = true; + + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + } + /** * Test that connector plugin metadata is optional. * @@ -1225,6 +1459,52 @@ private function unregister_test_ai_provider(): void { $classes_to_ids->setValue( $registry, $class_map ); } + /** + * Registers the image generation stub provider in the AI client registry. + * + * @since 1.0.3 + */ + private function register_test_image_provider(): void { + $registry = AiClient::defaultRegistry(); + + $ids_to_classes = new ReflectionProperty( $registry, 'registeredIdsToClassNames' ); + $ids_to_classes->setAccessible( true ); + $id_map = (array) $ids_to_classes->getValue( $registry ); + $id_map[ self::TEST_IMAGE_PROVIDER_ID ] = Image_Generation_Test_Provider::class; + $ids_to_classes->setValue( $registry, $id_map ); + + $classes_to_ids = new ReflectionProperty( $registry, 'registeredClassNamesToIds' ); + $classes_to_ids->setAccessible( true ); + $class_map = (array) $classes_to_ids->getValue( $registry ); + $class_map[ Image_Generation_Test_Provider::class ] = self::TEST_IMAGE_PROVIDER_ID; + $classes_to_ids->setValue( $registry, $class_map ); + } + + /** + * Unregisters the image generation stub provider from the AI client registry. + * + * @since 1.0.3 + */ + private function unregister_test_image_provider(): void { + if ( ! class_exists( AiClient::class ) ) { + return; + } + + $registry = AiClient::defaultRegistry(); + + $ids_to_classes = new ReflectionProperty( $registry, 'registeredIdsToClassNames' ); + $ids_to_classes->setAccessible( true ); + $id_map = (array) $ids_to_classes->getValue( $registry ); + unset( $id_map[ self::TEST_IMAGE_PROVIDER_ID ] ); + $ids_to_classes->setValue( $registry, $id_map ); + + $classes_to_ids = new ReflectionProperty( $registry, 'registeredClassNamesToIds' ); + $classes_to_ids->setAccessible( true ); + $class_map = (array) $classes_to_ids->getValue( $registry ); + unset( $class_map[ Image_Generation_Test_Provider::class ] ); + $classes_to_ids->setValue( $registry, $class_map ); + } + /** * Marks a plugin basename as active for the current test. * From a11055f334b4df3c2aa3755d5091f8383f87942b Mon Sep 17 00:00:00 2001 From: Diarmuid Mac Namara Date: Tue, 23 Jun 2026 09:07:00 +0100 Subject: [PATCH 2/5] Detect image generation support via filter instead of is_connector_configured Addresses review feedback: is_connector_configured() calls isProviderConfigured(), which issues a live API request, and has_image_generation_support() runs on nearly every admin page load. Replace it with a request-free wpai_has_image_generation_support filter so connectors that authenticate without an API key (e.g. OAuth) can advertise support, mirroring the existing wpai_has_ai_credentials pattern. Update tests to cover advertising and suppressing support through the filter. --- includes/helpers.php | 80 +++++++++++---------- tests/Integration/Includes/HelpersTest.php | 84 +++++++++++++++++----- 2 files changed, 111 insertions(+), 53 deletions(-) diff --git a/includes/helpers.php b/includes/helpers.php index fbdf388f2..c22080b61 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -505,64 +505,70 @@ function has_ai_credentials(): bool { /** * Checks whether any configured connector exposes an image-generation-capable model. * - * The result is memoized for the duration of the request. Pass true to recompute, - * which is useful when connector configuration changes within a single request. + * Only connectors with API-key credentials are inspected, and the detection + * never issues a live API request, so it is safe to call on every admin page + * load. Connectors that authenticate without an API key (e.g. OAuth) can + * advertise support through the {@see 'wpai_has_image_generation_support'} + * filter. * * @since 1.0.2 - * @since 1.0.3 Added the `$reset_cache` parameter. * - * @param bool $reset_cache Optional. Whether to bypass the memoized result and recompute it. Default false. - * @return bool True if at least one configured connector has an image-generation-capable model. + * @param bool $reset_cache Whether to bypass the static cache and recompute. Default false. + * @return bool True if at least one connector supports image generation. */ function has_image_generation_support( bool $reset_cache = false ): bool { static $result = null; - if ( $reset_cache ) { - $result = null; - } - - if ( null !== $result ) { + if ( ! $reset_cache && null !== $result ) { return $result; } - if ( ! class_exists( AiClient::class ) ) { - $result = false; - return $result; - } + $connectors = array(); + $has_support = false; - $registry = AiClient::defaultRegistry(); - $connectors = get_ai_connectors(); + if ( class_exists( AiClient::class ) ) { + $registry = AiClient::defaultRegistry(); + $connectors = get_ai_connectors(); - foreach ( array_keys( $connectors ) as $connector_id ) { - // A connector qualifies when it has API-key credentials, or when its - // provider reports itself configured through the registry. The latter - // covers connectors that authenticate without an API key (e.g. OAuth), - // consistent with how text generation honors connectors that do not - // rely on API key settings. - if ( ! has_connector_authentication( $connector_id ) && ! is_connector_configured( $connector_id ) ) { - continue; - } + foreach ( array_keys( $connectors ) as $connector_id ) { + if ( ! has_connector_authentication( $connector_id ) ) { + continue; + } - try { - $provider_class = $registry->getProviderClassName( $connector_id ); + try { + $provider_class = $registry->getProviderClassName( $connector_id ); - /** @var \WordPress\AiClient\Providers\Contracts\ProviderInterface $provider_class */ - $models = $provider_class::modelMetadataDirectory()->listModelMetadata(); + /** @var \WordPress\AiClient\Providers\Contracts\ProviderInterface $provider_class */ + $models = $provider_class::modelMetadataDirectory()->listModelMetadata(); - foreach ( $models as $model ) { - foreach ( $model->getSupportedCapabilities() as $capability ) { - if ( CapabilityEnum::IMAGE_GENERATION === $capability->value ) { - $result = true; - return $result; + foreach ( $models as $model ) { + foreach ( $model->getSupportedCapabilities() as $capability ) { + if ( CapabilityEnum::IMAGE_GENERATION === $capability->value ) { + $has_support = true; + break 3; + } } } + } catch ( Throwable $e ) { + continue; } - } catch ( Throwable $e ) { - continue; } } - $result = false; + /** + * Filters whether image generation is supported. + * + * Allows third-party plugins to declare image generation support for + * connectors that do not rely on API key settings (e.g. OAuth), without + * triggering a live API request. + * + * @since 1.0.3 + * + * @param bool $has_support Whether image generation is supported. + * @param array $connectors The registered connectors. + */ + $result = (bool) apply_filters( 'wpai_has_image_generation_support', $has_support, $connectors ); + return $result; } diff --git a/tests/Integration/Includes/HelpersTest.php b/tests/Integration/Includes/HelpersTest.php index 3461ba6cc..ad0717496 100644 --- a/tests/Integration/Includes/HelpersTest.php +++ b/tests/Integration/Includes/HelpersTest.php @@ -1174,15 +1174,15 @@ public function test_has_connector_authentication_detects_environment_variable() } /** - * Test that has_image_generation_support() detects connectors that authenticate without an API key. + * Test that a connector can advertise image generation support through the filter. * * Regression test: connectors that authenticate without an API key (e.g. OAuth) are - * not picked up by has_connector_authentication(), so support detection must also - * honor connectors the registry reports as configured. + * not picked up by has_connector_authentication(), so they advertise support through + * the wpai_has_image_generation_support filter, which is request-free. * * @since 1.0.3 */ - public function test_has_image_generation_support_detects_configured_connector_without_api_key(): void { + public function test_has_image_generation_support_detects_connector_via_filter(): void { if ( ! class_exists( AiClient::class ) ) { $this->markTestSkipped( 'AiClient not available.' ); } @@ -1198,17 +1198,27 @@ public function test_has_image_generation_support_detects_configured_connector_w ), ) ); - Helper_Test_Provider_Availability::$is_configured = true; Image_Generation_Test_Model_Metadata::$supports_image_generation = true; $this->assertFalse( \WordPress\AI\has_connector_authentication( self::TEST_IMAGE_PROVIDER_ID ), 'A non-API-key connector should not report API-key authentication.' ); - $this->assertTrue( + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ), - 'A configured connector with an image-generation model should be detected.' + 'A non-API-key connector is not detected until it advertises support.' ); + + add_filter( 'wpai_has_image_generation_support', '__return_true' ); + + try { + $this->assertTrue( + \WordPress\AI\has_image_generation_support( true ), + 'A connector advertising support through the filter should be detected.' + ); + } finally { + remove_filter( 'wpai_has_image_generation_support', '__return_true' ); + } } /** @@ -1235,8 +1245,6 @@ public function test_has_image_generation_support_detects_api_key_connector(): v $setting_name = 'connectors_ai_provider_' . self::TEST_IMAGE_PROVIDER_ID . '_api_key'; update_option( $setting_name, 'test-api-key' ); - // Force the configured state off to prove detection happens via API-key auth. - Helper_Test_Provider_Availability::$is_configured = false; Image_Generation_Test_Model_Metadata::$supports_image_generation = true; try { @@ -1247,7 +1255,7 @@ public function test_has_image_generation_support_detects_api_key_connector(): v } /** - * Test that has_image_generation_support() returns false when configured models lack the capability. + * Test that has_image_generation_support() returns false when a connector's models lack the capability. * * @since 1.0.3 */ @@ -1263,22 +1271,31 @@ public function test_has_image_generation_support_returns_false_when_models_lack 'name' => 'Helper Test Image Provider', 'type' => 'ai_provider', 'authentication' => array( - 'method' => 'none', + 'method' => 'api_key', ), ) ); - Helper_Test_Provider_Availability::$is_configured = true; + $setting_name = 'connectors_ai_provider_' . self::TEST_IMAGE_PROVIDER_ID . '_api_key'; + update_option( $setting_name, 'test-api-key' ); + Image_Generation_Test_Model_Metadata::$supports_image_generation = false; - $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + try { + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + } finally { + delete_option( $setting_name ); + } } /** - * Test that has_image_generation_support() skips connectors without authentication or configuration. + * Test that has_image_generation_support() skips connectors without credentials. + * + * A non-API-key connector that does not advertise support through the + * wpai_has_image_generation_support filter must not be detected. * * @since 1.0.3 */ - public function test_has_image_generation_support_skips_unauthenticated_unconfigured_connector(): void { + public function test_has_image_generation_support_skips_connector_without_credentials(): void { if ( ! class_exists( AiClient::class ) ) { $this->markTestSkipped( 'AiClient not available.' ); } @@ -1294,12 +1311,47 @@ public function test_has_image_generation_support_skips_unauthenticated_unconfig ), ) ); - Helper_Test_Provider_Availability::$is_configured = false; Image_Generation_Test_Model_Metadata::$supports_image_generation = true; $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); } + /** + * Test that the filter can suppress support for an otherwise-qualifying connector. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_filter_can_suppress(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'api_key', + ), + ) + ); + $setting_name = 'connectors_ai_provider_' . self::TEST_IMAGE_PROVIDER_ID . '_api_key'; + update_option( $setting_name, 'test-api-key' ); + + Image_Generation_Test_Model_Metadata::$supports_image_generation = true; + + add_filter( 'wpai_has_image_generation_support', '__return_false' ); + + try { + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + } finally { + remove_filter( 'wpai_has_image_generation_support', '__return_false' ); + delete_option( $setting_name ); + } + } + /** * Test that connector plugin metadata is optional. * From 60d955c93569ce9ce79779a6fca0678194c899d4 Mon Sep 17 00:00:00 2001 From: Diarmuid Mac Namara Date: Tue, 23 Jun 2026 09:08:58 +0100 Subject: [PATCH 3/5] Remove implementation detail from has_image_generation_support docblock --- includes/helpers.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/includes/helpers.php b/includes/helpers.php index c22080b61..5fbd8db75 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -505,12 +505,6 @@ function has_ai_credentials(): bool { /** * Checks whether any configured connector exposes an image-generation-capable model. * - * Only connectors with API-key credentials are inspected, and the detection - * never issues a live API request, so it is safe to call on every admin page - * load. Connectors that authenticate without an API key (e.g. OAuth) can - * advertise support through the {@see 'wpai_has_image_generation_support'} - * filter. - * * @since 1.0.2 * * @param bool $reset_cache Whether to bypass the static cache and recompute. Default false. From 41ea64b0bd5c267b5224eb857089972861dfda99 Mon Sep 17 00:00:00 2001 From: Diarmuid Mac Namara Date: Tue, 23 Jun 2026 09:21:00 +0100 Subject: [PATCH 4/5] Cover memoization and provider-failure paths in has_image_generation_support tests Adds tests for the two patch lines Codecov flagged as uncovered: the memoized cache-hit return path, and the catch/continue guard when a connector's provider throws while listing model metadata. --- tests/Integration/Includes/HelpersTest.php | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Integration/Includes/HelpersTest.php b/tests/Integration/Includes/HelpersTest.php index ad0717496..d727c1eaa 100644 --- a/tests/Integration/Includes/HelpersTest.php +++ b/tests/Integration/Includes/HelpersTest.php @@ -136,15 +136,30 @@ public function getSupportedCapabilities(): array { */ final class Image_Generation_Test_Model_Metadata_Directory { + /** + * Whether listing model metadata should throw to simulate a provider failure. + * + * @since 1.0.3 + * + * @var bool + */ + public static bool $should_throw = false; + /** * Lists the stub model metadata. * * @since 1.0.3 * + * @throws \RuntimeException When $should_throw is set, to exercise the support-detection guard. + * * @return list<\WordPress\AI\Tests\Integration\Includes\Image_Generation_Test_Model_Metadata> Stub model metadata. */ // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Matches the AI client model metadata directory API. public function listModelMetadata(): array { + if ( self::$should_throw ) { + throw new \RuntimeException( 'Simulated provider failure.' ); + } + return array( new Image_Generation_Test_Model_Metadata() ); } } @@ -265,6 +280,7 @@ public function tearDown(): void { delete_option( 'wpai_feature_test-feature_field_developer' ); Helper_Test_Provider_Availability::$is_configured = false; Image_Generation_Test_Model_Metadata::$supports_image_generation = true; + Image_Generation_Test_Model_Metadata_Directory::$should_throw = false; $this->unregister_test_ai_provider(); $this->unregister_test_image_provider(); @@ -1352,6 +1368,65 @@ public function test_has_image_generation_support_filter_can_suppress(): void { } } + /** + * Test that has_image_generation_support() memoizes its result until the cache is reset. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_memoizes_result(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + add_filter( 'wpai_has_image_generation_support', '__return_true' ); + + try { + $computed = \WordPress\AI\has_image_generation_support( true ); + remove_filter( 'wpai_has_image_generation_support', '__return_true' ); + + // Without a cache reset the memoized result is returned, even though the + // filter that produced it has since been removed. + $this->assertTrue( $computed ); + $this->assertSame( $computed, \WordPress\AI\has_image_generation_support() ); + } finally { + remove_filter( 'wpai_has_image_generation_support', '__return_true' ); + } + } + + /** + * Test that has_image_generation_support() skips a connector whose provider throws. + * + * @since 1.0.3 + */ + public function test_has_image_generation_support_skips_connector_that_throws(): void { + if ( ! class_exists( AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $this->register_test_image_provider(); + $this->register_test_connector( + self::TEST_IMAGE_PROVIDER_ID, + array( + 'name' => 'Helper Test Image Provider', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'api_key', + ), + ) + ); + $setting_name = 'connectors_ai_provider_' . self::TEST_IMAGE_PROVIDER_ID . '_api_key'; + update_option( $setting_name, 'test-api-key' ); + + Image_Generation_Test_Model_Metadata_Directory::$should_throw = true; + + try { + $this->assertFalse( \WordPress\AI\has_image_generation_support( true ) ); + } finally { + Image_Generation_Test_Model_Metadata_Directory::$should_throw = false; + delete_option( $setting_name ); + } + } + /** * Test that connector plugin metadata is optional. * From 3fbd4050041ce6708dfe1ffb569b1bf19688bbdf Mon Sep 17 00:00:00 2001 From: Diarmuid Mac Namara Date: Wed, 24 Jun 2026 16:38:36 +0100 Subject: [PATCH 5/5] Set new @since tags to x.x.x placeholder Addresses review comment on PR #748: the @since tags introduced by this change, in both includes/helpers.php and the integration test file, are set to the x.x.x placeholder rather than a concrete version, to be filled in at release time. --- includes/helpers.php | 2 +- tests/Integration/Includes/HelpersTest.php | 38 +++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/includes/helpers.php b/includes/helpers.php index 5fbd8db75..835d08367 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -556,7 +556,7 @@ function has_image_generation_support( bool $reset_cache = false ): bool { * connectors that do not rely on API key settings (e.g. OAuth), without * triggering a live API request. * - * @since 1.0.3 + * @since x.x.x * * @param bool $has_support Whether image generation is supported. * @param array $connectors The registered connectors. diff --git a/tests/Integration/Includes/HelpersTest.php b/tests/Integration/Includes/HelpersTest.php index d727c1eaa..380390c8d 100644 --- a/tests/Integration/Includes/HelpersTest.php +++ b/tests/Integration/Includes/HelpersTest.php @@ -97,14 +97,14 @@ public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface /** * Stub model metadata used by image generation support tests. * - * @since 1.0.3 + * @since x.x.x */ final class Image_Generation_Test_Model_Metadata { /** * Whether the stub model advertises image-generation support. * - * @since 1.0.3 + * @since x.x.x * * @var bool */ @@ -113,7 +113,7 @@ final class Image_Generation_Test_Model_Metadata { /** * Returns the stub model's supported capabilities. * - * @since 1.0.3 + * @since x.x.x * * @return list Supported capabilities. */ @@ -132,14 +132,14 @@ public function getSupportedCapabilities(): array { /** * Stub model metadata directory used by image generation support tests. * - * @since 1.0.3 + * @since x.x.x */ final class Image_Generation_Test_Model_Metadata_Directory { /** * Whether listing model metadata should throw to simulate a provider failure. * - * @since 1.0.3 + * @since x.x.x * * @var bool */ @@ -148,7 +148,7 @@ final class Image_Generation_Test_Model_Metadata_Directory { /** * Lists the stub model metadata. * - * @since 1.0.3 + * @since x.x.x * * @throws \RuntimeException When $should_throw is set, to exercise the support-detection guard. * @@ -170,14 +170,14 @@ public function listModelMetadata(): array { * Mirrors only the static methods that has_image_generation_support() and the AI * client registry invoke, so it intentionally does not implement ProviderInterface. * - * @since 1.0.3 + * @since x.x.x */ final class Image_Generation_Test_Provider { /** * Returns the stub provider availability. * - * @since 1.0.3 + * @since x.x.x * * @return \WordPress\AI\Tests\Integration\Includes\Helper_Test_Provider_Availability Stub availability reporting configured state. */ @@ -188,7 +188,7 @@ public static function availability(): Helper_Test_Provider_Availability { /** * Returns the stub model metadata directory. * - * @since 1.0.3 + * @since x.x.x * * @return \WordPress\AI\Tests\Integration\Includes\Image_Generation_Test_Model_Metadata_Directory Stub model metadata directory. */ @@ -219,7 +219,7 @@ class HelpersTest extends WP_UnitTestCase { /** * Stub provider ID used for image generation support tests. * - * @since 1.0.3 + * @since x.x.x * * @var string */ @@ -1196,7 +1196,7 @@ public function test_has_connector_authentication_detects_environment_variable() * not picked up by has_connector_authentication(), so they advertise support through * the wpai_has_image_generation_support filter, which is request-free. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_detects_connector_via_filter(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1240,7 +1240,7 @@ public function test_has_image_generation_support_detects_connector_via_filter() /** * Test that has_image_generation_support() still detects API-key connectors. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_detects_api_key_connector(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1273,7 +1273,7 @@ public function test_has_image_generation_support_detects_api_key_connector(): v /** * Test that has_image_generation_support() returns false when a connector's models lack the capability. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_returns_false_when_models_lack_capability(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1309,7 +1309,7 @@ public function test_has_image_generation_support_returns_false_when_models_lack * A non-API-key connector that does not advertise support through the * wpai_has_image_generation_support filter must not be detected. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_skips_connector_without_credentials(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1335,7 +1335,7 @@ public function test_has_image_generation_support_skips_connector_without_creden /** * Test that the filter can suppress support for an otherwise-qualifying connector. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_filter_can_suppress(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1371,7 +1371,7 @@ public function test_has_image_generation_support_filter_can_suppress(): void { /** * Test that has_image_generation_support() memoizes its result until the cache is reset. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_memoizes_result(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1396,7 +1396,7 @@ public function test_has_image_generation_support_memoizes_result(): void { /** * Test that has_image_generation_support() skips a connector whose provider throws. * - * @since 1.0.3 + * @since x.x.x */ public function test_has_image_generation_support_skips_connector_that_throws(): void { if ( ! class_exists( AiClient::class ) ) { @@ -1589,7 +1589,7 @@ private function unregister_test_ai_provider(): void { /** * Registers the image generation stub provider in the AI client registry. * - * @since 1.0.3 + * @since x.x.x */ private function register_test_image_provider(): void { $registry = AiClient::defaultRegistry(); @@ -1610,7 +1610,7 @@ private function register_test_image_provider(): void { /** * Unregisters the image generation stub provider from the AI client registry. * - * @since 1.0.3 + * @since x.x.x */ private function unregister_test_image_provider(): void { if ( ! class_exists( AiClient::class ) ) {