From fb575fa984eb9f30ff9d7a71462524ea85304597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 15 Nov 2025 00:52:04 +0100 Subject: [PATCH 1/4] Handle noisy plugin activation responses Plugin activation step choked when PHP emitted extra output during the verification run, mis-parsing the active plugin list and throwing even though activation succeeded. I now wrap a second marker-delimited status check when unexpected output appears and added a regression test where activation and shutdown hooks echo text, confirming we log the noise and continue. --- .../src/lib/steps/activate-plugin.spec.ts | 25 +++++++ .../src/lib/steps/activate-plugin.ts | 66 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts index 030886a2a8..e6995c1cd7 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts @@ -180,6 +180,31 @@ describe('Blueprint step activatePlugin()', () => { ).resolves.not.toThrow(); }); + it('should log noisy activation output and still treat the plugin as active', async () => { + const docroot = handler.documentRoot; + php.writeFile( + `${docroot}/wp-content/plugins/noisy-plugin.php`, + ` { const docroot = handler.documentRoot; php.writeFile( diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index ad3d3a9f98..01e452f8af 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -153,6 +153,72 @@ export const activatePlugin: StepHandler = async ( if (isActiveCheckResult.text !== 'false') { logger.debug(isActiveCheckResult.text); + + const recheckMarkerStart = '__WP_PLAYGROUND_PLUGIN_STATUS_START__'; + const recheckMarkerEnd = '__WP_PLAYGROUND_PLUGIN_STATUS_END__'; + const recheckResult = await playground.run({ + code: ` Date: Sat, 15 Nov 2025 01:23:26 +0100 Subject: [PATCH 2/4] simplify the post-activation chech --- .../src/lib/steps/activate-plugin.ts | 143 +++++------------- 1 file changed, 34 insertions(+), 109 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index 01e452f8af..a6dc37fd3f 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -37,6 +37,21 @@ export const activatePlugin: StepHandler = async ( progress?.tracker.setCaption(`Activating ${pluginName || pluginPath}`); const docroot = await playground.documentRoot; + /** + * Instead of checking the plugin activation response, + * check if the plugin is active by looking at the active plugins list. + * + * We have to split the activation and the check into two PHP runs + * because some plugins might redirect during activation, + * which would prevent any output that happens after activation from being returned. + * + * Relying on the plugin activation response is not reliable because if the plugin activation + * produces any output, WordPress will assume it's an activation error and return a WP_Error. + * WordPress will still activate the plugin and load the required page, + * but it will also show the error as a notice in wp-admin. + * See WordPress source code for more details: + * https://github.com/WordPress/wordpress-develop/blob/6.7/src/wp-admin/includes/plugin.php#L733 + */ const activatePluginResult = await playground.run({ code: ` = async ( } /** - * Instead of checking the plugin activation response, - * check if the plugin is active by looking at the active plugins list. - * - * We have to split the activation and the check into two PHP runs - * because some plugins might redirect during activation, - * which would prevent any output that happens after activation from being returned. + * Instead of trusting the activation response, check the active plugins list. * - * Relying on the plugin activation response is not reliable because if the plugin activation - * produces any output, WordPress will assume it's an activation error and return a WP_Error. - * WordPress will still activate the plugin and load the required page, - * but it will also show the error as a notice in wp-admin. - * See WordPress source code for more details: - * https://github.com/WordPress/wordpress-develop/blob/6.7/src/wp-admin/includes/plugin.php#L733 - * - * Because some plugins can create an output, we need to use output buffering - * to ensure the 'true' response is not polluted by other outputs. - * If the plugin activation fails, we will return the buffered output as it might - * contain more information about the failure. + * We try to discard any extra output via output buffering. The script below must end by + * printing a single "1" (active) or "0" (inactive) with no other output. Any other response + * is treated as an activation failure so the caller can surface the raw text to the user. */ - const isActiveCheckResult = await playground.run({ + const activationStatusResult = await playground.run({ code: ` = async ( } $active_plugins = get_option( 'active_plugins' ); + if ( ! is_array( $active_plugins ) ) { + $active_plugins = array(); + } + ob_end_clean(); + foreach ( $active_plugins as $plugin ) { if ( substr( $plugin, 0, strlen( $relative_plugin_path ) ) === $relative_plugin_path ) { - ob_end_clean(); - die( 'true' ); + die('1'); + break; } } - die( ob_get_flush() ?: 'false' ); + die('0'); `, env: { DOCROOT: docroot, @@ -146,80 +137,14 @@ export const activatePlugin: StepHandler = async ( }, }); - if (isActiveCheckResult.text === 'true') { - // Plugin activation was successful, yay! + const rawStatus = (activationStatusResult.text ?? '').trim(); + if (rawStatus === '1') { return; } - - if (isActiveCheckResult.text !== 'false') { - logger.debug(isActiveCheckResult.text); - - const recheckMarkerStart = '__WP_PLAYGROUND_PLUGIN_STATUS_START__'; - const recheckMarkerEnd = '__WP_PLAYGROUND_PLUGIN_STATUS_END__'; - const recheckResult = await playground.run({ - code: ` Date: Tue, 18 Nov 2025 02:03:58 +0100 Subject: [PATCH 3/4] Adjust the implementation to pass the unit tests --- .../src/lib/steps/activate-plugin.spec.ts | 21 ++++++++++++++++++ .../src/lib/steps/activate-plugin.ts | 22 ++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts index e6995c1cd7..23e2997fc3 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts @@ -205,6 +205,27 @@ describe('Blueprint step activatePlugin()', () => { ).resolves.not.toThrow(); }); + it('should throw an error if the plugin was not activated and noisy output is present', async () => { + const docroot = handler.documentRoot; + php.writeFile( + `${docroot}/wp-content/plugins/noisy-plugin.php`, + ` { const docroot = handler.documentRoot; php.writeFile( diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index a6dc37fd3f..f041260e17 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -123,13 +123,19 @@ export const activatePlugin: StepHandler = async ( } ob_end_clean(); - foreach ( $active_plugins as $plugin ) { - if ( substr( $plugin, 0, strlen( $relative_plugin_path ) ) === $relative_plugin_path ) { - die('1'); - break; + /** + * Use a shutdown function to ensure the activation-related output comes + * last in stdout. + */ + register_shutdown_function( function() use ( $relative_plugin_path, $active_plugins ) { + foreach ( $active_plugins as $plugin ) { + if ( substr( $plugin, 0, strlen( $relative_plugin_path ) ) === $relative_plugin_path ) { + die('{"success": true}'); + break; + } } - } - die('0'); + die('{"success": false}'); + }); `, env: { DOCROOT: docroot, @@ -138,10 +144,10 @@ export const activatePlugin: StepHandler = async ( }); const rawStatus = (activationStatusResult.text ?? '').trim(); - if (rawStatus === '1') { + if (rawStatus.endsWith('{"success": true}')) { return; } - if (rawStatus !== '0') { + if (rawStatus !== '{"success": false}') { logger.debug(rawStatus); } From d5e3bee92164932c4e8cb7f66a4c039c7f68d886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 11:22:19 +0100 Subject: [PATCH 4/4] Change activation response format to JSON Updated the activation response handling to check for JSON success output. --- .../playground/blueprints/src/lib/steps/activate-plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index f041260e17..300c34cfcb 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -98,9 +98,9 @@ export const activatePlugin: StepHandler = async ( /** * Instead of trusting the activation response, check the active plugins list. * - * We try to discard any extra output via output buffering. The script below must end by - * printing a single "1" (active) or "0" (inactive) with no other output. Any other response - * is treated as an activation failure so the caller can surface the raw text to the user. + * We try to discard any extra output via output buffering. The output of the script below + * end with `{"success": true}` or `{"success": false}`. Only `{"success": true}` is + * treated as a successful plugin activation. */ const activationStatusResult = await playground.run({ code: `