From cf8200ef6d17a10b054acc2b98c8c6d0439abbed Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Jul 2026 23:55:37 -0700 Subject: [PATCH 1/7] Introduce environment variables to override the default speculative loading mode/eagerness --- src/wp-includes/speculative-loading.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index 319e04d36ddde..bcdb48c1ed60a 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -59,8 +59,24 @@ function wp_get_speculation_rules_configuration(): ?array { } // Sanitize the configuration and replace 'auto' with current defaults. - $default_mode = 'prefetch'; - $default_eagerness = 'conservative'; + if ( + isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) && + is_string( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) && + WP_Speculation_Rules::is_valid_mode( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) + ) { + $default_mode = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE']; + } else { + $default_mode = 'prefetch'; + } + if ( + isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) && + is_string( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) && + WP_Speculation_Rules::is_valid_eagerness( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) + ) { + $default_eagerness = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS']; + } else { + $default_eagerness = 'conservative'; + } if ( ! is_array( $config ) ) { return array( 'mode' => $default_mode, From 50289aa3eefb581188ec2016a675c68cce1cccbe Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 00:00:58 -0700 Subject: [PATCH 2/7] Allow passing mixed values to is_valid_mode/is_valid_eagerness --- src/wp-includes/class-wp-speculation-rules.php | 18 ++++++++++++------ src/wp-includes/speculative-loading.php | 2 -- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-speculation-rules.php b/src/wp-includes/class-wp-speculation-rules.php index bca9e28a383a8..45e6f3ff0be0a 100644 --- a/src/wp-includes/class-wp-speculation-rules.php +++ b/src/wp-includes/class-wp-speculation-rules.php @@ -259,24 +259,30 @@ private function is_valid_id( string $id ): bool { * Checks whether the given speculation rules mode is valid. * * @since 6.8.0 + * @since 7.1.0 The $mode param now allows mixed and not just string. * - * @param string $mode Speculation rules mode. + * @param mixed $mode Speculation rules mode. * @return bool True if valid, false otherwise. + * + * @phpstan-assert-if-true 'prefetch'|'prerender' $mode */ - public static function is_valid_mode( string $mode ): bool { - return isset( self::$mode_allowlist[ $mode ] ); + public static function is_valid_mode( $mode ): bool { + return is_string( $mode ) && isset( self::$mode_allowlist[ $mode ] ); } /** * Checks whether the given speculation rules eagerness is valid. * * @since 6.8.0 + * @since 7.1.0 The $eagerness param now allows mixed and not just string. * - * @param string $eagerness Speculation rules eagerness. + * @param mixed $eagerness Speculation rules eagerness. * @return bool True if valid, false otherwise. + * + * @phpstan-assert-if-true 'conservative'|'moderate'|'eager'|'immediate' $eagerness */ - public static function is_valid_eagerness( string $eagerness ): bool { - return isset( self::$eagerness_allowlist[ $eagerness ] ); + public static function is_valid_eagerness( $eagerness ): bool { + return is_string( $eagerness ) && isset( self::$eagerness_allowlist[ $eagerness ] ); } /** diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index bcdb48c1ed60a..354a53a4630f9 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -61,7 +61,6 @@ function wp_get_speculation_rules_configuration(): ?array { // Sanitize the configuration and replace 'auto' with current defaults. if ( isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) && - is_string( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) && WP_Speculation_Rules::is_valid_mode( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) ) { $default_mode = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE']; @@ -70,7 +69,6 @@ function wp_get_speculation_rules_configuration(): ?array { } if ( isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) && - is_string( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) && WP_Speculation_Rules::is_valid_eagerness( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) ) { $default_eagerness = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS']; From 90f2ab5040e88eeeefc30af21560e38509ef2573 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 00:02:29 -0700 Subject: [PATCH 3/7] Add return type array shape to wp_get_speculation_rules_configuration() --- src/wp-includes/speculative-loading.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index 354a53a4630f9..4b16b0a7d4e35 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -14,6 +14,10 @@ * * @return array|null Associative array with 'mode' and 'eagerness' keys, or null if speculative * loading is disabled. + * @phpstan-return array{ + * mode: 'prefetch'|'prerender', + * eagerness: 'conservative'|'moderate'|'eager'|'immediate', + * }|null */ function wp_get_speculation_rules_configuration(): ?array { // By default, speculative loading is only enabled for sites with pretty permalinks when no user is logged in. From 652ba8f1252cfa9ebd8f528111f8d4bb91efd027 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 00:07:29 -0700 Subject: [PATCH 4/7] Address argument.type error This fixes the last PHPStan level 10 error in the file: > Parameter #1 $callback of function array_map expects (callable(mixed): mixed)|null, Closure(string): string given. --- src/wp-includes/speculative-loading.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index 4b16b0a7d4e35..a07a459c71159 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -170,6 +170,7 @@ function wp_get_speculation_rules(): ?WP_Speculation_Rules { * @param string $mode Mode used to apply speculative loading. Either 'prefetch' or 'prerender'. */ $href_exclude_paths = (array) apply_filters( 'wp_speculation_rules_href_exclude_paths', array(), $mode ); + $href_exclude_paths = array_filter( $href_exclude_paths, 'is_string' ); // Ensure that: // 1. There are no duplicates. From 28304efffba88c7698bb8a3d109f1a41215431ae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 00:09:44 -0700 Subject: [PATCH 5/7] Add since tag --- src/wp-includes/speculative-loading.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index a07a459c71159..45fa04a4cb198 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -11,6 +11,8 @@ * Returns the speculation rules configuration. * * @since 6.8.0 + * @since 7.1.0 Environment variables `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` + * now can specify the default mode and eagerness, respectively. * * @return array|null Associative array with 'mode' and 'eagerness' keys, or null if speculative * loading is disabled. From da32ce6c66dcc8a82d2020dab13868725da7de18 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 00:49:00 -0700 Subject: [PATCH 6/7] Read default overrides via getenv()/constant and forbid immediate eagerness Fix two problems with the speculative loading default overrides: - `$_ENV` is only populated when `variables_order` includes `E`, which is not the case for the `php.ini-production` and `php.ini-development` files shipped with PHP. The overrides were therefore silently ignored on a stock install even when the environment variable was in fact set. Read them with `getenv()` instead, and let a constant of the same name take precedence, consistent with how `wp_get_environment_type()` resolves `WP_ENVIRONMENT_TYPE`. This also gives hosts the `wp-config.php` route. - `immediate` passed `is_valid_eagerness()` and so could be supplied as the default eagerness, bypassing the guard that keeps it out of the document-level rules that WordPress generates. `WP_Speculation_Rules::add_rule()` then rejected the main rule, leaving the site with no speculation rules at all. Move the default resolution into `wp_get_speculation_rules_default_configuration()` so that the overrides only change what `auto` resolves to. An explicit mode or eagerness supplied via the `wp_speculation_rules_configuration` filter continues to take precedence over a host default. Add unit tests covering both, including the constant/environment variable precedence and the rejection of `immediate`. Co-Authored-By: Claude Opus 4.8 --- src/wp-includes/speculative-loading.php | 105 ++++++-- .../wpGetSpeculationRules.php | 46 ++++ .../wpGetSpeculationRulesConfiguration.php | 66 +++++ ...etSpeculationRulesDefaultConfiguration.php | 231 ++++++++++++++++++ 4 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php diff --git a/src/wp-includes/speculative-loading.php b/src/wp-includes/speculative-loading.php index 45fa04a4cb198..dd33cc5395ab5 100644 --- a/src/wp-includes/speculative-loading.php +++ b/src/wp-includes/speculative-loading.php @@ -11,14 +11,16 @@ * Returns the speculation rules configuration. * * @since 6.8.0 - * @since 7.1.0 Environment variables `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` - * now can specify the default mode and eagerness, respectively. + * @since 7.1.0 The `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` constants and + * environment variables can now specify the default mode and eagerness, respectively. + * + * @see wp_get_speculation_rules_default_configuration() * * @return array|null Associative array with 'mode' and 'eagerness' keys, or null if speculative * loading is disabled. * @phpstan-return array{ * mode: 'prefetch'|'prerender', - * eagerness: 'conservative'|'moderate'|'eager'|'immediate', + * eagerness: 'conservative'|'moderate'|'eager', * }|null */ function wp_get_speculation_rules_configuration(): ?array { @@ -65,22 +67,10 @@ function wp_get_speculation_rules_configuration(): ?array { } // Sanitize the configuration and replace 'auto' with current defaults. - if ( - isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) && - WP_Speculation_Rules::is_valid_mode( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE'] ) - ) { - $default_mode = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_MODE']; - } else { - $default_mode = 'prefetch'; - } - if ( - isset( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) && - WP_Speculation_Rules::is_valid_eagerness( $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS'] ) - ) { - $default_eagerness = $_ENV['WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS']; - } else { - $default_eagerness = 'conservative'; - } + $defaults = wp_get_speculation_rules_default_configuration(); + $default_mode = $defaults['mode']; + $default_eagerness = $defaults['eagerness']; + if ( ! is_array( $config ) ) { return array( 'mode' => $default_mode, @@ -110,6 +100,83 @@ function wp_get_speculation_rules_configuration(): ?array { ); } +/** + * Returns the default speculation rules configuration that the value 'auto' resolves to. + * + * WordPress Core defaults to a mode of 'prefetch' and an eagerness of 'conservative'. Hosting providers can override + * either default by way of the `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` + * constants or environment variables. This only changes what the 'auto' value resolves to, so a plugin which supplies + * an explicit mode or eagerness via the {@see 'wp_speculation_rules_configuration'} filter continues to take + * precedence. + * + * Note that an eagerness of 'immediate' is not permitted as a default, since WordPress does not allow it for the + * document-level rules that it generates. + * + * @since 7.1.0 + * @access private + * + * @return array Associative array with 'mode' and 'eagerness' keys. + * @phpstan-return array{ + * mode: 'prefetch'|'prerender', + * eagerness: 'conservative'|'moderate'|'eager', + * } + */ +function wp_get_speculation_rules_default_configuration(): array { + $default_mode = 'prefetch'; + $mode = wp_get_speculative_loading_override( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); + if ( WP_Speculation_Rules::is_valid_mode( $mode ) ) { + $default_mode = $mode; + } + + $default_eagerness = 'conservative'; + $eagerness = wp_get_speculative_loading_override( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + if ( + WP_Speculation_Rules::is_valid_eagerness( $eagerness ) && + // 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules. + 'immediate' !== $eagerness + ) { + $default_eagerness = $eagerness; + } + + return array( + 'mode' => $default_mode, + 'eagerness' => $default_eagerness, + ); +} + +/** + * Returns the value of a speculative loading override, as supplied by a constant or an environment variable. + * + * The constant takes precedence over the environment variable, consistent with {@see wp_get_environment_type()}. + * + * @since 7.1.0 + * @access private + * + * @param string $name Name of the constant and environment variable to look up. + * @return string|null The override value, or null if neither is set. + */ +function wp_get_speculative_loading_override( string $name ): ?string { + $value = null; + + // Check if the environment variable has been set, if `getenv` is available on the system. + if ( function_exists( 'getenv' ) ) { + $has_env = getenv( $name ); + if ( false !== $has_env ) { + $value = $has_env; + } + } + + // Fetch the value from a constant, which overrides the environment variable. + if ( defined( $name ) ) { + $has_constant = constant( $name ); + if ( is_string( $has_constant ) ) { + $value = $has_constant; + } + } + + return $value; +} + /** * Returns the full speculation rules data based on the configuration. * diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php index a7b5594bdf0d1..4a9cd75a68d2b 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php @@ -41,6 +41,13 @@ static function () { update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } + public function tear_down(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + + parent::tear_down(); + } + /** * Tests speculation rules output with prefetch for the different eagerness levels. * @@ -558,4 +565,43 @@ function () { $this->assertSame( 'moderate', $rules['prerender'][0]['eagerness'] ); $this->assertSame( 'eager', $rules['prerender'][1]['eagerness'] ); } + + /** + * Tests that an overridden default eagerness is applied to the main document-level rule. + * + * @ticket 65624 + */ + public function test_wp_get_speculation_rules_with_overridden_default_eagerness(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' ); + + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertCount( 1, $rules['prefetch'] ); + $this->assertSame( 'moderate', $rules['prefetch'][0]['eagerness'] ); + } + + /** + * Tests that an eagerness of 'immediate' cannot be forced as the default. + * + * WordPress does not allow 'immediate' for the document-level rules it generates, so allowing it as a default + * would cause the main rule to be rejected, leaving no speculation rules at all. + * + * @ticket 65624 + */ + public function test_wp_get_speculation_rules_with_immediate_default_eagerness(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=immediate' ); + + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertCount( 1, $rules['prefetch'] ); + $this->assertSame( 'conservative', $rules['prefetch'][0]['eagerness'] ); + } } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php index a4eb9c8a6878b..20f305d4ae828 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php @@ -18,6 +18,13 @@ public function set_up() { update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } + public function tear_down(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + + parent::tear_down(); + } + /** * Tests that the default configuration is the expected value. * @@ -264,4 +271,63 @@ public static function data_wp_get_speculation_rules_configuration_filter(): arr ), ); } + + /** + * Tests that an overridden default is what the 'auto' value resolves to. + * + * @ticket 65624 + */ + public function test_wp_get_speculation_rules_configuration_with_overridden_default(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' ); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + wp_get_speculation_rules_configuration() + ); + } + + /** + * Tests that an explicit eagerness from the filter takes precedence over an overridden default. + * + * This ensures a plugin such as Speculative Loading continues to win over a hosting provider's default. + * + * @ticket 65624 + */ + public function test_wp_get_speculation_rules_configuration_filter_beats_overridden_default(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' ); + + add_filter( + 'wp_speculation_rules_configuration', + static function () { + return array( + 'mode' => 'auto', + 'eagerness' => 'conservative', + ); + } + ); + + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + wp_get_speculation_rules_configuration() + ); + } + + /** + * Tests that speculative loading remains disabled when a default is overridden. + * + * @ticket 65624 + */ + public function test_wp_get_speculation_rules_configuration_with_overridden_default_while_disabled(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' ); + + add_filter( 'wp_speculation_rules_configuration', '__return_null' ); + + $this->assertNull( wp_get_speculation_rules_configuration() ); + } } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php new file mode 100644 index 0000000000000..bf319d7cdf046 --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php @@ -0,0 +1,231 @@ +assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + wp_get_speculation_rules_default_configuration() + ); + } + + /** + * Tests that the defaults can be overridden by environment variables. + * + * @ticket 65624 + * @dataProvider data_environment_variables + * + * @param string|null $mode Value for the mode environment variable, or null to leave it unset. + * @param string|null $eagerness Value for the eagerness environment variable, or null to leave it unset. + * @param array $expected Expected default configuration. + */ + public function test_environment_variables( ?string $mode, ?string $eagerness, array $expected ): void { + if ( null !== $mode ) { + putenv( "WP_SPECULATIVE_LOADING_DEFAULT_MODE={$mode}" ); + } + if ( null !== $eagerness ) { + putenv( "WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS={$eagerness}" ); + } + + $this->assertSame( $expected, wp_get_speculation_rules_default_configuration() ); + } + + /** + * Data provider. + * + * @return array Test parameters, keyed by test case name. Each entry consists of the value for the mode environment + * variable, the value for the eagerness environment variable, and the expected default configuration. A + * `null` environment variable value means the variable is left unset. + */ + public static function data_environment_variables(): array { + return array( + 'moderate eagerness' => array( + null, + 'moderate', + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + ), + 'eager eagerness' => array( + null, + 'eager', + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + ), + 'prerender mode' => array( + 'prerender', + null, + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + 'both mode and eagerness' => array( + 'prerender', + 'moderate', + array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ), + ), + /* + * 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules, + * so it must not be usable as the default either. + */ + 'immediate eagerness' => array( + null, + 'immediate', + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'invalid mode' => array( + 'invalid', + null, + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'invalid eagerness' => array( + null, + 'invalid', + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'empty string' => array( + '', + '', + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'invalid mode, valid eagerness' => array( + 'invalid', + 'moderate', + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + ), + 'valid mode, invalid eagerness' => array( + 'prerender', + 'invalid', + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + 'wrong case is not normalized' => array( + 'PREFETCH', + 'MODERATE', + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'auto is not a valid env value' => array( + 'auto', + 'auto', + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + ); + } + + /** + * Tests that the defaults can be overridden by constants. + * + * @ticket 65624 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_constants(): void { + define( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'prerender' ); + define( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS', 'moderate' ); + + $this->assertSame( + array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ), + wp_get_speculation_rules_default_configuration() + ); + } + + /** + * Tests that a constant takes precedence over an environment variable. + * + * @ticket 65624 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_constant_overrides_environment_variable(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=eager' ); + define( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS', 'moderate' ); + + $config = wp_get_speculation_rules_default_configuration(); + + $this->assertSame( 'moderate', $config['eagerness'] ); + } + + /** + * Tests that an invalid constant falls back to the Core default rather than to the environment variable. + * + * @ticket 65624 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_invalid_constant_falls_back_to_core_default(): void { + putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=eager' ); + define( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS', 'invalid' ); + + $config = wp_get_speculation_rules_default_configuration(); + + $this->assertSame( 'conservative', $config['eagerness'] ); + } +} From 522aa2bd782436a404c1acff9fb1c67cdaab9238 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Jul 2026 20:21:13 -0700 Subject: [PATCH 7/7] Speculative Loading: Preserve and restore env vars in tests. The speculative loading tests unset the `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` environment variables in `tear_down()` without restoring any values present before the test run. Capture the original values in `set_up()` and restore them in `tear_down()` so the suite stays deterministic when these vars are pre-set in a developer or CI environment. Also add native type hints for the new property and `set_up()` return types. Co-Authored-By: Claude Opus 4.8 --- .../wpGetSpeculationRules.php | 22 ++++++++++++++--- .../wpGetSpeculationRulesConfiguration.php | 22 ++++++++++++++--- ...etSpeculationRulesDefaultConfiguration.php | 24 +++++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php index 4a9cd75a68d2b..a4854db0e41d6 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php @@ -21,9 +21,20 @@ class Tests_Speculative_Loading_wpGetSpeculationRules extends WP_UnitTestCase { 'eagerness' => 'conservative', ); - public function set_up() { + /** + * Stores the original speculative loading env values for cleanup. + * + * @var array + */ + private array $original_env = array(); + + public function set_up(): void { parent::set_up(); + foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) { + $this->original_env[ $name ] = getenv( $name ); + } + add_filter( 'template_directory_uri', static function () { @@ -42,8 +53,13 @@ static function () { } public function tear_down(): void { - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + foreach ( $this->original_env as $name => $value ) { + if ( false === $value ) { + putenv( $name ); + } else { + putenv( "{$name}={$value}" ); + } + } parent::tear_down(); } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php index 20f305d4ae828..2fb5c300198f8 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php @@ -12,15 +12,31 @@ */ class Tests_Speculative_Loading_wpGetSpeculationRulesConfiguration extends WP_UnitTestCase { - public function set_up() { + /** + * Stores the original speculative loading env values for cleanup. + * + * @var array + */ + private array $original_env = array(); + + public function set_up(): void { parent::set_up(); + foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) { + $this->original_env[ $name ] = getenv( $name ); + } + update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } public function tear_down(): void { - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + foreach ( $this->original_env as $name => $value ) { + if ( false === $value ) { + putenv( $name ); + } else { + putenv( "{$name}={$value}" ); + } + } parent::tear_down(); } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php index bf319d7cdf046..59a27818c07f7 100644 --- a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesDefaultConfiguration.php @@ -12,9 +12,29 @@ */ class Tests_Speculative_Loading_wpGetSpeculationRulesDefaultConfiguration extends WP_UnitTestCase { + /** + * Stores the original speculative loading env values for cleanup. + * + * @var array + */ + private array $original_env = array(); + + public function set_up(): void { + parent::set_up(); + + foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) { + $this->original_env[ $name ] = getenv( $name ); + } + } + public function tear_down(): void { - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' ); - putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ); + foreach ( $this->original_env as $name => $value ) { + if ( false === $value ) { + putenv( $name ); + } else { + putenv( "{$name}={$value}" ); + } + } parent::tear_down(); }