From e8dff071bd29adf6bb5d370867305318f0ba9647 Mon Sep 17 00:00:00 2001 From: Pratik Nawkar Date: Thu, 9 Apr 2026 11:21:35 +0530 Subject: [PATCH 1/2] #64066 Add unit tests: Moderate speculation eagerness when caches detected --- .../includes/class-wp-site-health.php | 53 ++++- src/wp-includes/speculative-loading.php | 22 ++ tests/phpunit/tests/admin/wpSiteHealth.php | 76 +++++++ .../wpGetSpeculationRulesConfiguration.php | 197 ++++++++++++++++++ 4 files changed, 342 insertions(+), 6 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 44c04175abaf2..32df491c72c82 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2447,6 +2447,7 @@ public function get_test_page_cache() { $description = '

' . __( 'Page cache enhances the speed and performance of your site by saving and serving static pages instead of calling for a page every time a user visits.' ) . '

'; $description .= '

' . __( 'Page cache is detected by looking for an active page cache plugin as well as making three requests to the homepage and looking for one or more of the following HTTP client caching response headers:' ) . '

'; $description .= '' . implode( ', ', array_keys( $this->get_page_cache_headers() ) ) . '.'; + $description .= '

' . $this->get_speculative_loading_cache_description() . '

'; $result = array( 'badge' => array( @@ -2580,8 +2581,9 @@ public function get_test_persistent_object_cache() { ), 'label' => __( 'A persistent object cache is being used' ), 'description' => sprintf( - '

%s

', - __( 'A persistent object cache makes your site’s database more efficient, resulting in faster load times because WordPress can retrieve your site’s content and settings much more quickly.' ) + '

%s

%s

', + __( 'A persistent object cache makes your site’s database more efficient, resulting in faster load times because WordPress can retrieve your site’s content and settings much more quickly.' ), + $this->get_speculative_loading_cache_description() ), 'actions' => sprintf( '

%s %s

