From c8a6e1d337568b2fefcd86885c587089f2028988 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 14 Sep 2026 08:13:04 +0000 Subject: [PATCH 1/4] Make the PHP built-in web server step work without server-command `start_php_server()` always passed `{RUN_DIR}/vendor/wp-cli/server-command/router.php` as the router script, but no built-in step puts wp-cli/server-command into the run directory (`Given a WP installation with Composer` only requires WordPress core). PHP then fails every request with a fatal error while trying to open the router, and with `display_errors=On` that error page is even served with a 200 status, so a scenario serving static files gets an error page instead of its fixture and a missing file never yields a 404. Only pass the router when it actually exists, looking in the run directory first and then in the vendor directory of the project under test. Without a router, PHP's built-in web server serves the directory as-is, which is what a plain directory of files needs. The `Then the HTTP status code should be` step relied on the Requests v1 class, which WP-CLI no longer autoloads, so it failed with a fatal error before the request was made. Use whichever Requests version WP-CLI ships. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019g8sd6U4w59qiQW7LTt1jG --- features/behat-steps.feature | 9 +++++++ src/Context/FeatureContext.php | 40 ++++++++++++++++++++++++++--- src/Context/ThenStepDefinitions.php | 11 +++++--- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 399bf0949..c9f1c9551 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -403,6 +403,15 @@ Feature: Test that WP-CLI Behat steps work as expected No syntax errors detected """ + Scenario: Test "a PHP built-in web server" step in a plain directory + Given an empty directory + And an index.html file: + """ + Hello from the built-in web server + """ + And a PHP built-in web server + Then the HTTP status code should be 200 + @require-wp Scenario: Test background process launch Given a WP installation diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4e6434b04..db5a023b9 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -2122,16 +2122,50 @@ public function start_php_server( $subdir = '' ): void { $dir .= trim( $subdir, '/' ) . '/'; } $cmd = Utils\esc_cmd( - '%s -S %s -t %s -c %s %s', + '%s -S %s -t %s -c %s', Utils\get_php_binary(), 'localhost:8080', $dir, - get_cfg_var( 'cfg_file_path' ), - $this->variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php' + get_cfg_var( 'cfg_file_path' ) ); + + // Route requests through the router script of wp-cli/server-command when + // it is available, so that a WordPress installation gets pretty permalinks. + // Without a router, PHP's built-in web server serves the directory as-is, + // which is what a plain directory of static files needs. Passing a router + // path that does not exist would make every request fail with a fatal error. + $router = $this->get_php_server_router(); + if ( null !== $router ) { + $cmd .= ' ' . escapeshellarg( $router ); + } + $this->background_proc( $cmd ); } + /** + * Locate the router script of wp-cli/server-command, if it is installed. + * + * The run directory is checked first, so that a scenario which installs + * the package itself takes precedence, followed by the vendor directory of + * the project under test. + * + * @return string|null Absolute path to the router script, or null if none was found. + */ + private function get_php_server_router(): ?string { + $candidates = [ + $this->variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php', + self::get_vendor_dir() . '/wp-cli/server-command/router.php', + ]; + + foreach ( $candidates as $candidate ) { + if ( is_file( $candidate ) ) { + return $candidate; + } + } + + return null; + } + /** * @param string $cmd */ diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 294a20ed8..2dc700206 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -5,8 +5,8 @@ use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; use WP_CLI\Path; +use WP_CLI\RequestsLibrary; use Exception; -use Requests; use RuntimeException; trait ThenStepDefinitions { @@ -613,8 +613,11 @@ public function then_an_email_should_be_sent( $expected ): void { * @param int $return_code Expected HTTP status code. */ public function then_the_http_status_code_should_be( $return_code ): void { - // @phpstan-ignore staticMethod.deprecatedClass - $response = Requests::request( 'http://localhost:8080' ); - $this->assert_equals( $return_code, $response->status_code ); + // Use whichever version of the Requests library WP-CLI ships with. + RequestsLibrary::register_autoloader(); + $requests_class = RequestsLibrary::get_class_name(); + + $response = $requests_class::request( 'http://localhost:8080' ); + $this->assert_equals( (int) $return_code, (int) $response->status_code ); } } From ced919e296cc1ae31580d820375940e24de75ca4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 14 Sep 2026 08:25:41 +0000 Subject: [PATCH 2/4] Honor a custom Composer vendor-dir and assert the served response Look up the server-command router in the vendor directory configured in the run directory's composer.json, so that a WP installation with a custom vendor directory is covered as well. Extend the scenarios to check the response body and a real 404, so that they fail against the old behavior of serving PHP's fatal-error page with a 200 status. PHP's built-in web server falls back to index.html for unknown paths when it exists, so the 404 case lives in its own scenario without an index file. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019g8sd6U4w59qiQW7LTt1jG --- features/behat-steps.feature | 48 ++++++++++++++++++++++++++++++++++ src/Context/FeatureContext.php | 48 ++++++++++++++++++++++++++++------ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index c9f1c9551..6a6fe15bc 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -409,9 +409,57 @@ Feature: Test that WP-CLI Behat steps work as expected """ Hello from the built-in web server """ + And a fetch.php file: + """ + variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php', - self::get_vendor_dir() . '/wp-cli/server-command/router.php', + $this->variables['RUN_DIR'] . '/' . $this->get_run_dir_composer_vendor_dir(), + self::get_vendor_dir(), ]; - foreach ( $candidates as $candidate ) { - if ( is_file( $candidate ) ) { - return $candidate; + foreach ( $candidates as $vendor_dir ) { + $router = $vendor_dir . '/wp-cli/server-command/router.php'; + if ( is_file( $router ) ) { + return $router; } } return null; } + /** + * Get the Composer vendor directory configured in the run directory. + * + * Honors a custom `vendor-dir` in the run directory's composer.json, as set + * up by `Given a WP installation with Composer and a custom vendor directory`. + * + * @return string Vendor directory, relative to the run directory. + */ + private function get_run_dir_composer_vendor_dir(): string { + $composer_json = $this->variables['RUN_DIR'] . '/composer.json'; + + if ( ! is_file( $composer_json ) ) { + return 'vendor'; + } + + $composer_data = json_decode( (string) file_get_contents( $composer_json ), true ); + + if ( + is_array( $composer_data ) + && isset( $composer_data['config'] ) + && is_array( $composer_data['config'] ) + && isset( $composer_data['config']['vendor-dir'] ) + && is_string( $composer_data['config']['vendor-dir'] ) + && '' !== $composer_data['config']['vendor-dir'] + ) { + return trim( $composer_data['config']['vendor-dir'], '/' ); + } + + return 'vendor'; + } + /** * @param string $cmd */ From c1c30829e793d61881e6d080c79d5dc3360acd47 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 14 Sep 2026 08:27:36 +0000 Subject: [PATCH 3/4] Avoid $argv in the feature file fixture PHPStan flags `$argv` as possibly undefined in the PHP blocks of feature files, so read the argument from `$_SERVER['argv']` instead. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019g8sd6U4w59qiQW7LTt1jG --- features/behat-steps.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 6a6fe15bc..b2fc57b78 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -412,7 +412,7 @@ Feature: Test that WP-CLI Behat steps work as expected And a fetch.php file: """ Date: Mon, 14 Sep 2026 08:28:51 +0000 Subject: [PATCH 4/4] Use get_headers() in the feature file fixture PHP 8.5 deprecates the `$http_response_header` variable, and the deprecation notice on STDERR fails the `I run` step. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019g8sd6U4w59qiQW7LTt1jG --- features/behat-steps.feature | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index b2fc57b78..1620e011c 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -412,8 +412,9 @@ Feature: Test that WP-CLI Behat steps work as expected And a fetch.php file: """