diff --git a/projects/packages/jetpack-mu-wpcom/changelog/forno-217-update-image-studio-activation-conditions b/projects/packages/jetpack-mu-wpcom/changelog/forno-217-update-image-studio-activation-conditions new file mode 100644 index 00000000000..76bc6f5b11a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/forno-217-update-image-studio-activation-conditions @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Agents Manager: Allow plugins to enable the agents manager unified experience via filter. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/agents-manager/class-agents-manager.php b/projects/packages/jetpack-mu-wpcom/src/features/agents-manager/class-agents-manager.php index da079b10a22..9fe4f107b34 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/agents-manager/class-agents-manager.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/agents-manager/class-agents-manager.php @@ -566,9 +566,10 @@ public function register_rest_api() { /** * Determine if user should see unified experience. * + * @param bool $use_unified_experience Whether to use unified experience. * @return bool */ - public function should_use_unified_experience() { + public function should_use_unified_experience( $use_unified_experience = false ) { // Early return for non-proxied/dev mode requests. // This feature is currently only available to Automattic employees testing via proxy. if ( ! self::is_dev_mode() ) { @@ -598,9 +599,9 @@ public function should_use_unified_experience() { return true; } - // False, for now. + // Default to false, for now. // In the future: users with a big sky site (similar to https://github.a8c.com/Automattic/wpcom/pull/196449/files), a big-sky free trial or a paid plan. - return false; + return $use_unified_experience; } /** diff --git a/projects/plugins/jetpack/changelog/forno-217-update-image-studio-activation-conditions b/projects/plugins/jetpack/changelog/forno-217-update-image-studio-activation-conditions new file mode 100644 index 00000000000..4eab38422ec --- /dev/null +++ b/projects/plugins/jetpack/changelog/forno-217-update-image-studio-activation-conditions @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Image Studio: Only enable when AI features are available diff --git a/projects/plugins/jetpack/extensions/plugins/image-studio/image-studio.php b/projects/plugins/jetpack/extensions/plugins/image-studio/image-studio.php index 8430c2985d8..03c269bad03 100644 --- a/projects/plugins/jetpack/extensions/plugins/image-studio/image-studio.php +++ b/projects/plugins/jetpack/extensions/plugins/image-studio/image-studio.php @@ -7,6 +7,10 @@ namespace Automattic\Jetpack\Extensions\ImageStudio; +use Automattic\Jetpack\Connection\Manager as Connection_Manager; +use Automattic\Jetpack\Status; +use Automattic\Jetpack\Status\Host; + if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } @@ -25,16 +29,43 @@ /** * Check if Image Studio is enabled. * - * Returns true if either the unified chat experience or the - * jetpack_image_studio_enabled filter is active. + * Requires AI features (Big Sky or AI Assistant) plus at least one of: + * - The unified chat experience (agents_manager_use_unified_experience). + * - The jetpack_image_studio_enabled filter. * * @return bool */ function is_image_studio_enabled() { + if ( ! has_ai_features() ) { + return false; + } + return apply_filters( 'agents_manager_use_unified_experience', false ) || apply_filters( 'jetpack_image_studio_enabled', false ); } +/** + * Check whether AI features are available. + * + * - wpcom simple: always available. + * - Atomic: requires Big Sky or AI Assistant feature flags. + * - Self-hosted: requires a connected owner with AI not disabled + * (same conditions the AI Assistant plugin uses to register). + * + * @return bool + */ +function has_ai_features() { + $host = new Host(); + + if ( $host->is_wpcom_simple() ) { + return true; + } + + return ( new Connection_Manager( 'jetpack' ) )->has_connected_owner() + && ! ( new Status() )->is_offline_mode() + && apply_filters( 'jetpack_ai_enabled', true ); +} + /** * Check if the current screen is a block editor (Post Editor or Site Editor). * @@ -332,7 +363,7 @@ function disable_jetpack_ai_image_extensions() { /** * Enable the agents manager unified experience on self-hosted sites - * when jetpack_image_studio_enabled is true. + * when Image Studio is enabled with AI capabilities. * * This ensures the agents manager loads and can host the headless agent * even when the unified chat experience is not otherwise enabled. @@ -345,7 +376,7 @@ function enable_agents_manager_for_image_studio( $use_unified_experience ) { return true; } - return (bool) apply_filters( 'jetpack_image_studio_enabled', false ); + return has_ai_features() && (bool) apply_filters( 'jetpack_image_studio_enabled', false ); } add_filter( 'agents_manager_use_unified_experience', __NAMESPACE__ . '\enable_agents_manager_for_image_studio' ); diff --git a/projects/plugins/jetpack/tests/php/extensions/plugins/image-studio/Image_Studio_Test.php b/projects/plugins/jetpack/tests/php/extensions/plugins/image-studio/Image_Studio_Test.php index e144f848b85..3b72b1d574b 100644 --- a/projects/plugins/jetpack/tests/php/extensions/plugins/image-studio/Image_Studio_Test.php +++ b/projects/plugins/jetpack/tests/php/extensions/plugins/image-studio/Image_Studio_Test.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\Attributes\DataProvider; require_once JETPACK__PLUGIN_DIR . '/extensions/plugins/image-studio/image-studio.php'; +require_once JETPACK__PLUGIN_DIR . '/extensions/plugins/ai-assistant-plugin/ai-assistant-plugin.php'; /** * Image Studio extension tests. @@ -59,6 +60,7 @@ public function set_up() { $GLOBALS['wp_scripts'] = new WP_Scripts(); $GLOBALS['wp_styles'] = new WP_Styles(); $this->reset_availability(); + $this->simulate_connected_owner(); unset( $_GET['enable_image_studio'] ); $this->saved_screen = $GLOBALS['current_screen'] ?? null; } @@ -73,6 +75,8 @@ public function tear_down() { remove_all_filters( 'agents_manager_agent_providers' ); remove_all_filters( 'pre_http_request' ); remove_all_filters( 'locale' ); + remove_all_filters( 'jetpack_ai_enabled' ); + ( new \Automattic\Jetpack\Connection\Manager( 'jetpack' ) )->reset_connection_status(); unset( $_GET['enable_image_studio'] ); $GLOBALS['current_screen'] = $this->saved_screen; $GLOBALS['wp_scripts'] = $this->saved_wp_scripts; @@ -90,6 +94,19 @@ private function reset_availability() { $property->setValue( null, array() ); } + /** + * Simulate a connected Jetpack owner so has_ai_features() returns true. + * + * Called in set_up() so every test starts with AI features available. + * Tests that need AI features off should use disable_ai_features() instead. + */ + private function simulate_connected_owner() { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + \Jetpack_Options::update_option( 'master_user', $user_id ); + \Jetpack_Options::update_option( 'user_tokens', array( $user_id => 'token.secret.' . $user_id ) ); + ( new \Automattic\Jetpack\Connection\Manager( 'jetpack' ) )->reset_connection_status(); + } + /** * Enable Image Studio via jetpack_image_studio_enabled filter. */ @@ -97,6 +114,13 @@ private function enable_image_studio() { add_filter( 'jetpack_image_studio_enabled', '__return_true' ); } + /** + * Disable AI features via the jetpack_ai_enabled kill switch. + */ + private function disable_ai_features() { + add_filter( 'jetpack_ai_enabled', '__return_false' ); + } + /** * Disable Image Studio via filter. */ @@ -223,12 +247,31 @@ function () use ( $status_code, $body, $content_type ) { ); } + // ------------------------------------------------------------------------- + // has_ai_features() tests + // ------------------------------------------------------------------------- + + /** + * AI features available by default in the test environment. + */ + public function test_has_ai_features_true_by_default() { + $this->assertTrue( ImageStudio\has_ai_features() ); + } + + /** + * AI features disabled via jetpack_ai_enabled kill switch. + */ + public function test_has_ai_features_false_when_ai_disabled() { + $this->disable_ai_features(); + $this->assertFalse( ImageStudio\has_ai_features() ); + } + // ------------------------------------------------------------------------- // is_image_studio_enabled() tests // ------------------------------------------------------------------------- /** - * Test is_image_studio_enabled returns true when jetpack_image_studio_enabled is true. + * Enabled when jetpack_image_studio_enabled filter is true and AI features exist. */ public function test_is_enabled_via_jetpack_filter() { $this->enable_image_studio(); @@ -236,7 +279,7 @@ public function test_is_enabled_via_jetpack_filter() { } /** - * Test is_image_studio_enabled returns true when unified experience is true. + * Enabled when unified experience is true and AI features exist. */ public function test_is_enabled_via_unified_experience() { $this->enable_unified_experience(); @@ -244,14 +287,14 @@ public function test_is_enabled_via_unified_experience() { } /** - * Test is_image_studio_enabled returns false when both filters are false. + * Not enabled when neither filter is set. */ public function test_is_not_enabled_when_both_filters_false() { $this->assertFalse( ImageStudio\is_image_studio_enabled() ); } /** - * Test is_image_studio_enabled returns true when both filters are true. + * Enabled when both filters are true. */ public function test_is_enabled_when_both_filters_true() { $this->enable_image_studio(); @@ -259,6 +302,39 @@ public function test_is_enabled_when_both_filters_true() { $this->assertTrue( ImageStudio\is_image_studio_enabled() ); } + /** + * AI features alone aren't enough; a filter must also be set. + */ + public function test_is_not_enabled_with_ai_features_but_no_filter() { + $this->assertFalse( ImageStudio\is_image_studio_enabled() ); + } + + /** + * Enabled via filter with AI features available. + */ + public function test_is_enabled_via_filter_with_ai_features() { + add_filter( 'jetpack_image_studio_enabled', '__return_true' ); + $this->assertTrue( ImageStudio\is_image_studio_enabled() ); + } + + /** + * Unified experience alone isn't enough; AI features must also exist. + */ + public function test_is_not_enabled_via_unified_experience_without_ai_features() { + $this->enable_unified_experience(); + $this->disable_ai_features(); + $this->assertFalse( ImageStudio\is_image_studio_enabled() ); + } + + /** + * Filter alone isn't enough; AI features must also exist. + */ + public function test_is_not_enabled_via_filter_without_ai_features() { + add_filter( 'jetpack_image_studio_enabled', '__return_true' ); + $this->disable_ai_features(); + $this->assertFalse( ImageStudio\is_image_studio_enabled() ); + } + // ------------------------------------------------------------------------- // is_block_editor() tests // ------------------------------------------------------------------------- @@ -1460,4 +1536,28 @@ public function test_js_url_has_min_css_urls_do_not() { public function test_asset_transient_constant() { $this->assertEquals( 'jetpack_image_studio_asset', ImageStudio\ASSET_TRANSIENT ); } + + // ------------------------------------------------------------------------- + // Hook priority tests + // ------------------------------------------------------------------------- + + /** + * Disable_jetpack_ai_image_extensions must run after AI extensions register. + */ + public function test_disable_ai_extensions_priority_after_ai_assistant() { + $hook = 'jetpack_register_gutenberg_extensions'; + + $jp_ai_priority = has_action( + $hook, + 'Automattic\Jetpack\Extensions\AiAssistantPlugin\register_plugin' + ); + $disable_priority = has_action( + $hook, + 'Automattic\Jetpack\Extensions\ImageStudio\disable_jetpack_ai_image_extensions' + ); + + $this->assertNotFalse( $jp_ai_priority, 'AI Assistant register_plugin should be hooked.' ); + $this->assertNotFalse( $disable_priority, 'disable_jetpack_ai_image_extensions should be hooked.' ); + $this->assertGreaterThan( $jp_ai_priority, $disable_priority ); + } }