', @@ -2647,6 +2649,32 @@ public function get_test_persistent_object_cache() { return $result; } + /** + * Gets the description of speculative loading used in the page cache and persistent object cache tests. + * + * @since 6.9.0 + * + * @see self::get_test_persistent_object_cache() + * @see self::get_test_page_cache() + * @see wp_get_speculation_rules_configuration() + * + * @return string Description. + */ + private function get_speculative_loading_cache_description() { + return sprintf( + /* translators: 1: Link to the Speculative Loading dev note. 2: Additional link attributes. 3: Accessibility text. */ + __( 'When a page cache is detected and a persistent object cache is enabled, Speculative Loading%3$s will accelerate cross-site navigations by using a default eagerness of moderate instead of conservative.' ), + /* translators: Localized Speculative Loading dev note, if one exists. */ + esc_url( __( 'https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/' ) ), + 'target="_blank"', + sprintf( + ' %s', + /* translators: Hidden accessibility text. */ + __( '(opens in a new tab)' ) + ) + ); + } + /** * Calculates total amount of autoloaded data. * @@ -3603,9 +3631,10 @@ public function get_page_cache_headers(): array { * @return WP_Error|array { * Page cache detection details or else error information. * - * @type bool $advanced_cache_present Whether a page cache plugin is present. - * @type array[] $page_caching_response_headers Sets of client caching headers for the responses. - * @type float[] $response_timing Response timings. + * @type bool $advanced_cache_present Whether a page cache plugin is present. + * @type array[] $page_caching_response_headers Sets of client caching headers for the responses. + * @type float[] $response_timing Response timings. + * @type string[] $caching_response_headers List of response headers detected which indicate page caching. * } */ private function check_for_page_caching() { @@ -3628,6 +3657,7 @@ private function check_for_page_caching() { $page_caching_response_headers = array(); $response_timing = array(); + $all_seen_headers = array(); for ( $i = 1; $i <= 3; $i++ ) { $start_time = microtime( true ); $http_response = wp_remote_get( home_url( '/' ), compact( 'sslverify', 'headers' ) ); @@ -3653,6 +3683,7 @@ private function check_for_page_caching() { $header_values = (array) $header_values; if ( empty( $callback ) || ( is_callable( $callback ) && count( array_filter( $header_values, $callback ) ) > 0 ) ) { $response_headers[ $header ] = $header_values; + $all_seen_headers[] = $header; } } @@ -3660,7 +3691,7 @@ private function check_for_page_caching() { $response_timing[] = ( $end_time - $start_time ) * 1000; } - return array( + $result = array( 'advanced_cache_present' => ( file_exists( WP_CONTENT_DIR . '/advanced-cache.php' ) && @@ -3671,7 +3702,17 @@ private function check_for_page_caching() { ), 'page_caching_response_headers' => $page_caching_response_headers, 'response_timing' => $response_timing, + 'caching_response_headers' => array_unique( $all_seen_headers ), ); + + /* + * Store the results in a non-expiring (autoloaded) transient so that Speculative Loading can use this to change + * the default mode from conservative to moderate when page caching is enabled. + * See wp_get_speculation_rules_configuration(). + */ + set_transient( 'health_check_page_cache_detail', $result ); + + return $result; } /** diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index 319e04d36ddde..43caa7bd99def 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -61,6 +61,28 @@ function wp_get_speculation_rules_configuration(): ?array { // Sanitize the configuration and replace 'auto' with current defaults. $default_mode = 'prefetch'; $default_eagerness = 'conservative'; + + // Default to moderate eagerness on production when page caching is detected and a persistent object cache is used. + if ( 'production' === wp_get_environment_type() && wp_using_ext_object_cache() ) { + $page_cache_detail = get_transient( 'health_check_page_cache_detail' ); + if ( + is_array( $page_cache_detail ) && + ( + ( + isset( $page_cache_detail['advanced_cache_present'] ) && + $page_cache_detail['advanced_cache_present'] + ) || + ( + isset( $page_cache_detail['caching_response_headers'] ) && + is_array( $page_cache_detail['caching_response_headers'] ) && + count( $page_cache_detail['caching_response_headers'] ) > 0 + ) + ) + ) { + $default_eagerness = 'moderate'; + } + } + if ( ! is_array( $config ) ) { return array( 'mode' => $default_mode, diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 6080b477f54c3..98da43b44f615 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -707,4 +707,80 @@ public function test_get_test_opcode_cache_result_by_environment() { $this->assertStringContainsString( __( 'Enabling this cache can significantly improve the performance of your site.' ), $result['description'] ); } } + + /** + * @ticket 64066 + * + * @covers ::check_for_page_caching + */ + public function test_check_for_page_caching_stores_health_check_page_cache_detail_transient() { + delete_transient( 'health_check_page_cache_detail' ); + + $pre_http_response = function () { + return array( + 'headers' => array( 'age' => '100' ), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + ); + }; + add_filter( 'pre_http_request', $pre_http_response, 10 ); + + $this->instance->get_test_page_cache(); + + $detail = get_transient( 'health_check_page_cache_detail' ); + + remove_filter( 'pre_http_request', $pre_http_response, 10 ); + + $this->assertIsArray( $detail ); + $this->assertArrayHasKey( 'caching_response_headers', $detail ); + $this->assertContains( 'age', $detail['caching_response_headers'] ); + } + + /** + * @ticket 64066 + * + * @covers ::get_test_page_cache + */ + public function test_get_test_page_cache_description_includes_speculative_loading_note() { + delete_transient( 'health_check_page_cache_detail' ); + + $pre_http_response = function () { + return array( + 'headers' => array( 'age' => '100' ), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + ); + }; + add_filter( 'pre_http_request', $pre_http_response, 10 ); + + $result = $this->instance->get_test_page_cache(); + + remove_filter( 'pre_http_request', $pre_http_response, 10 ); + + $this->assertStringContainsString( 'Speculative Loading', $result['description'] ); + $this->assertStringContainsString( 'moderate', $result['description'] ); + $this->assertStringContainsString( 'conservative', $result['description'] ); + } + + /** + * @ticket 64066 + * + * @covers ::get_test_persistent_object_cache + */ + public function test_get_test_persistent_object_cache_includes_speculative_loading_note_when_ext_object_cache_enabled() { + $initial = wp_using_ext_object_cache(); + wp_using_ext_object_cache( true ); + + $result = $this->instance->get_test_persistent_object_cache(); + + wp_using_ext_object_cache( $initial ); + + $this->assertStringContainsString( 'Speculative Loading', $result['description'] ); + $this->assertStringContainsString( 'moderate', $result['description'] ); + $this->assertStringContainsString( 'conservative', $result['description'] ); + } } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php index a4eb9c8a6878b..f1527e4ac853d 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php @@ -12,12 +12,29 @@ */ class Tests_Speculative_Loading_wpGetSpeculationRulesConfiguration extends WP_UnitTestCase { + /** + * Whether an external object cache was in use before each test. + * + * @var bool + */ + private $initial_using_ext_object_cache; + public function set_up() { parent::set_up(); + $this->initial_using_ext_object_cache = wp_using_ext_object_cache(); + update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } + public function tear_down() { + wp_using_ext_object_cache( $this->initial_using_ext_object_cache ); + delete_transient( 'health_check_page_cache_detail' ); + // Reset for following tests: wp_get_environment_type() keeps a static when WP_RUN_CORE_TESTS is defined. + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + parent::tear_down(); + } + /** * Tests that the default configuration is the expected value. * @@ -264,4 +281,184 @@ public static function data_wp_get_speculation_rules_configuration_filter(): arr ), ); } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_uses_moderate_eagerness_when_production_persistent_object_cache_and_advanced_cache_detected() { + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + wp_using_ext_object_cache( true ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => true, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array(), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_uses_moderate_eagerness_when_production_persistent_object_cache_and_caching_headers_detected() { + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + wp_using_ext_object_cache( true ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => false, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array( 'age' ), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_remains_conservative_on_staging_even_with_caches_detected() { + putenv( 'WP_ENVIRONMENT_TYPE=staging' ); + wp_using_ext_object_cache( true ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => true, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array(), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_remains_conservative_in_non_production_even_with_caches_detected() { + putenv( 'WP_ENVIRONMENT_TYPE=development' ); + wp_using_ext_object_cache( true ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => true, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array(), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_remains_conservative_without_persistent_object_cache() { + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + wp_using_ext_object_cache( false ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => true, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array(), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_remains_conservative_when_no_health_check_page_cache_detail() { + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + wp_using_ext_object_cache( true ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config + ); + } + + /** + * @ticket 64066 + */ + public function test_wp_get_speculation_rules_configuration_remains_conservative_when_page_cache_detail_has_no_signals() { + putenv( 'WP_ENVIRONMENT_TYPE=production' ); + wp_using_ext_object_cache( true ); + set_transient( + 'health_check_page_cache_detail', + array( + 'advanced_cache_present' => false, + 'page_caching_response_headers' => array(), + 'response_timing' => array(), + 'caching_response_headers' => array(), + ) + ); + + $config = wp_get_speculation_rules_configuration(); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config + ); + } } From 39b28b05dfe70aa009627529b3b39b55a0498741 Mon Sep 17 00:00:00 2001 From: Pratik Nawkar Date: Thu, 9 Apr 2026 14:16:44 +0530 Subject: [PATCH 2/2] Moderate speculation eagerness when page + object cache; Site Health transient, PHPUnit isolation --- tests/phpunit/includes/abstract-testcase.php | 8 ++++++++ .../wpGetSpeculationRulesConfiguration.php | 20 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 3a5d52b0706a9..444ba1fc47a96 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -135,6 +135,14 @@ public function set_up() { if ( $wp_rewrite->permalink_structure ) { $this->set_permalink_structure( '' ); } + + /* + * Site Health stores page cache probe results in this transient. When a persistent object cache is used + * (common in CI), it would otherwise change the default resolved eagerness for unrelated tests. + * + * @ticket 64066 + */ + delete_transient( 'health_check_page_cache_detail' ); } $this->start_transaction(); diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php index f1527e4ac853d..283d8587c2a37 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php @@ -19,19 +19,31 @@ class Tests_Speculative_Loading_wpGetSpeculationRulesConfiguration extends WP_Un */ private $initial_using_ext_object_cache; + /** + * getenv( 'WP_ENVIRONMENT_TYPE' ) at the start of the test, for restoration in tear_down(). + * + * @var string|false + */ + private $wp_environment_type_env_before_test; + public function set_up() { parent::set_up(); - $this->initial_using_ext_object_cache = wp_using_ext_object_cache(); + $this->initial_using_ext_object_cache = wp_using_ext_object_cache(); + $this->wp_environment_type_env_before_test = getenv( 'WP_ENVIRONMENT_TYPE' ); update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } public function tear_down() { wp_using_ext_object_cache( $this->initial_using_ext_object_cache ); - delete_transient( 'health_check_page_cache_detail' ); - // Reset for following tests: wp_get_environment_type() keeps a static when WP_RUN_CORE_TESTS is defined. - putenv( 'WP_ENVIRONMENT_TYPE=production' ); + + if ( false !== $this->wp_environment_type_env_before_test && '' !== $this->wp_environment_type_env_before_test ) { + putenv( 'WP_ENVIRONMENT_TYPE=' . $this->wp_environment_type_env_before_test ); + } else { + putenv( 'WP_ENVIRONMENT_TYPE' ); + } + parent::tear_down(); }